From 33d3a41104c906c8c62e68cf43d26fb4002777db Mon Sep 17 00:00:00 2001 From: Juan Pablo Arce Date: Tue, 21 Jul 2020 08:57:50 -0300 Subject: [PATCH] (965c31410a) Unstable v0.10.4.0 --- .../BarotraumaClient/ClientSource/Camera.cs | 44 +- .../Characters/AI/HumanAIController.cs | 4 + .../Characters/Animation/Ragdoll.cs | 54 +- .../ClientSource/Characters/Character.cs | 51 +- .../ClientSource/Characters/CharacterHUD.cs | 41 +- .../ClientSource/Characters/CharacterInfo.cs | 9 +- .../Characters/CharacterNetworking.cs | 42 +- .../Characters/Health/CharacterHealth.cs | 33 +- .../ClientSource/Characters/Jobs/JobPrefab.cs | 5 +- .../ClientSource/DebugConsole.cs | 203 +- .../Events/EventActions/ConversationAction.cs | 365 + .../ClientSource/Events/EventManager.cs | 455 +- .../Events/Missions/CargoMission.cs | 3 +- .../ClientSource/Events/Missions/Mission.cs | 11 + .../ClientSource/Fonts/ScalableFont.cs | 13 +- .../ClientSource/GUI/ComponentStyle.cs | 10 + .../ClientSource/GUI/CrewManagement.cs | 729 ++ .../BarotraumaClient/ClientSource/GUI/GUI.cs | 1309 +- .../ClientSource/GUI/GUIButton.cs | 47 +- .../ClientSource/GUI/GUICanvas.cs | 2 +- .../ClientSource/GUI/GUIColorSettings.cs | 24 - .../ClientSource/GUI/GUIComponent.cs | 129 +- .../ClientSource/GUI/GUIFrame.cs | 4 +- .../ClientSource/GUI/GUIImage.cs | 35 +- .../ClientSource/GUI/GUILayoutGroup.cs | 2 +- .../ClientSource/GUI/GUIListBox.cs | 239 +- .../ClientSource/GUI/GUIMessage.cs | 4 +- .../ClientSource/GUI/GUIMessageBox.cs | 106 +- .../ClientSource/GUI/GUINumberInput.cs | 4 +- .../ClientSource/GUI/GUIProgressBar.cs | 2 +- .../ClientSource/GUI/GUIStyle.cs | 8 +- .../ClientSource/GUI/GUITextBlock.cs | 27 +- .../ClientSource/GUI/GUITextBox.cs | 5 + .../ClientSource/GUI/HUDLayoutSettings.cs | 14 +- .../ClientSource/GUI/LoadingScreen.cs | 4 +- .../ClientSource/GUI/Store.cs | 1089 ++ .../ClientSource/GUI/SubmarineSelection.cs | 683 + .../ClientSource/GUI/TabMenu.cs | 54 +- .../ClientSource/GUI/UISprite.cs | 18 +- .../ClientSource/GUI/UpgradeStore.cs | 1142 ++ .../ClientSource/GUI/VotingInterface.cs | 235 + .../BarotraumaClient/ClientSource/GameMain.cs | 136 +- .../ClientSource/GameSession/CargoManager.cs | 173 + .../ClientSource/GameSession/CrewManager.cs | 228 +- .../GameSession/GameModes/CampaignMode.cs | 268 +- .../GameModes/Data/CampaignMetadata.cs | 114 + .../GameModes/MultiPlayerCampaign.cs | 671 +- .../GameModes/SinglePlayerCampaign.cs | 987 +- .../GameSession/GameModes/SubTestMode.cs | 80 - .../GameSession/GameModes/TestGameMode.cs | 34 + .../GameModes/Tutorials/BasicTutorial.cs | 2 +- .../GameModes/Tutorials/CaptainTutorial.cs | 6 +- .../GameModes/Tutorials/DoctorTutorial.cs | 5 + .../GameModes/Tutorials/EngineerTutorial.cs | 25 +- .../GameModes/Tutorials/MechanicTutorial.cs | 24 +- .../GameModes/Tutorials/OfficerTutorial.cs | 9 +- .../GameModes/Tutorials/ScenarioTutorial.cs | 42 +- .../GameModes/Tutorials/Tutorial.cs | 4 +- .../GameModes/Tutorials/TutorialMode.cs | 9 +- .../ClientSource/GameSession/GameSession.cs | 34 +- .../ClientSource/GameSession/RoundSummary.cs | 711 +- .../GameSession/UpgradeManager.cs | 81 + .../ClientSource/GameSettings.cs | 61 +- .../ClientSource/Items/CharacterInventory.cs | 65 +- .../ClientSource/Items/Components/Door.cs | 20 +- .../Items/Components/Holdable/RangedWeapon.cs | 1 + .../Items/Components/ItemComponent.cs | 73 +- .../Items/Components/ItemContainer.cs | 18 +- .../Items/Components/ItemLabel.cs | 10 +- .../ClientSource/Items/Components/Ladder.cs | 5 +- .../Items/Components/Machines/Controller.cs | 16 + .../Components/Machines/Deconstructor.cs | 13 +- .../Items/Components/Machines/Fabricator.cs | 25 +- .../Items/Components/Machines/MiniMap.cs | 38 +- .../Components/Machines/OutpostTerminal.cs | 42 + .../Items/Components/Machines/Sonar.cs | 45 +- .../Items/Components/Machines/Steering.cs | 72 +- .../Items/Components/Power/PowerContainer.cs | 13 +- .../Items/Components/RepairTool.cs | 2 +- .../Items/Components/Repairable.cs | 84 +- .../Items/Components/Signal/Connection.cs | 2 +- .../Components/Signal/CustomInterface.cs | 15 +- .../Items/Components/Signal/Wire.cs | 2 +- .../ClientSource/Items/DockingPort.cs | 2 +- .../ClientSource/Items/Inventory.cs | 19 +- .../ClientSource/Items/Item.cs | 180 +- .../ClientSource/Items/ItemPrefab.cs | 21 +- .../BarotraumaClient/ClientSource/Map/Gap.cs | 2 +- .../BarotraumaClient/ClientSource/Map/Hull.cs | 2 +- .../ClientSource/Map/Levels/Level.cs | 12 +- .../Levels/LevelObjects/LevelObjectManager.cs | 11 +- .../ClientSource/Map/Levels/LevelRenderer.cs | 2 +- .../ClientSource/Map/Lights/ConvexHull.cs | 4 +- .../ClientSource/Map/Lights/LightManager.cs | 35 +- .../ClientSource/Map/Map/Location.cs | 50 - .../ClientSource/Map/Map/Map.cs | 969 +- .../ClientSource/Map/MapEntityPrefab.cs | 3 + .../ClientSource/Map/Structure.cs | 28 +- .../ClientSource/Map/Submarine.cs | 82 +- .../ClientSource/Map/SubmarineInfo.cs | 159 +- .../ClientSource/Map/WayPoint.cs | 55 +- .../ClientSource/Networking/ChatMessage.cs | 112 +- .../ClientSource/Networking/Client.cs | 3 + .../Networking/FileTransfer/FileReceiver.cs | 15 +- .../ClientSource/Networking/GameClient.cs | 957 +- .../Primitives/Peers/SteamP2PClientPeer.cs | 50 +- .../Primitives/Peers/SteamP2POwnerPeer.cs | 2 + .../ClientSource/Networking/ServerInfo.cs | 25 +- .../ClientSource/Networking/ServerSettings.cs | 8 + .../ClientSource/Networking/SteamManager.cs | 140 +- .../Networking/Voip/VoipCapture.cs | 33 +- .../ClientSource/Networking/Voting.cs | 146 +- .../ClientSource/Particles/ParticleEmitter.cs | 23 +- .../ClientSource/Particles/ParticlePrefab.cs | 6 + .../BarotraumaClient/ClientSource/Program.cs | 18 +- .../ClientSource/Screens/CampaignEndScreen.cs | 120 + .../ClientSource/Screens/CampaignSetupUI.cs | 192 +- .../ClientSource/Screens/CampaignUI.cs | 1158 +- .../CharacterEditor/CharacterEditorScreen.cs | 4 +- .../ClientSource/Screens/CreditsPlayer.cs | 38 +- .../Screens/EventEditor/EditorNode.cs | 662 + .../Screens/EventEditor/EventEditorScreen.cs | 1077 ++ .../Screens/EventEditor/NodeConnection.cs | 351 + .../ClientSource/Screens/GameScreen.cs | 35 +- .../ClientSource/Screens/LevelEditorScreen.cs | 216 +- .../ClientSource/Screens/LobbyScreen.cs | 94 - .../ClientSource/Screens/MainMenuScreen.cs | 44 +- .../ClientSource/Screens/NetLobbyScreen.cs | 546 +- .../Screens/ParticleEditorScreen.cs | 3 +- .../Screens/RoundSummaryScreen.cs | 55 + .../ClientSource/Screens/Screen.cs | 8 +- .../ClientSource/Screens/ServerListScreen.cs | 67 +- .../Screens/SpriteEditorScreen.cs | 2 +- .../Screens/SteamWorkshopScreen.cs | 42 +- .../ClientSource/Screens/SubEditorScreen.cs | 535 +- .../Serialization/SerializableEntityEditor.cs | 18 +- .../ClientSource/Sounds/OggSound.cs | 120 +- .../ClientSource/Sounds/OpenAL/Alc.cs | 70 +- .../ClientSource/Sounds/Sound.cs | 76 +- .../ClientSource/Sounds/SoundChannel.cs | 1 + .../ClientSource/Sounds/SoundManager.cs | 142 +- .../ClientSource/Sounds/SoundPlayer.cs | 5 +- .../ClientSource/Sprite/Sprite.cs | 15 +- .../Traitors/TraitorMissionResult.cs | 22 + .../ClientSource/Upgrades/UpgradePrefab.cs | 10 + .../Utils/LocalizationCSVtoXML.cs | 1 - .../ClientSource/Utils/Quad.cs | 2 +- .../ClientSource/Utils/TextureLoader.cs | 5 + .../ClientSource/Utils/ToolBox.cs | 57 + .../BarotraumaClient/LinuxClient.csproj | 6 +- Barotrauma/BarotraumaClient/MacClient.csproj | 4 +- .../BarotraumaClient/WindowsClient.csproj | 2 +- Barotrauma/BarotraumaClient/soft_oal_x64.dll | Bin 1401856 -> 1485824 bytes .../BarotraumaServer/LinuxServer.csproj | 6 +- Barotrauma/BarotraumaServer/MacServer.csproj | 6 +- .../Characters/CharacterNetworking.cs | 35 +- .../ServerSource/DebugConsole.cs | 56 +- .../Events/EventActions/ConversationAction.cs | 115 + .../Events/EventActions/StatusEffectAction.cs | 28 + .../ServerSource/Events/EventManager.cs | 37 + .../Events/Missions/CargoMission.cs | 5 +- .../Events/Missions/MonsterMission.cs | 2 +- .../Events/Missions/SalvageMission.cs | 6 +- .../BarotraumaServer/ServerSource/GameMain.cs | 12 +- .../ServerSource/GameSession/CargoManager.cs | 57 + .../ServerSource/GameSession/CrewManager.cs | 23 +- .../GameSession/GameModes/CampaignMode.cs | 11 +- .../GameModes/CharacterCampaignData.cs | 10 + .../GameModes/MultiPlayerCampaign.cs | 622 +- .../GameSession/UpgradeManager.cs | 52 + .../Items/Components/ItemLabel.cs | 6 +- .../Components/Machines/OutpostTerminal.cs | 17 + .../ServerSource/Items/Inventory.cs | 1 + .../ServerSource/Items/Item.cs | 48 +- .../ServerSource/Map/Structure.cs | 2 +- .../ServerSource/Networking/ChatMessage.cs | 9 +- .../ServerSource/Networking/EntitySpawner.cs | 2 +- .../Networking/FileTransfer/FileSender.cs | 189 +- .../ServerSource/Networking/GameServer.cs | 682 +- .../ServerEntityEventManager.cs | 8 +- .../ServerSource/Networking/ServerSettings.cs | 1 + .../ServerSource/Networking/Voting.cs | 108 + .../BarotraumaServer/ServerSource/Program.cs | 21 +- .../ServerSource/Screens/NetLobbyScreen.cs | 69 +- .../ServerSource/Traitors/TraitorManager.cs | 21 +- .../Traitors/TraitorMissionResult.cs | 30 + .../BarotraumaServer/WindowsServer.csproj | 2 +- Barotrauma/BarotraumaShared/.gitignore | 1 - .../Data/ContentPackages/Vanilla 0.9.xml | 51 +- .../BarotraumaShared/Data/karmasettings.xml | 2 +- .../BarotraumaShared/SharedCode.projitems | 297 - .../SharedSource/CameraTransition.cs | 158 + .../Characters/AI/EnemyAIController.cs | 50 +- .../Characters/AI/HumanAIController.cs | 682 +- .../Characters/AI/IndoorsSteeringManager.cs | 60 +- .../SharedSource/Characters/AI/LatchOntoAI.cs | 8 +- .../Characters/AI/NPCConversation.cs | 62 +- .../Characters/AI/Objectives/AIObjective.cs | 25 +- .../AI/Objectives/AIObjectiveCombat.cs | 471 +- .../AI/Objectives/AIObjectiveContainItem.cs | 12 +- .../Objectives/AIObjectiveFightIntruders.cs | 23 +- .../Objectives/AIObjectiveFindDivingGear.cs | 2 +- .../AI/Objectives/AIObjectiveFindSafety.cs | 30 +- .../AI/Objectives/AIObjectiveFixLeak.cs | 7 +- .../AI/Objectives/AIObjectiveGetItem.cs | 90 +- .../AI/Objectives/AIObjectiveGoTo.cs | 25 +- .../AI/Objectives/AIObjectiveIdle.cs | 343 +- .../AI/Objectives/AIObjectiveManager.cs | 6 +- .../AI/Objectives/AIObjectiveOperateItem.cs | 12 +- .../AI/Objectives/AIObjectiveRepairItem.cs | 5 +- .../AI/Objectives/AIObjectiveRepairItems.cs | 10 +- .../AI/Objectives/AIObjectiveRescue.cs | 77 +- .../AI/Objectives/AIObjectiveRescueAll.cs | 17 +- .../SharedSource/Characters/AI/Order.cs | 31 +- .../SharedSource/Characters/AI/PathFinder.cs | 170 +- .../Characters/AI/Wreck/WreckAI.cs | 2 +- .../SharedSource/Characters/AICharacter.cs | 2 +- .../Animation/FishAnimController.cs | 2 +- .../Animation/HumanoidAnimController.cs | 92 +- .../Characters/Animation/Ragdoll.cs | 44 +- .../SharedSource/Characters/Character.cs | 273 +- .../SharedSource/Characters/CharacterInfo.cs | 45 +- .../{Map => Characters}/CorpsePrefab.cs | 101 +- .../Characters/Health/CharacterHealth.cs | 84 +- .../SharedSource/Characters/HumanPrefab.cs | 178 + .../SharedSource/Characters/Jobs/Job.cs | 14 +- .../SharedSource/Characters/Jobs/JobPrefab.cs | 10 + .../SharedSource/Characters/Jobs/Skill.cs | 69 +- .../Characters/Jobs/SkillPrefab.cs | 11 +- .../Characters/Params/CharacterParams.cs | 3 + .../SharedSource/ContentPackage.cs | 48 +- .../SharedSource/DebugConsole.cs | 350 +- .../SharedSource/Events/ArtifactEvent.cs | 9 +- .../SharedSource/Events/Event.cs | 60 + .../Events/EventActions/AfflictionAction.cs | 69 + .../Events/EventActions/BinaryOptionAction.cs | 113 + .../Events/EventActions/CheckDataAction.cs | 126 + .../Events/EventActions/CheckItemAction.cs | 64 + .../EventActions/CheckReputationAction.cs | 61 + .../Events/EventActions/CombatAction.cs | 91 + .../Events/EventActions/ConversationAction.cs | 359 + .../Events/EventActions/EventAction.cs | 178 + .../Events/EventActions/FireAction.cs | 50 + .../Events/EventActions/GiveSkillExpAction.cs | 56 + .../SharedSource/Events/EventActions/GoTo.cs | 25 + .../SharedSource/Events/EventActions/Label.cs | 29 + .../Events/EventActions/MissionAction.cs | 96 + .../Events/EventActions/MoneyAction.cs | 44 + .../Events/EventActions/NPCFollowAction.cs | 94 + .../Events/EventActions/NPCWaitAction.cs | 85 + .../Events/EventActions/RNGAction.cs | 31 + .../Events/EventActions/RemoveItemAction.cs | 58 + .../Events/EventActions/ReputationAction.cs | 97 + .../Events/EventActions/SetDataAction.cs | 105 + .../EventActions/SetPriceMultiplierAction.cs | 105 + .../Events/EventActions/SkillCheckAction.cs | 46 + .../Events/EventActions/SpawnAction.cs | 335 + .../Events/EventActions/StatusEffectAction.cs | 68 + .../Events/EventActions/TagAction.cs | 102 + .../Events/EventActions/TriggerAction.cs | 173 + .../Events/EventActions/TriggerEventAction.cs | 48 + .../Events/EventActions/WaitAction.cs | 38 + .../SharedSource/Events/EventManager.cs | 209 +- ...{ScriptedEventPrefab.cs => EventPrefab.cs} | 14 +- .../{ScriptedEventSet.cs => EventSet.cs} | 206 +- .../SharedSource/Events/MalfunctionEvent.cs | 4 +- .../Events/Missions/CargoMission.cs | 16 +- .../Events/Missions/CombatMission.cs | 1 + .../SharedSource/Events/Missions/Mission.cs | 24 +- .../Events/Missions/MissionPrefab.cs | 30 +- .../Events/Missions/MonsterMission.cs | 58 +- .../Events/Missions/SalvageMission.cs | 19 +- .../SharedSource/Events/MonsterEvent.cs | 34 +- .../SharedSource/Events/ScriptedEvent.cs | 252 +- .../GameSession/AutoItemPlacer.cs | 73 +- .../SharedSource/GameSession/CargoManager.cs | 206 +- .../SharedSource/GameSession/CrewManager.cs | 195 +- .../GameSession/Data/CampaignMetadata.cs | 153 + .../SharedSource/GameSession/Data/Factions.cs | 141 + .../GameSession/Data/Reputation.cs | 46 + .../GameSession/GameModes/CampaignMode.cs | 665 +- .../GameModes/CharacterCampaignData.cs | 24 +- .../GameSession/GameModes/GameMode.cs | 36 +- .../GameSession/GameModes/GameModePreset.cs | 32 +- .../GameSession/GameModes/MissionMode.cs | 30 +- .../GameModes/MultiPlayerCampaign.cs | 208 +- .../SharedSource/GameSession/GameSession.cs | 529 +- .../SharedSource/GameSession/HireManager.cs | 24 +- .../GameSession/UpgradeManager.cs | 747 ++ .../SharedSource/GameSettings.cs | 46 +- .../Items/Components/DockingPort.cs | 87 +- .../SharedSource/Items/Components/Door.cs | 47 +- .../Items/Components/Holdable/Holdable.cs | 16 +- .../Components/Holdable/LevelResource.cs | 1 + .../Items/Components/Holdable/MeleeWeapon.cs | 18 +- .../Items/Components/Holdable/Pickable.cs | 9 +- .../Items/Components/Holdable/RangedWeapon.cs | 2 +- .../Items/Components/Holdable/RepairTool.cs | 14 +- .../Items/Components/Holdable/Throwable.cs | 2 +- .../Items/Components/ItemComponent.cs | 35 +- .../Items/Components/ItemContainer.cs | 15 +- .../SharedSource/Items/Components/Ladder.cs | 1 + .../Items/Components/Machines/Controller.cs | 129 +- .../Components/Machines/Deconstructor.cs | 7 +- .../Items/Components/Machines/Fabricator.cs | 46 +- .../Components/Machines/OutpostTerminal.cs | 16 + .../Items/Components/Machines/Pump.cs | 7 + .../Items/Components/Machines/Reactor.cs | 33 +- .../Items/Components/Machines/Sonar.cs | 4 +- .../Items/Components/Machines/Steering.cs | 4 + .../Items/Components/Machines/Vent.cs | 7 +- .../Items/Components/Power/PowerContainer.cs | 2 +- .../Items/Components/Power/Powered.cs | 5 + .../Items/Components/Projectile.cs | 46 +- .../Items/Components/Repairable.cs | 48 +- .../Items/Components/Signal/Connection.cs | 13 +- .../Components/Signal/ConnectionPanel.cs | 10 + .../Components/Signal/CustomInterface.cs | 2 + .../Items/Components/Signal/LightComponent.cs | 2 +- .../Items/Components/Signal/SmokeDetector.cs | 2 +- .../Items/Components/Signal/WifiComponent.cs | 8 + .../Items/Components/Signal/Wire.cs | 25 +- .../SharedSource/Items/Components/Turret.cs | 10 +- .../SharedSource/Items/Components/Wearable.cs | 5 +- .../SharedSource/Items/Inventory.cs | 19 +- .../SharedSource/Items/Item.cs | 165 +- .../SharedSource/Items/ItemPrefab.cs | 137 +- .../SharedSource/Items/RelatedItem.cs | 12 +- .../SharedSource/Map/Entity.cs | 13 +- .../SharedSource/Map/Explosion.cs | 2 +- .../BarotraumaShared/SharedSource/Map/Gap.cs | 23 +- .../BarotraumaShared/SharedSource/Map/Hull.cs | 70 +- .../SharedSource/Map/ItemAssemblyPrefab.cs | 13 + .../SharedSource/Map/Levels/Level.cs | 714 +- .../SharedSource/Map/Levels/LevelData.cs | 164 + .../Map/Levels/LevelGenerationParams.cs | 118 +- .../Map/Levels/LevelObjects/LevelObject.cs | 10 +- .../Levels/LevelObjects/LevelObjectManager.cs | 70 +- .../Levels/LevelObjects/LevelObjectPrefab.cs | 12 +- .../Map/Levels/Ruins/RuinGenerationParams.cs | 4 +- .../SharedSource/Map/LinkedSubmarine.cs | 2 +- .../SharedSource/Map/Map/Location.cs | 731 +- .../Map/Map/LocationConnection.cs | 7 +- .../SharedSource/Map/Map/LocationType.cs | 35 +- .../SharedSource/Map/Map/Map.cs | 632 +- .../Map/Map/MapGenerationParams.cs | 209 +- .../SharedSource/Map/MapEntity.cs | 157 +- .../SharedSource/Map/MapEntityPrefab.cs | 10 + .../SharedSource/Map/Md5Hash.cs | 53 +- .../SharedSource/Map/Outposts/NPCSet.cs | 112 + .../Map/Outposts/OutpostGenerationParams.cs | 174 + .../Map/Outposts/OutpostGenerator.cs | 1436 ++ .../Map/Outposts/OutpostModuleInfo.cs | 167 + .../SharedSource/Map/PriceInfo.cs | 59 +- .../SharedSource/Map/Structure.cs | 105 +- .../SharedSource/Map/StructurePrefab.cs | 25 +- .../SharedSource/Map/Submarine.cs | 274 +- .../SharedSource/Map/SubmarineBody.cs | 15 +- .../SharedSource/Map/SubmarineInfo.cs | 101 +- .../SharedSource/Map/WayPoint.cs | 281 +- .../SharedSource/Networking/ChatMessage.cs | 24 +- .../SharedSource/Networking/Client.cs | 2 +- .../SharedSource/Networking/EntitySpawner.cs | 26 +- .../NetEntityEvent/NetEntityEvent.cs | 4 +- .../SharedSource/Networking/NetIdUtils.cs | 15 +- .../SharedSource/Networking/NetworkMember.cs | 20 +- .../SharedSource/Networking/RespawnManager.cs | 2 +- .../SharedSource/Networking/ServerSettings.cs | 35 + .../SharedSource/Networking/Voting.cs | 4 + .../SharedSource/Screens/GameScreen.cs | 31 +- .../SharedSource/Screens/Screen.cs | 1 - .../Serialization/SerializableProperty.cs | 41 +- .../SharedSource/Sprite/SpriteSheet.cs | 4 +- .../StatusEffects/PropertyConditional.cs | 90 +- .../StatusEffects/StatusEffect.cs | 18 +- .../SharedSource/SteamAchievementManager.cs | 2 +- .../SharedSource/TextManager.cs | 15 +- .../Traitors/TraitorMissionResult.cs | 15 + .../SharedSource/Upgrades/Upgrade.cs | 401 + .../SharedSource/Upgrades/UpgradePrefab.cs | 424 + .../SharedSource/Utils/MathUtils.cs | 32 +- .../SharedSource/Utils/Rand.cs | 18 +- .../SharedSource/Utils/SaveUtil.cs | 100 +- .../SharedSource/Utils/TaskPool.cs | 40 +- .../SharedSource/Utils/ToolBox.cs | 33 + .../BarotraumaShared/Submarines/Azimuth.sub | Bin 217413 -> 201804 bytes .../BarotraumaShared/Submarines/Berilia.sub | Bin 303617 -> 309909 bytes .../BarotraumaShared/Submarines/Dugong.sub | Bin 211096 -> 201166 bytes .../BarotraumaShared/Submarines/Hemulen.sub | Bin 255181 -> 225813 bytes .../BarotraumaShared/Submarines/Humpback.sub | Bin 207639 -> 192581 bytes .../BarotraumaShared/Submarines/Kastrull.sub | Bin 572637 -> 485915 bytes .../Submarines/KastrullDrone.sub | Bin 289652 -> 226253 bytes .../BarotraumaShared/Submarines/Orca.sub | Bin 205089 -> 215896 bytes .../BarotraumaShared/Submarines/Remora.sub | Bin 332315 -> 556638 bytes .../Submarines/RemoraDrone.sub | Bin 71714 -> 278141 bytes .../BarotraumaShared/Submarines/Selkie.sub | Bin 256791 -> 220780 bytes .../BarotraumaShared/Submarines/Typhon.sub | Bin 283709 -> 266793 bytes .../BarotraumaShared/Submarines/Typhon2.sub | Bin 291927 -> 267967 bytes .../BarotraumaShared/Submarines/Venture.sub | Bin 400601 -> 312110 bytes Barotrauma/BarotraumaShared/changelog.txt | 160 +- .../BarotraumaShared/libsteam_api.dylib | Bin 670560 -> 446352 bytes .../BarotraumaShared/libsteam_api64.dylib | Bin 655056 -> 446352 bytes Barotrauma/BarotraumaShared/libsteam_api64.so | Bin 436084 -> 398662 bytes Barotrauma/BarotraumaShared/steam_api64.dll | Bin 289568 -> 262944 bytes .../Callbacks/CallResult.cs | 95 + .../Callbacks/Callback.cs | 43 - .../Facepunch.Steamworks/Callbacks/Event.cs | 135 - .../Callbacks/ICallbackData.cs | 17 + .../Facepunch.Steamworks/Classes/Dispatch.cs | 334 + .../Facepunch.Steamworks/Classes/SteamApi.cs | 50 + .../{Generated => Classes}/SteamGameServer.cs | 8 - .../Classes/SteamInternal.cs | 24 + .../Enum/ConnectionState.cs | 107 - .../Enum/DebugOutputType.cs | 17 - .../Facepunch.Steamworks/Enum/NetConfig.cs | 57 - .../Enum/NetConfigResult.cs | 13 - .../Enum/NetConfigType.cs | 13 - .../Facepunch.Steamworks/Enum/NetScope.cs | 12 - ...proj => Facepunch.Steamworks.Posix.csproj} | 0 .../Facepunch.Steamworks.Win64.csproj | 34 +- .../Facepunch.Steamworks.jpg | Bin 0 -> 32489 bytes .../Facepunch.Steamworks.targets | 5 + .../Generated/CustomEnums.cs | 426 + .../Generated/Interfaces/ISteamAppList.cs | 83 + .../Generated/Interfaces/ISteamApps.cs | 227 +- .../Generated/Interfaces/ISteamClient.cs | 414 + .../Generated/Interfaces/ISteamController.cs | 392 + .../Generated/Interfaces/ISteamFriends.cs | 633 +- .../Generated/Interfaces/ISteamGameSearch.cs | 180 + .../Generated/Interfaces/ISteamGameServer.cs | 364 +- .../Interfaces/ISteamGameServerStats.cs | 116 +- .../Generated/Interfaces/ISteamHTMLSurface.cs | 399 + .../Generated/Interfaces/ISteamHTTP.cs | 325 + .../Generated/Interfaces/ISteamInput.cs | 306 +- .../Generated/Interfaces/ISteamInventory.cs | 340 +- .../Generated/Interfaces/ISteamMatchmaking.cs | 322 +- .../ISteamMatchmakingPingResponse.cs | 39 + .../ISteamMatchmakingPlayersResponse.cs | 49 + .../ISteamMatchmakingRulesResponse.cs | 49 + .../ISteamMatchmakingServerListResponse.cs | 49 + .../Interfaces/ISteamMatchmakingServers.cs | 135 +- .../Generated/Interfaces/ISteamMusic.cs | 77 +- .../Generated/Interfaces/ISteamMusicRemote.cs | 408 + .../Generated/Interfaces/ISteamNetworking.cs | 271 +- ...teamNetworkingConnectionCustomSignaling.cs | 41 + ...eamNetworkingCustomSignalingRecvContext.cs | 40 + .../Interfaces/ISteamNetworkingSockets.cs | 388 +- .../Interfaces/ISteamNetworkingUtils.cs | 319 +- .../Interfaces/ISteamParentalSettings.cs | 56 +- .../Generated/Interfaces/ISteamParties.cs | 110 +- .../Generated/Interfaces/ISteamRemotePlay.cs | 103 + .../Interfaces/ISteamRemoteStorage.cs | 279 +- .../Generated/Interfaces/ISteamScreenshots.cs | 77 +- .../Generated/Interfaces/ISteamTV.cs | 97 + .../Generated/Interfaces/ISteamUGC.cs | 680 +- .../Generated/Interfaces/ISteamUser.cs | 257 +- .../Generated/Interfaces/ISteamUserStats.cs | 412 +- .../Generated/Interfaces/ISteamUtils.cs | 285 +- .../Generated/Interfaces/ISteamVideo.cs | 42 +- .../Generated/SteamApi.cs | 98 - .../Generated/SteamCallbacks.cs | 2909 +++++ .../Generated/SteamConstants.cs | 180 +- .../Generated/SteamEnums.cs | 626 +- .../Generated/SteamInternal.cs | 49 - .../Generated/SteamStructFunctions.cs | 208 + .../Generated/SteamStructs.cs | 10838 +--------------- .../Generated/SteamTypes.cs | 103 +- .../{Structs => Networking}/Connection.cs | 62 +- .../{Structs => Networking}/ConnectionInfo.cs | 22 +- .../ConnectionManager.cs} | 44 +- .../Networking/IConnectionManager.cs | 28 + .../Networking/ISocketManager.cs | 28 + .../Networking/NetAddress.cs | 156 + .../Networking/NetDebugFunc.cs | 9 + .../Networking/NetErrorMessage.cs | 10 + .../Networking/NetIdentity.cs | 126 + .../Networking/NetKeyValue.cs | 31 + .../{Structs => Networking}/NetMsg.cs | 14 +- .../NetPingLocation.cs} | 8 +- .../Facepunch.Steamworks/Networking/Socket.cs | 29 + .../SocketManager.cs} | 89 +- .../SteamDatagramRelayAuthTicket.cs | 12 + .../Facepunch.Steamworks/ServerList/Base.cs | 22 +- Libraries/Facepunch.Steamworks/SteamApps.cs | 35 +- Libraries/Facepunch.Steamworks/SteamClient.cs | 129 +- .../Facepunch.Steamworks/SteamFriends.cs | 160 +- Libraries/Facepunch.Steamworks/SteamInput.cs | 70 +- .../Facepunch.Steamworks/SteamInventory.cs | 53 +- .../Facepunch.Steamworks/SteamMatchmaking.cs | 65 +- .../SteamMatchmakingServers.cs | 22 + Libraries/Facepunch.Steamworks/SteamMusic.cs | 28 +- .../Facepunch.Steamworks/SteamNetworking.cs | 79 +- .../SteamNetworkingSockets.cs | 173 +- .../SteamNetworkingUtils.cs | 215 +- .../Facepunch.Steamworks/SteamParental.cs | 27 +- .../Facepunch.Steamworks/SteamParties.cs | 46 +- .../Facepunch.Steamworks/SteamRemotePlay.cs | 58 + .../SteamRemoteStorage.cs | 22 +- .../Facepunch.Steamworks/SteamScreenshots.cs | 29 +- Libraries/Facepunch.Steamworks/SteamServer.cs | 144 +- .../Facepunch.Steamworks/SteamServerStats.cs | 32 +- Libraries/Facepunch.Steamworks/SteamUgc.cs | 163 +- Libraries/Facepunch.Steamworks/SteamUser.cs | 94 +- .../Facepunch.Steamworks/SteamUserStats.cs | 62 +- Libraries/Facepunch.Steamworks/SteamUtils.cs | 40 +- Libraries/Facepunch.Steamworks/SteamVideo.cs | 30 +- .../ISteamMatchmakingResponses.cs | 420 - .../SteamMatchmakingResponses.cs | 151 + .../Structs/Achievement.cs | 4 +- .../Facepunch.Steamworks/Structs/Clan.cs | 50 + .../Structs/DurationControl.cs | 42 + .../Facepunch.Steamworks/Structs/Friend.cs | 79 +- .../Structs/InventoryDef.cs | 13 +- .../Structs/InventoryItem.cs | 35 +- .../Structs/Leaderboard.cs | 22 +- .../Facepunch.Steamworks/Structs/Lobby.cs | 4 +- .../Structs/MatchMakingKeyValuePair.cs | 2 +- .../Structs/NetAddress.cs | 107 - .../Structs/NetIdentity.cs | 38 - .../Structs/P2PSessionState.cs | 31 + .../Structs/PartyBeacon.cs | 4 +- .../Structs/RemotePlaySession.cs | 38 + .../Facepunch.Steamworks/Structs/Server.cs | 2 - .../Facepunch.Steamworks/Structs/Socket.cs | 23 - .../Facepunch.Steamworks/Structs/Stat.cs | 20 +- .../Structs/SteamIpAddress.cs | 26 + .../Structs/SteamNetworking.cs | 58 - Libraries/Facepunch.Steamworks/Structs/Ugc.cs | 6 +- .../Facepunch.Steamworks/Structs/UgcEditor.cs | 131 +- .../Facepunch.Steamworks/Structs/UgcItem.cs | 106 +- .../Facepunch.Steamworks/Structs/UgcQuery.cs | 117 +- .../Structs/UgcResultPage.cs | 59 +- .../Structs/UserItemVote.cs | 21 + .../Facepunch.Steamworks/Utility/Helpers.cs | 72 +- .../Facepunch.Steamworks/Utility/Platform.cs | 22 +- .../Utility/SourceServerQuery.cs | 106 +- .../Utility/SteamInterface.cs | 195 +- .../Utility/Utf8String.cs | 2 + .../Facepunch.Steamworks/Utility/Utility.cs | 41 +- .../Facepunch.Steamworks/libsteam_api.dylib | Bin 655056 -> 0 bytes .../Facepunch.Steamworks/libsteam_api.so | Bin 434456 -> 0 bytes .../Facepunch.Steamworks/libsteam_api64.dylib | Bin 655056 -> 0 bytes .../Facepunch.Steamworks/steam_api64.dll | Bin 288032 -> 262944 bytes Libraries/OpenAL-Soft/oal_soft.diff | 373 +- LinuxSolution.sln | 2 +- MacSolution.sln | 2 +- 546 files changed, 45952 insertions(+), 25762 deletions(-) create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs delete mode 100644 Barotrauma/BarotraumaClient/ClientSource/GUI/GUIColorSettings.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Data/CampaignMetadata.cs delete mode 100644 Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SubTestMode.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/GameSession/UpgradeManager.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs delete mode 100644 Barotrauma/BarotraumaClient/ClientSource/Map/Map/Location.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignEndScreen.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EditorNode.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/NodeConnection.cs delete mode 100644 Barotrauma/BarotraumaClient/ClientSource/Screens/LobbyScreen.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Screens/RoundSummaryScreen.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionResult.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Upgrades/UpgradePrefab.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/StatusEffectAction.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/GameSession/UpgradeManager.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/OutpostTerminal.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMissionResult.cs delete mode 100644 Barotrauma/BarotraumaShared/.gitignore delete mode 100644 Barotrauma/BarotraumaShared/SharedCode.projitems create mode 100644 Barotrauma/BarotraumaShared/SharedSource/CameraTransition.cs rename Barotrauma/BarotraumaShared/SharedSource/{Map => Characters}/CorpsePrefab.cs (51%) create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/AfflictionAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/BinaryOptionAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckReputationAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CombatAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/FireAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/Label.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetDataAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetPriceMultiplierAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SkillCheckAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/StatusEffectAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitAction.cs rename Barotrauma/BarotraumaShared/SharedSource/Events/{ScriptedEventPrefab.cs => EventPrefab.cs} (83%) rename Barotrauma/BarotraumaShared/SharedSource/Events/{ScriptedEventSet.cs => EventSet.cs} (50%) create mode 100644 Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OutpostTerminal.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/NPCSet.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorMissionResult.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs create mode 100644 Libraries/Facepunch.Steamworks/Callbacks/CallResult.cs delete mode 100644 Libraries/Facepunch.Steamworks/Callbacks/Callback.cs delete mode 100644 Libraries/Facepunch.Steamworks/Callbacks/Event.cs create mode 100644 Libraries/Facepunch.Steamworks/Callbacks/ICallbackData.cs create mode 100644 Libraries/Facepunch.Steamworks/Classes/Dispatch.cs create mode 100644 Libraries/Facepunch.Steamworks/Classes/SteamApi.cs rename Libraries/Facepunch.Steamworks/{Generated => Classes}/SteamGameServer.cs (78%) create mode 100644 Libraries/Facepunch.Steamworks/Classes/SteamInternal.cs delete mode 100644 Libraries/Facepunch.Steamworks/Enum/ConnectionState.cs delete mode 100644 Libraries/Facepunch.Steamworks/Enum/DebugOutputType.cs delete mode 100644 Libraries/Facepunch.Steamworks/Enum/NetConfig.cs delete mode 100644 Libraries/Facepunch.Steamworks/Enum/NetConfigResult.cs delete mode 100644 Libraries/Facepunch.Steamworks/Enum/NetConfigType.cs delete mode 100644 Libraries/Facepunch.Steamworks/Enum/NetScope.cs rename Libraries/Facepunch.Steamworks/{Facepunch.Steamworks.Posix64.csproj => Facepunch.Steamworks.Posix.csproj} (100%) create mode 100644 Libraries/Facepunch.Steamworks/Facepunch.Steamworks.jpg create mode 100644 Libraries/Facepunch.Steamworks/Generated/CustomEnums.cs create mode 100644 Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamAppList.cs create mode 100644 Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamClient.cs create mode 100644 Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamController.cs create mode 100644 Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamGameSearch.cs create mode 100644 Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamHTMLSurface.cs create mode 100644 Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamHTTP.cs create mode 100644 Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingPingResponse.cs create mode 100644 Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingPlayersResponse.cs create mode 100644 Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingRulesResponse.cs create mode 100644 Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingServerListResponse.cs create mode 100644 Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMusicRemote.cs create mode 100644 Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingConnectionCustomSignaling.cs create mode 100644 Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingCustomSignalingRecvContext.cs create mode 100644 Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamRemotePlay.cs create mode 100644 Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamTV.cs delete mode 100644 Libraries/Facepunch.Steamworks/Generated/SteamApi.cs create mode 100644 Libraries/Facepunch.Steamworks/Generated/SteamCallbacks.cs delete mode 100644 Libraries/Facepunch.Steamworks/Generated/SteamInternal.cs create mode 100644 Libraries/Facepunch.Steamworks/Generated/SteamStructFunctions.cs rename Libraries/Facepunch.Steamworks/{Structs => Networking}/Connection.cs (64%) rename Libraries/Facepunch.Steamworks/{Structs => Networking}/ConnectionInfo.cs (51%) rename Libraries/Facepunch.Steamworks/{Classes/ConnectionInterface.cs => Networking/ConnectionManager.cs} (69%) create mode 100644 Libraries/Facepunch.Steamworks/Networking/IConnectionManager.cs create mode 100644 Libraries/Facepunch.Steamworks/Networking/ISocketManager.cs create mode 100644 Libraries/Facepunch.Steamworks/Networking/NetAddress.cs create mode 100644 Libraries/Facepunch.Steamworks/Networking/NetDebugFunc.cs create mode 100644 Libraries/Facepunch.Steamworks/Networking/NetErrorMessage.cs create mode 100644 Libraries/Facepunch.Steamworks/Networking/NetIdentity.cs create mode 100644 Libraries/Facepunch.Steamworks/Networking/NetKeyValue.cs rename Libraries/Facepunch.Steamworks/{Structs => Networking}/NetMsg.cs (57%) rename Libraries/Facepunch.Steamworks/{Structs/PingLocation.cs => Networking/NetPingLocation.cs} (93%) create mode 100644 Libraries/Facepunch.Steamworks/Networking/Socket.cs rename Libraries/Facepunch.Steamworks/{Classes/SocketInterface.cs => Networking/SocketManager.cs} (55%) create mode 100644 Libraries/Facepunch.Steamworks/Networking/SteamDatagramRelayAuthTicket.cs create mode 100644 Libraries/Facepunch.Steamworks/SteamMatchmakingServers.cs create mode 100644 Libraries/Facepunch.Steamworks/SteamRemotePlay.cs delete mode 100644 Libraries/Facepunch.Steamworks/Steamworks.NET/ISteamMatchmakingResponses.cs create mode 100644 Libraries/Facepunch.Steamworks/Steamworks.NET/SteamMatchmakingResponses.cs create mode 100644 Libraries/Facepunch.Steamworks/Structs/Clan.cs create mode 100644 Libraries/Facepunch.Steamworks/Structs/DurationControl.cs delete mode 100644 Libraries/Facepunch.Steamworks/Structs/NetAddress.cs delete mode 100644 Libraries/Facepunch.Steamworks/Structs/NetIdentity.cs create mode 100644 Libraries/Facepunch.Steamworks/Structs/P2PSessionState.cs create mode 100644 Libraries/Facepunch.Steamworks/Structs/RemotePlaySession.cs delete mode 100644 Libraries/Facepunch.Steamworks/Structs/Socket.cs create mode 100644 Libraries/Facepunch.Steamworks/Structs/SteamIpAddress.cs delete mode 100644 Libraries/Facepunch.Steamworks/Structs/SteamNetworking.cs create mode 100644 Libraries/Facepunch.Steamworks/Structs/UserItemVote.cs delete mode 100644 Libraries/Facepunch.Steamworks/libsteam_api.dylib delete mode 100644 Libraries/Facepunch.Steamworks/libsteam_api.so delete mode 100644 Libraries/Facepunch.Steamworks/libsteam_api64.dylib diff --git a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs index 1c5adb3ff..02fe9bec7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs @@ -163,7 +163,8 @@ namespace Barotrauma position = Vector2.Zero; CreateMatrices(); - GameMain.Instance.OnResolutionChanged += () => { CreateMatrices(); }; + // TODO: Needs to unregister if ever destroy cameras. + GameMain.Instance.ResolutionChanged += CreateMatrices; UpdateTransform(false); } @@ -260,27 +261,30 @@ namespace Barotrauma if (targetPos == Vector2.Zero) { Vector2 moveInput = Vector2.Zero; - if (allowMove && GUI.KeyboardDispatcher.Subscriber == null) + if (allowMove) { - 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; - } - - velocity = Vector2.Lerp(velocity, moveInput, deltaTime * 10.0f); - moveCam = velocity * moveSpeed * deltaTime * 60.0f; - - if (Screen.Selected == GameMain.GameScreen && FollowSub) - { - var closestSub = Submarine.FindClosest(WorldViewCenter); - if (closestSub != null) + if (GUI.KeyboardDispatcher.Subscriber == null) { - moveCam += FarseerPhysics.ConvertUnits.ToDisplayUnits(closestSub.Velocity * deltaTime); + 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; } + + velocity = Vector2.Lerp(velocity, moveInput, deltaTime * 10.0f); + moveCam = velocity * moveSpeed * deltaTime * 60.0f; + + if (Screen.Selected == GameMain.GameScreen && FollowSub) + { + var closestSub = Submarine.FindClosest(WorldViewCenter); + if (closestSub != null) + { + moveCam += FarseerPhysics.ConvertUnits.ToDisplayUnits(closestSub.Velocity * deltaTime); + } + } } if (allowZoom && GUI.MouseOn == null) @@ -311,7 +315,7 @@ namespace Barotrauma { Freeze = true; } - if (CharacterHealth.OpenHealthWindow != null || CrewManager.IsCommandInterfaceOpen) + if (CharacterHealth.OpenHealthWindow != null || CrewManager.IsCommandInterfaceOpen || ConversationAction.IsDialogOpen) { offset *= 0; Freeze = false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs index 48d6c619c..a16f76d39 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs @@ -6,6 +6,8 @@ namespace Barotrauma { partial class HumanAIController : AIController { + public static bool debugai; + partial void InitProjSpecific() { /*if (GameMain.GameSession != null && GameMain.GameSession.CrewManager != null) @@ -18,6 +20,8 @@ namespace Barotrauma public override void DebugDraw(Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch) { + if (Character == Character.Controlled) { return; } + if (!debugai) { return; } Vector2 pos = Character.WorldPosition; pos.Y = -pos.Y; Vector2 textOffset = new Vector2(-40, -160); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs index 132570e65..aa19f80c1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs @@ -268,7 +268,7 @@ namespace Barotrauma } else if (body.UserData is Limb || body == Collider.FarseerBody) { - if (!character.IsRemotePlayer && impact > ImpactTolerance) + if (!character.IsRemotelyControlled && impact > ImpactTolerance) { SoundPlayer.PlayDamageSound("LimbBlunt", strongestImpact, Collider); } @@ -460,21 +460,14 @@ namespace Barotrauma /// public float GetDepthOffset() { + float maxDepth = 0.0f; + float minDepth = 1.0f; float depthOffset = 0.0f; var ladder = character.SelectedConstruction?.GetComponent(); + if (ladder != null) { - float maxDepth = 0.0f; - float minDepth = 1.0f; - foreach (Limb limb in Limbs) - { - var activeSprite = limb.ActiveSprite; - if (activeSprite != null) - { - maxDepth = Math.Max(activeSprite.Depth, maxDepth); - minDepth = Math.Min(activeSprite.Depth, minDepth); - } - } + CalculateLimbDepths(); if (character.WorldPosition.X < character.SelectedConstruction.WorldPosition.X) { //at the left side of the ladder, needs to be drawn in front of the rungs @@ -486,6 +479,36 @@ namespace Barotrauma depthOffset = Math.Max(ladder.BackgroundSpriteDepth + 0.01f - minDepth, 0.0f); } } + else + { + CalculateLimbDepths(); + var controller = character.SelectedConstruction?.GetComponent(); + if (controller != null && controller.ControlCharacterPose && controller.User == character) + { + if (controller.Item.SpriteDepth > maxDepth) + { + depthOffset = Math.Max(controller.Item.SpriteDepth - 0.0001f - maxDepth, 0.0f); + } + else + { + depthOffset = Math.Max(controller.Item.SpriteDepth + 0.0001f - minDepth, -minDepth); + } + } + } + + void CalculateLimbDepths() + { + foreach (Limb limb in Limbs) + { + var activeSprite = limb.ActiveSprite; + if (activeSprite != null) + { + maxDepth = Math.Max(activeSprite.Depth, maxDepth); + minDepth = Math.Min(activeSprite.Depth, minDepth); + } + } + } + return depthOffset; } @@ -498,10 +521,15 @@ namespace Barotrauma { if (limb.PullJointEnabled) { - Vector2 pos = ConvertUnits.ToDisplayUnits(limb.PullJointWorldAnchorA); + 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); + + pos = ConvertUnits.ToDisplayUnits(limb.PullJointWorldAnchorA); + 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), Color.Cyan, true, 0.01f); } limb.body.DebugDraw(spriteBatch, inWater ? (currentHull == null ? Color.Blue : Color.Cyan) : Color.White); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 96502da37..743e8511c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -22,8 +22,8 @@ namespace Barotrauma protected float soundTimer; protected float soundInterval; - protected float hudInfoTimer; - protected bool hudInfoVisible; + protected float hudInfoTimer = 1.0f; + protected bool hudInfoVisible = false; private float pressureParticleTimer; @@ -31,7 +31,7 @@ namespace Barotrauma protected float lastRecvPositionUpdateTime; - private float hudInfoHeight; + private float hudInfoHeight = 100.0f; private List sounds; @@ -264,7 +264,7 @@ namespace Barotrauma } else if (Lights.LightManager.ViewTarget is Item item && item.Prefab.FocusOnSelected) { - cam.OffsetAmount = targetOffsetAmount = item.Prefab.OffsetOnSelected; + cam.OffsetAmount = targetOffsetAmount = item.Prefab.OffsetOnSelected * item.OffsetOnSelectedMultiplier; } else if (SelectedConstruction != null && ViewTarget == null && SelectedConstruction.Components.Any(ic => ic?.GuiFrame != null && ic.ShouldDrawHUD(this))) @@ -549,11 +549,12 @@ namespace Barotrauma public bool ShouldLockHud() { if (this != controlled) { return false; } - + if (GameMain.GameSession?.Campaign != null && GameMain.GameSession.Campaign.ShowCampaignUI) { return true; } + var controller = SelectedConstruction?.GetComponent(); //lock if using a controller, except if we're also using a connection panel in the same item return SelectedConstruction != null && - SelectedConstruction?.GetComponent()?.User == this && + controller?.User == this && controller.HideHUD && SelectedConstruction?.GetComponent()?.User != this; } @@ -661,7 +662,7 @@ namespace Barotrauma public void DrawHUD(SpriteBatch spriteBatch, Camera cam, bool drawHealth = true) { CharacterHUD.Draw(spriteBatch, this, cam); - if (drawHealth) CharacterHealth.DrawHUD(spriteBatch); + if (drawHealth && !CharacterHUD.IsCampaignInterfaceOpen) { CharacterHealth.DrawHUD(spriteBatch); } } public virtual void DrawFront(SpriteBatch spriteBatch, Camera cam) @@ -672,8 +673,8 @@ namespace Barotrauma { AnimController.DebugDraw(spriteBatch); } - - if (GUI.DisableHUD) return; + + if (GUI.DisableHUD) { return; } if (Controlled != null && Controlled != this && @@ -756,19 +757,23 @@ namespace Barotrauma float hoverRange = 300.0f; float fadeOutRange = 200.0f; float cursorDist = Vector2.Distance(WorldPosition, cam.ScreenToWorld(PlayerInput.MousePosition)); - float hudInfoAlpha = MathHelper.Clamp(1.0f - (cursorDist - (hoverRange - fadeOutRange)) / fadeOutRange, 0.2f, 1.0f); + float hudInfoAlpha = + CampaignInteractionType == CampaignMode.InteractionType.None ? + MathHelper.Clamp(1.0f - (cursorDist - (hoverRange - fadeOutRange)) / fadeOutRange, 0.2f, 1.0f) : + 1.0f; if (!GUI.DisableCharacterNames && hudInfoVisible && info != null && - (controlled == null || this != controlled.FocusedCharacter)) + (controlled == null || this != controlled.FocusedCharacter) && cam.Zoom > 0.4f) { string name = Info.DisplayName; - if (controlled == null && name != Info.Name) name += " " + TextManager.Get("Disguised"); + if (controlled == null && name != Info.Name) { name += " " + TextManager.Get("Disguised"); } - Vector2 namePos = new Vector2(pos.X, pos.Y - 10.0f - (5.0f / cam.Zoom)) - GUI.Font.MeasureString(name) * 0.5f / cam.Zoom; + Vector2 nameSize = GUI.Font.MeasureString(name); + Vector2 namePos = new Vector2(pos.X, pos.Y - 10.0f - (5.0f / cam.Zoom)) - nameSize * 0.5f / cam.Zoom; Vector2 screenSize = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight); Vector2 viewportSize = new Vector2(cam.WorldView.Width, cam.WorldView.Height); - namePos.X -= cam.WorldView.X; namePos.Y += cam.WorldView.Y; + namePos.X -= cam.WorldView.X; namePos.Y += cam.WorldView.Y; namePos *= screenSize / viewportSize; namePos.X = (float)Math.Floor(namePos.X); namePos.Y = (float)Math.Floor(namePos.Y); namePos *= viewportSize / screenSize; @@ -779,16 +784,30 @@ namespace Barotrauma { nameColor = TeamID == TeamType.FriendlyNPC ? Color.SkyBlue : GUI.Style.Red; } + if (CampaignInteractionType != CampaignMode.InteractionType.None && AllowCustomInteract) + { + var iconStyle = GUI.Style.GetComponentStyle("CampaignInteractionIcon." + CampaignInteractionType); + if (iconStyle != null) + { + Vector2 headPos = AnimController.GetLimb(LimbType.Head)?.WorldPosition ?? WorldPosition + Vector2.UnitY * 100.0f; + Vector2 iconPos = headPos; + iconPos.Y = -iconPos.Y; + nameColor = iconStyle.Color; + var icon = iconStyle.Sprites[GUIComponent.ComponentState.None].First(); + float iconScale = 30.0f / icon.Sprite.size.X / cam.Zoom; + icon.Sprite.Draw(spriteBatch, iconPos + new Vector2(-35.0f, -25.0f), iconStyle.Color * hudInfoAlpha, scale: iconScale); + } + } + 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); - if (GameMain.DebugDraw) { GUI.Font.DrawString(spriteBatch, ID.ToString(), namePos - new Vector2(0.0f, 20.0f), Color.White); } } - if (IsDead) return; + if (IsDead) { return; } if (CharacterHealth.DisplayedVitality < MaxVitality * 0.98f && hudInfoVisible) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index 989b52794..97af07d7e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -41,13 +41,21 @@ namespace Barotrauma private static bool shouldRecreateHudTexts = true; private static bool heldDownShiftWhenGotHudTexts; + public static bool IsCampaignInterfaceOpen => + GameMain.GameSession?.Campaign != null && + (GameMain.GameSession.Campaign.ShowCampaignUI || GameMain.GameSession.Campaign.ForceMapUI); + private static bool ShouldDrawInventory(Character character) { + var controller = character.SelectedConstruction?.GetComponent(); + return character?.Inventory != null && character.AllowInput && - !character.LockHands && - character.SelectedConstruction?.GetComponent()?.User != character; + !character.LockHands && + (controller?.User != character || !controller.HideHUD) && + !IsCampaignInterfaceOpen && + !ConversationAction.FadeScreenToBlack; } private static string GetCachedHudText(string textTag, string keyBind) @@ -65,7 +73,7 @@ namespace Barotrauma { if (GUI.DisableHUD) return; - if (!character.IsIncapacitated && character.Stun <= 0.0f) + if (!character.IsIncapacitated && character.Stun <= 0.0f && !IsCampaignInterfaceOpen) { if (character.Inventory != null) { @@ -92,9 +100,17 @@ namespace Barotrauma public static void Update(float deltaTime, Character character, Camera cam) { - if (GUI.DisableHUD) { return; } - - if (!character.IsIncapacitated && character.Stun <= 0.0f) + if (GUI.DisableHUD) + { + if (character.Inventory != null && !LockInventory(character)) + { + character.Inventory.UpdateSlotInput(); + } + + return; + } + + if (!character.IsIncapacitated && character.Stun <= 0.0f && !IsCampaignInterfaceOpen) { if (character.Info != null && !character.ShouldLockHud() && character.SelectedCharacter == null) { @@ -163,7 +179,7 @@ namespace Barotrauma foreach (Item item in Item.ItemList) { if (item.Submarine == null || item.Submarine.TeamID != character.TeamID || item.Submarine.Info.IsWreck) { continue; } - if (!item.Repairables.Any(r => item.ConditionPercentage <= r.RepairThreshold)) { continue; } + if (!item.Repairables.Any(r => item.ConditionPercentage <= r.RepairIconThreshold)) { continue; } if (Submarine.VisibleEntities != null && !Submarine.VisibleEntities.Contains(item)) { continue; } Vector2 diff = item.WorldPosition - character.WorldPosition; @@ -211,7 +227,7 @@ namespace Barotrauma Color.Lerp(GUI.Style.Red, GUI.Style.Orange * 0.5f, brokenItem.Condition / brokenItem.MaxCondition) * alpha); } - if (!character.IsIncapacitated && character.Stun <= 0.0f) + if (!character.IsIncapacitated && character.Stun <= 0.0f && !IsCampaignInterfaceOpen) { if (character.FocusedCharacter != null && character.FocusedCharacter.CanBeSelected) { @@ -299,6 +315,8 @@ namespace Barotrauma character.SelectedConstruction.DrawHUD(spriteBatch, cam, Character.Controlled); } + if (IsCampaignInterfaceOpen) { return; } + if (character.Inventory != null) { for (int i = 0; i < character.Inventory.Items.Length - 1; i++) @@ -308,10 +326,11 @@ namespace Barotrauma foreach (ItemComponent ic in item.Components) { - if (ic.DrawHudWhenEquipped) ic.DrawHUD(spriteBatch, character); + if (ic.DrawHudWhenEquipped) { ic.DrawHUD(spriteBatch, character); } } } } + bool mouseOnPortrait = false; if (character.Stun <= 0.1f && !character.IsDead) { @@ -421,7 +440,7 @@ namespace Barotrauma GUI.Style.Green, Color.Black, 2, GUI.SmallFont); textPos.Y += textSize.Y; } - if (!string.IsNullOrEmpty(character.FocusedCharacter.customInteractHUDText)) + if (!string.IsNullOrEmpty(character.FocusedCharacter.customInteractHUDText) && character.FocusedCharacter.AllowCustomInteract) { GUI.DrawString(spriteBatch, textPos, character.FocusedCharacter.customInteractHUDText, GUI.Style.Green, Color.Black, 2, GUI.SmallFont); textPos.Y += textSize.Y; @@ -430,7 +449,7 @@ namespace Barotrauma private static bool LockInventory(Character character) { - if (character?.Inventory == null || !character.AllowInput || character.LockHands) { return true; } + if (character?.Inventory == null || !character.AllowInput || character.LockHands || IsCampaignInterfaceOpen) { return true; } return character.ShouldLockHud(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index 6419470f9..87fecf8a9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using System.Xml.Linq; namespace Barotrauma { @@ -12,9 +13,11 @@ namespace Barotrauma { private static Sprite infoAreaPortraitBG; + public bool LastControlled; + public static void Init() { - infoAreaPortraitBG = GUI.Style.GetComponentStyle("InfoAreaPortraitBG")?.Sprites[GUIComponent.ComponentState.None][0].Sprite; + infoAreaPortraitBG = GUI.Style.GetComponentStyle("InfoAreaPortraitBG")?.GetDefaultSprite(); new Sprite("Content/UI/InventoryUIAtlas.png", new Rectangle(833, 298, 142, 98), null, 0); } @@ -117,6 +120,7 @@ namespace Barotrauma private void DrawInfoFrameCharacterIcon(SpriteBatch sb, Rectangle componentRect) { + if (headSprite == null) { return; } Vector2 targetAreaSize = componentRect.Size.ToVector2(); float scale = Math.Min(targetAreaSize.X / headSprite.size.X, targetAreaSize.Y / headSprite.size.Y); DrawIcon(sb, componentRect.Location.ToVector2() + headSprite.size / 2 * scale, targetAreaSize); @@ -140,6 +144,9 @@ namespace Barotrauma partial void OnSkillChanged(string skillIdentifier, float prevLevel, float newLevel, Vector2 textPopupPos) { + if (TeamID == Character.TeamType.FriendlyNPC) { return; } + if (Character.Controlled != null && Character.Controlled.TeamID != TeamID) { return; } + if (newLevel - prevLevel > 0.1f) { GUI.AddMessage( diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 8a815e11f..59190cd54 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -75,8 +75,15 @@ namespace Barotrauma states = newInput, intAim = intAngle }; - if (focusedItem != null && !CharacterInventory.DraggingItemToWorld && - (!newMem.states.HasFlag(InputNetFlags.Grab) && !newMem.states.HasFlag(InputNetFlags.Health))) + + if (FocusedCharacter != null && + FocusedCharacter.CampaignInteractionType != CampaignMode.InteractionType.None && + newMem.states.HasFlag(InputNetFlags.Use)) + { + newMem.interact = FocusedCharacter.ID; + } + else if (focusedItem != null && !CharacterInventory.DraggingItemToWorld && + !newMem.states.HasFlag(InputNetFlags.Grab) && !newMem.states.HasFlag(InputNetFlags.Health)) { newMem.interact = focusedItem.ID; } @@ -278,10 +285,10 @@ namespace Barotrauma break; case ServerNetObject.ENTITY_EVENT: - int eventType = msg.ReadRangedInteger(0, 4); + int eventType = msg.ReadRangedInteger(0, 5); switch (eventType) { - case 0: + case 0: //NetEntityEvent.Type.InventoryState if (Inventory == null) { string errorMsg = "Received an inventory update message for an entity with no inventory (" + Name + ", removed: " + Removed + ")"; @@ -301,7 +308,7 @@ namespace Barotrauma Inventory.ClientRead(type, msg, sendingTime); } break; - case 1: + case 1: //NetEntityEvent.Type.Control byte ownerID = msg.ReadByte(); ResetNetState(); if (ownerID == GameMain.Client.ID) @@ -326,10 +333,10 @@ namespace Barotrauma } } break; - case 2: + case 2: //NetEntityEvent.Type.Status ReadStatus(msg); break; - case 3: + case 3: //NetEntityEvent.Type.UpdateSkills int skillCount = msg.ReadByte(); for (int i = 0; i < skillCount; i++) { @@ -338,17 +345,17 @@ namespace Barotrauma info?.SetSkillLevel(skillIdentifier, skillLevel, WorldPosition + Vector2.UnitY * 150.0f); } break; - case 4: + case 4: //NetEntityEvent.Type.ExecuteAttack int attackLimbIndex = msg.ReadByte(); UInt16 targetEntityID = msg.ReadUInt16(); int targetLimbIndex = msg.ReadByte(); //255 = entity already removed, no need to do anything - if (attackLimbIndex == 255) { break; } + if (attackLimbIndex == 255 || Removed) { break; } if (attackLimbIndex >= AnimController.Limbs.Length) { - DebugConsole.ThrowError($"Received invalid ExecuteAttack message. Limb index out of bounds ({attackLimbIndex})"); + DebugConsole.ThrowError($"Received invalid ExecuteAttack message. Limb index out of bounds (character: {Name}, limb index: {attackLimbIndex}, limb count: {AnimController.Limbs.Length})"); break; } Limb attackLimb = AnimController.Limbs[attackLimbIndex]; @@ -362,7 +369,7 @@ namespace Barotrauma { if (targetLimbIndex >= targetCharacter.AnimController.Limbs.Length) { - DebugConsole.ThrowError($"Received invalid ExecuteAttack message. Target limb index out of bounds ({targetLimbIndex})"); + DebugConsole.ThrowError($"Received invalid 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]; @@ -372,6 +379,10 @@ namespace Barotrauma attackLimb.ExecuteAttack(targetEntity, targetLimb, out _); } break; + case 5: //NetEntityEvent.Type.AssignCampaignInteraction + byte campaignInteractionType = msg.ReadByte(); + (GameMain.GameSession?.GameMode as CampaignMode)?.AssignNPCMenuInteraction(this, (CampaignMode.InteractionType)campaignInteractionType); + break; } msg.ReadPadBits(); break; @@ -398,7 +409,7 @@ namespace Barotrauma Character character = null; if (noInfo) { - character = Create(speciesName, position, seed, null, true); + character = Create(speciesName, position, seed, null, false); character.ID = id; bool containsStatusData = inc.ReadBoolean(); if (containsStatusData) @@ -416,9 +427,14 @@ namespace Barotrauma CharacterInfo info = CharacterInfo.ClientRead(infoSpeciesName, inc); - character = Create(speciesName, position, seed, info, GameMain.Client.ID != ownerId, hasAi); + character = Create(speciesName, position, seed, info, ownerId > 0 && GameMain.Client.ID != ownerId, hasAi); character.ID = id; character.TeamID = (TeamType)teamID; + character.CampaignInteractionType = (CampaignMode.InteractionType)inc.ReadByte(); + if (character.CampaignInteractionType != CampaignMode.InteractionType.None) + { + (GameMain.GameSession.GameMode as CampaignMode)?.AssignNPCMenuInteraction(character, character.CampaignInteractionType); + } // Check if the character has a current order if (inc.ReadBoolean()) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 54c1ba354..336f42425 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -525,6 +525,7 @@ namespace Barotrauma }, TextManager.Get("GiveInButton"), style: "GUIButtonLarge") { + Visible = false, ToolTip = TextManager.Get(GameMain.NetworkMember == null ? "GiveInHelpSingleplayer" : "GiveInHelpMultiplayer"), OnClicked = (button, userData) => { @@ -944,6 +945,22 @@ namespace Barotrauma suicideButton.Visible = Character == Character.Controlled && !Character.IsDead && Character.IsIncapacitated; + if (GameMain.GameSession?.Campaign is { } campaign) + { + RectTransform endRoundButton = campaign?.EndRoundButton.RectTransform; + if (endRoundButton != null) + { + if (suicideButton.Visible) + { + endRoundButton.ScreenSpaceOffset = new Point(0, suicideButton.Rect.Height); + } + else if (endRoundButton.ScreenSpaceOffset != Point.Zero) + { + endRoundButton.ScreenSpaceOffset = Point.Zero; + } + } + } + cprButton.Visible = Character == Character.Controlled?.SelectedCharacter && (Character.IsUnconscious || Character.Stun > 0.0f) @@ -965,23 +982,29 @@ namespace Barotrauma public void AddToGUIUpdateList() { - if (GUI.DisableHUD) return; + if (GUI.DisableHUD) { return; } if (OpenHealthWindow == this) { healthInterfaceFrame.AddToGUIUpdateList(); afflictionTooltip?.AddToGUIUpdateList(); } - else if (Character.Controlled == Character) + else if (Character.Controlled == Character && !CharacterHUD.IsCampaignInterfaceOpen) { healthBarHolder.AddToGUIUpdateList(); } - if (suicideButton.Visible && Character == Character.Controlled) suicideButton.AddToGUIUpdateList(); - if (cprButton != null && cprButton.Visible) cprButton.AddToGUIUpdateList(); + if (suicideButton.Visible && Character == Character.Controlled) + { + suicideButton.AddToGUIUpdateList(); + } + if (cprButton != null && cprButton.Visible) + { + cprButton.AddToGUIUpdateList(); + } } public void DrawHUD(SpriteBatch spriteBatch) { - if (GUI.DisableHUD) return; + if (GUI.DisableHUD) { return; } if (GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y || Math.Abs(inventoryScale - Inventory.UIScale) > 0.01f || diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs index ad9f5ff23..f4ceb3ce9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs @@ -8,7 +8,7 @@ namespace Barotrauma { partial class JobPrefab : IPrefab, IDisposable { - public GUIButton CreateInfoFrame(int variant) + public GUIButton CreateInfoFrame(out GUIComponent buttonContainer) { int width = 500, height = 400; @@ -34,6 +34,8 @@ namespace Barotrauma font: GUI.SmallFont); } + buttonContainer = paddedFrame; + /*if (!ItemIdentifiers.TryGetValue(variant, out var itemIdentifiers)) { return backFrame; } var itemContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 0.5f), paddedFrame.RectTransform, Anchor.TopRight) { RelativeOffset = new Vector2(0.0f, 0.2f + descriptionBlock.RectTransform.RelativeSize.Y) }) @@ -54,7 +56,6 @@ namespace Barotrauma return frameHolder; } - public class OutfitPreview { /// diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index fd785b74b..a64888009 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -50,7 +50,12 @@ namespace Barotrauma } private static bool isOpen; - public static bool IsOpen => isOpen; + public static bool IsOpen + { + get => isOpen; + set => isOpen = value; + } + public static bool Paused = false; private static GUITextBlock activeQuestionText; @@ -66,6 +71,8 @@ namespace Barotrauma public static void Init() { + OpenAL.Alc.SetErrorReasonCallback((string msg) => NewMessage(msg, Color.Orange)); + frame = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.45f), GUI.Canvas) { MinSize = new Point(400, 300), AbsoluteOffset = new Point(10, 10) }, color: new Color(0.4f, 0.4f, 0.4f, 0.8f)); @@ -263,12 +270,31 @@ namespace Barotrauma try { - var textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), listBox.Content.RectTransform), - msg.Text, font: GUI.SmallFont, wrap: true) + if (msg.IsError) { - CanBeFocused = false, - TextColor = msg.Color - }; + var textContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), listBox.Content.RectTransform), style: "InnerFrame", color: Color.White) + { + CanBeFocused = false + }; + 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) + { + CanBeFocused = false, + TextColor = msg.Color + }; + textContainer.RectTransform.NonScaledSize = new Point(textContainer.RectTransform.NonScaledSize.X, textBlock.RectTransform.NonScaledSize.Y + 5); + textBlock.SetTextPos(); + } + else + { + var textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), listBox.Content.RectTransform), + msg.Text, font: GUI.SmallFont, wrap: true) + { + CanBeFocused = false, + TextColor = msg.Color + }; + } + listBox.UpdateScrollBarSize(); listBox.BarScroll = 1.0f; } @@ -446,6 +472,11 @@ namespace Barotrauma { GameMain.SpriteEditorScreen.Select(); })); + + commands.Add(new Command("editevents|eventeditor", "editevents/eventeditor: Switch to the Event Editor to edit scripted events.", (string[] args) => + { + GameMain.EventEditorScreen.Select(); + })); commands.Add(new Command("editcharacters|charactereditor", "editcharacters/charactereditor: Switch to the Character Editor to edit/create the ragdolls and animations of characters.", (string[] args) => { @@ -456,9 +487,11 @@ namespace Barotrauma GameMain.CharacterEditorScreen.Select(); })); - commands.Add(new Command("steamnetdebug", "steamnetdebug: Toggles Steamworks debug logging.", (string[] args) => + commands.Add(new Command("steamnetdebug", "steamnetdebug: Toggles Steamworks networking debug logging.", (string[] args) => { SteamManager.NetworkingDebugLog = !SteamManager.NetworkingDebugLog; + SteamManager.SetSteamworksNetworkingDebugLog(SteamManager.NetworkingDebugLog); + })); AssignRelayToServer("kick", false); @@ -493,6 +526,7 @@ namespace Barotrauma commands.Add(new Command("traitorlist", "", (string[] args) => { })); AssignRelayToServer("traitorlist", true); AssignRelayToServer("money", true); + AssignRelayToServer("setskill", true); AssignOnExecute("control", (string[] args) => { @@ -568,16 +602,46 @@ namespace Barotrauma AssignOnExecute("ambientlight", (string[] args) => { - Color color = XMLExtensions.ParseColor(string.Join(",", args)); + bool add = string.Equals(args.LastOrDefault(), "add"); + string colorString = string.Join(",", add ? args.SkipLast(1) : args); + if (colorString.Equals("restore", StringComparison.OrdinalIgnoreCase)) + { + foreach (Hull hull in Hull.hullList) + { + if (hull.OriginalAmbientLight != null) + { + hull.AmbientLight = hull.OriginalAmbientLight.Value; + hull.OriginalAmbientLight = null; + } + } + NewMessage("Restored all hull ambient lights", Color.White); + return; + } + + Color color = XMLExtensions.ParseColor(colorString); if (Level.Loaded != null) { Level.Loaded.GenerationParams.AmbientLightColor = color; } else { - GameMain.LightManager.AmbientLight = color; + GameMain.LightManager.AmbientLight = add ? GameMain.LightManager.AmbientLight.Add(color) : color; + } + + foreach (Hull hull in Hull.hullList) + { + hull.OriginalAmbientLight ??= hull.AmbientLight; + hull.AmbientLight = add ? hull.AmbientLight.Add(color) : color; + } + + if (add) + { + NewMessage($"Set ambient light color to {color}.", Color.White); + } + else + { + NewMessage($"Increased ambient light by {color}.", Color.White); } - NewMessage("Set ambient light color to " + color + ".", Color.White); }); AssignRelayToServer("ambientlight", false); @@ -849,17 +913,6 @@ namespace Barotrauma TutorialMode.StartTutorial(Tutorials.Tutorial.Tutorials[0]); })); - commands.Add(new Command("lobby|lobbyscreen", "", (string[] args) => - { - if (GameMain.Client != null) - { - ThrowError("This command cannot be used in multiplayer."); - return; - } - - GameMain.LobbyScreen.Select(); - })); - 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; } @@ -1011,9 +1064,39 @@ namespace Barotrauma }); AssignRelayToServer("toggleaitargets|aitargets", false); + AssignOnExecute("debugai", (string[] args) => + { + HumanAIController.debugai = !HumanAIController.debugai; + if (HumanAIController.debugai) + { + GameMain.DebugDraw = true; + GameMain.LightManager.LightingEnabled = false; + GameMain.LightManager.LosEnabled = false; + } + else + { + GameMain.DebugDraw = false; + GameMain.LightManager.LightingEnabled = true; + GameMain.LightManager.LosEnabled = true; + } + NewMessage(HumanAIController.debugai ? "AI debug info visible" : "AI debug info hidden", Color.White); + }); + AssignRelayToServer("debugai", false); + AssignRelayToServer("water|editwater", false); AssignRelayToServer("fire|editfire", false); + commands.Add(new Command("togglecampaignteleport", "togglecampaignteleport: Toggle on/off teleportation between campaign locations by double clicking on the campaign map.", (string[] args) => + { + if (GameMain.GameSession?.Campaign == null) + { + ThrowError("No campaign active."); + return; + } + GameMain.GameSession.Map.AllowDebugTeleport = !GameMain.GameSession.Map.AllowDebugTeleport; + NewMessage((GameMain.GameSession.Map.AllowDebugTeleport ? "Enabled" : "Disabled") + " teleportation on the campaign map.", Color.White); + }, isCheat: true)); + commands.Add(new Command("mute", "mute [name]: Prevent the client from speaking to anyone through the voice chat. Using this command requires a permission from the server host.", null, () => @@ -1044,7 +1127,7 @@ namespace Barotrauma } foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) { - int? minCost = itemPrefab.GetPrices()?.Min(p => p.BuyPrice); + int? minCost = itemPrefab.GetMinPrice(); int? fabricationCost = null; int? deconstructProductCost = null; @@ -1053,7 +1136,7 @@ namespace Barotrauma { foreach (var ingredient in fabricationRecipe.RequiredItems) { - int? ingredientPrice = ingredient.ItemPrefab.GetPrices()?.Min(p => p.BuyPrice); + int? ingredientPrice = ingredient.ItemPrefab.GetMinPrice(); if (ingredientPrice.HasValue) { if (!fabricationCost.HasValue) { fabricationCost = 0; } @@ -1071,7 +1154,7 @@ namespace Barotrauma continue; } - int? deconstructProductPrice = targetItem.GetPrices()?.Min(p => p.BuyPrice); + int? deconstructProductPrice = targetItem.GetMinPrice(); if (deconstructProductPrice.HasValue) { if (!deconstructProductCost.HasValue) { deconstructProductCost = 0; } @@ -1300,7 +1383,7 @@ namespace Barotrauma commands.Add(new Command("eventstats", "", (string[] args) => { - var debugLines = ScriptedEventSet.GetDebugStatistics(); + var debugLines = EventSet.GetDebugStatistics(); string filePath = "eventstats.txt"; File.WriteAllLines(filePath, debugLines); ToolBox.OpenFileWithShell(Path.GetFullPath(filePath)); @@ -1537,6 +1620,76 @@ namespace Barotrauma File.WriteAllLines(filePath, lines); })); + commands.Add(new Command("dumpeventtexts", "dumpeventtexts [filepath]: gets the texts from event files and and writes them into a file along with xml tags that can be used in translation files. If the filepath is omitted, the file is written to Content/Texts/EventTexts.txt", (string[] args) => + { + string filePath = args.Length > 0 ? args[0] : "Content/Texts/EventTexts.txt"; + List lines = new List(); + HashSet docs = new HashSet(); + HashSet textIds = new HashSet(); + + foreach (EventPrefab eventPrefab in EventSet.GetAllEventPrefabs()) + { + if (string.IsNullOrEmpty(eventPrefab.Identifier)) + { + continue; + } + docs.Add(eventPrefab.ConfigElement.Document); + getTextsFromElement(eventPrefab.ConfigElement, lines, eventPrefab.Identifier); + } + File.WriteAllLines(filePath, lines); + + ToolBox.OpenFileWithShell(Path.GetFullPath(filePath)); + + System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings + { + Indent = true, + NewLineOnAttributes = false + }; + + foreach (XDocument doc in docs) + { + using (var writer = XmlWriter.Create(new System.Uri(doc.BaseUri).LocalPath, settings)) + { + doc.WriteTo(writer); + writer.Flush(); + } + } + + void getTextsFromElement(XElement element, List list, string parentName) + { + string text = element.GetAttributeString("text", null); + string textId = $"EventText.{parentName}"; + if (!string.IsNullOrEmpty(text) && !text.Contains("EventText.", StringComparison.OrdinalIgnoreCase)) + { + list.Add($"<{textId}>{text}"); + element.SetAttributeValue("text", textId); + } + + int i = 1; + foreach (XElement subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "conversationaction": + while (textIds.Contains(parentName+".c"+i)) + { + i++; + } + parentName += ".c" + i; + break; + case "option": + while (textIds.Contains(parentName.Substring(0, parentName.Length - 3) + ".o" + i)) + { + i++; + } + parentName = parentName.Substring(0, parentName.Length - 3) + ".o" + i; + break; + } + textIds.Add(parentName); + getTextsFromElement(subElement, list, parentName); + } + } + })); commands.Add(new Command("itemcomponentdocumentation", "", (string[] args) => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs new file mode 100644 index 000000000..6d943d132 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs @@ -0,0 +1,365 @@ +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework.Graphics; + +namespace Barotrauma +{ + partial class ConversationAction : EventAction + { + private GUIMessageBox dialogBox; + + private static ConversationAction lastActiveAction; + private static GUIMessageBox lastMessageBox; + + public static bool IsDialogOpen + { + get + { + return GUIMessageBox.MessageBoxes.Any(mb => + mb.UserData as string == "ConversationAction" || + (mb.UserData is Pair pair && pair.First == "ConversationAction")); + } + } + public static bool FadeScreenToBlack + { + get { return IsDialogOpen && shouldFadeToBlack; } + } + + private static bool shouldFadeToBlack; + + private bool IsBlockedByAnotherConversation(IEnumerable _) + { + return + lastActiveAction != null && + lastActiveAction.ParentEvent != ParentEvent && + Timing.TotalTime < lastActiveAction.lastActiveTime + BlockOtherConversationsDuration; + } + + partial void ShowDialog(Character speaker, Character targetCharacter) + { + CreateDialog(Text, speaker, Options.Select(opt => opt.Text), GetEndingOptions(), actionInstance: this, spriteIdentifier: EventSprite, fadeToBlack: FadeToBlack, dialogType: DialogType, continueConversation: ContinueConversation); + } + + public static void CreateDialog(string text, Character speaker, IEnumerable options, int[] closingOptions, string eventSprite, UInt16 actionId, bool fadeToBlack, DialogTypes dialogType, bool continueConversation = false) + { + CreateDialog(text, speaker, options, closingOptions, actionInstance: null, actionId: actionId, spriteIdentifier: eventSprite, fadeToBlack: fadeToBlack, dialogType: dialogType, continueConversation: continueConversation); + } + + private static void CreateDialog(string text, Character speaker, IEnumerable options, int[] closingOptions, string spriteIdentifier = null, + ConversationAction actionInstance = null, UInt16? actionId = null, bool fadeToBlack = false, DialogTypes dialogType = DialogTypes.Regular, bool continueConversation = false) + { + Debug.Assert(actionInstance == null || actionId == null); + + shouldFadeToBlack = fadeToBlack; + + if (lastMessageBox != null && !lastMessageBox.Closed && GUIMessageBox.MessageBoxes.Contains(lastMessageBox)) + { + if (actionId != null && lastMessageBox.UserData is Pair userData) + { + if (userData.Second == actionId) { return; } + lastMessageBox.UserData = new Pair("ConversationAction", actionId.Value); + } + + GUIListBox conversationList = lastMessageBox.FindChild("conversationlist", true) as GUIListBox; + Debug.Assert(conversationList != null); + + // gray out the last text block + if (conversationList.Content.Children.LastOrDefault() is GUILayoutGroup lastElement) + { + if (lastElement.FindChild("text", true) is GUITextBlock textLayout) + { + textLayout.OverrideTextColor(Color.DarkGray * 0.8f); + } + } + + List extraButtons = CreateConversation(conversationList, text, speaker, options, string.IsNullOrWhiteSpace(spriteIdentifier)); + AssignActionsToButtons(extraButtons, lastMessageBox); + RecalculateLastMessage(conversationList, true); + + conversationList.ScrollToEnd(0.5f); + lastMessageBox.SetBackgroundIcon(EventSet.GetEventSprite(spriteIdentifier)); + return; + } + + var (relative, min) = GetSizes(dialogType); + + GUIMessageBox messageBox = new GUIMessageBox(string.Empty, string.Empty, new string[0], + relativeSize: relative, minSize: min, + type: GUIMessageBox.Type.InGame, backgroundIcon: EventSet.GetEventSprite(spriteIdentifier)) + { + UserData = "ConversationAction" + }; + + lastMessageBox = messageBox; + + messageBox.InnerFrame.ClearChildren(); + messageBox.AutoClose = false; + GUI.Style.Apply(messageBox.InnerFrame, "DialogBox"); + + if (actionInstance != null) + { + lastActiveAction = actionInstance; + actionInstance.dialogBox = messageBox; + } + else + { + messageBox.UserData = new Pair("ConversationAction", actionId.Value); + } + + int padding = GUI.IntScale(16); + + GUIListBox listBox = new GUIListBox(new RectTransform(messageBox.InnerFrame.Rect.Size - new Point(padding * 2), messageBox.InnerFrame.RectTransform, Anchor.Center), style: null) + { + KeepSpaceForScrollBar = true, + HoverCursor = CursorState.Default, + UserData = "conversationlist" + }; + + List buttons = CreateConversation(listBox, text, speaker, options, string.IsNullOrWhiteSpace(spriteIdentifier)); + AssignActionsToButtons(buttons, messageBox); + RecalculateLastMessage(listBox, false); + + messageBox.InnerFrame.RectTransform.MinSize = new Point(0, Math.Max(listBox.RectTransform.MinSize.Y + padding * 2, (int)(100 * GUI.yScale))); + + var shadow = new GUIFrame(new RectTransform(messageBox.InnerFrame.Rect.Size + new Point(padding * 4), messageBox.InnerFrame.RectTransform, Anchor.Center), style: "OuterGlow") + { + Color = Color.Black * 0.7f + }; + shadow.SetAsFirstChild(); + + void RecalculateLastMessage(GUIListBox conversationList, bool append) + { + if (conversationList.Content.Children.LastOrDefault() is GUILayoutGroup lastElement) + { + GUILayoutGroup textLayout = lastElement.GetChild(); + if (lastElement.Rect.Size.Y < textLayout.Rect.Size.Y && !append) + { + lastElement.RectTransform.MinSize = textLayout.Rect.Size; + } + if (textLayout != null) + { + int textHeight = textLayout.Children.Sum(c => c.Rect.Height); + textLayout.RectTransform.MaxSize = new Point(lastElement.RectTransform.MaxSize.X, textHeight); + textLayout.Recalculate(); + } + int sumHeight = lastElement.Children.Sum(c => c.Rect.Height); + lastElement.RectTransform.MaxSize = new Point(lastElement.RectTransform.MaxSize.X, sumHeight); + lastElement.Recalculate(); + conversationList.RecalculateChildren(); + + if (!append || textLayout == null) { return; } + + foreach (GUIComponent child in textLayout.Children) + { + conversationList.UpdateScrollBarSize(); + float wait = conversationList.BarSize < 1.0f ? 0.5f : 0.0f; + + if (child is GUITextBlock) { child.FadeIn(wait, 0.5f); } + + if (child is GUIButton btn) + { + btn.FadeIn(wait, 1.0f); + btn.TextBlock.FadeIn(wait, 0.5f); + } + } + } + } + + void AssignActionsToButtons(List optionButtons, GUIMessageBox target) + { + if (!options.Any()) + { + GUIButton closeButton = new GUIButton(new RectTransform(Vector2.One, target.InnerFrame.RectTransform, Anchor.BottomRight, scaleBasis: ScaleBasis.Smallest) + { + MaxSize = new Point(GUI.IntScale(24)), + MinSize = new Point(24), + AbsoluteOffset = new Point(GUI.IntScale(48), GUI.IntScale(16)) + }, style: "GUIButtonVerticalArrow") + { + UserData = "ContinueButton", + IgnoreLayoutGroups = true, + Bounce = true, + OnClicked = (btn, userdata) => + { + if (actionInstance != null) + { + actionInstance.selectedOption = 0; + } + else if (actionId.HasValue) + { + SendResponse(actionId.Value, 0); + } + + if (!continueConversation) + { + target.Close(); + } + else + { + btn.Frame.FadeOut(0.33f, true); + } + + return true; + } + }; + + closeButton.Children.ForEach(child => child.SpriteEffects = SpriteEffects.FlipVertically); + closeButton.Frame.FadeIn(0.5f, 0.5f); + closeButton.SlideIn(0.5f, 0.33f, 16, SlideDirection.Down); + } + + for (int i = 0; i < optionButtons.Count; i++) + { + optionButtons[i].UserData = i; + optionButtons[i].OnClicked += (btn, userdata) => + { + int selectedOption = (userdata as int?) ?? 0; + if (actionInstance != null) + { + actionInstance.selectedOption = selectedOption; + foreach (GUIButton otherButton in optionButtons) + { + otherButton.CanBeFocused = false; + if (otherButton != btn) + { + otherButton.TextBlock.OverrideTextColor(Color.DarkGray * 0.8f); + } + } + btn.ExternalHighlight = true; + return true; + } + + if (actionId.HasValue) + { + SendResponse(actionId.Value, selectedOption); + btn.CanBeFocused = false; + btn.ExternalHighlight = true; + foreach (GUIButton otherButton in optionButtons) + { + otherButton.CanBeFocused = false; + if (otherButton != btn) + { + otherButton.TextBlock.OverrideTextColor(Color.DarkGray * 0.8f); + } + } + return true; + } + //should not happen + return false; + }; + + if (closingOptions.Contains(i)) { optionButtons[i].OnClicked += target.Close; } + } + } + } + + private static Tuple GetSizes(DialogTypes dialogTypes) + { + return dialogTypes switch + { + DialogTypes.Regular => Tuple.Create(new Vector2(0.3f, 0.2f), new Point(512, 256)), + _ => Tuple.Create(new Vector2(0.3f, 0.15f), new Point(512, 128)) + }; + } + + private static List CreateConversation(GUIListBox parentBox, string text, Character speaker, IEnumerable options, bool drawChathead = true) + { + var content = new GUILayoutGroup(new RectTransform(Vector2.One, parentBox.Content.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true) + { + Stretch = true, + CanBeFocused = true, + AlwaysOverrideCursor = true + }; + + string translatedText = TextManager.Get(text, returnNull: true) ?? text; + + if (speaker?.Info != null && drawChathead) + { + // chathead + new GUICustomComponent(new RectTransform(new Vector2(0.15f, 0.8f), content.RectTransform), onDraw: (sb, customComponent) => + { + speaker.Info.DrawIcon(sb, customComponent.Rect.Center.ToVector2(), customComponent.Rect.Size.ToVector2()); + }); + } + + var textContent = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), content.RectTransform), childAnchor: Anchor.TopCenter) + { + AbsoluteSpacing = GUI.IntScale(5) + }; + + var textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), textContent.RectTransform), translatedText, wrap: true) + { + AlwaysOverrideCursor = true, + UserData = "text" + }; + + List buttons = new List(); + if (options.Any()) + { + 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"); + btn.TextBlock.TextAlignment = Alignment.CenterLeft; + btn.TextColor = btn.HoverTextColor = GUI.Style.Green; + btn.TextBlock.Wrap = true; + buttons.Add(btn); + } + } + + content.Recalculate(); + textContent.Recalculate(); + textBlock.CalculateHeightFromText(); + textBlock.RectTransform.MinSize = new Point(0, (int)(textBlock.Rect.Height * 1.2f)); + foreach (GUIButton btn in buttons) + { + btn.TextBlock.SetTextPos(); + btn.TextBlock.CalculateHeightFromText(); + btn.RectTransform.MinSize = new Point(0, (int)(btn.TextBlock.Rect.Height * 1.2f)); + } + + textContent.RectTransform.MinSize = new Point(0, textContent.Children.Sum(c => c.Rect.Height + textContent.AbsoluteSpacing) + GUI.IntScale(16)); + // content.RectTransform.MinSize = new Point(0, textContent.Rect.Height); + + return buttons; + } + + private static void SendResponse(UInt16 actionId, int selectedOption) + { + IWriteMessage outmsg = new WriteOnlyMessage(); + outmsg.Write((byte)ClientPacketHeader.EVENTMANAGER_RESPONSE); + outmsg.Write(actionId); + outmsg.Write((byte)selectedOption); + GameMain.Client?.ClientPeer?.Send(outmsg, DeliveryMethod.Reliable); + } + + // Too broken, left it here if I ever want to come back to it + private static List GetQuoteHighlights(string text, Color color) + { + char[] quotes = { '“', '”', '\"', '\'', '「', '」'}; + + List textColors = new List { new RichTextData { StartIndex = 0 } }; + bool start = true; + for (int i = 0; i < text.Length; i++) + { + char c = text[i]; + if (quotes.Contains(c)) + { + textColors.Last().EndIndex = i - 1; + textColors.Add(new RichTextData { StartIndex = i, Color = start ? color : (Color?) null }); + start = !start; + } + } + + if (textColors.LastOrDefault() is { } last && last.EndIndex == 0) + { + last.EndIndex = text.Length; + } + return textColors; + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs index 442c0384e..f23070044 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs @@ -1,6 +1,12 @@ -using Microsoft.Xna.Framework; +#nullable enable +using Barotrauma.Extensions; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; namespace Barotrauma { @@ -11,34 +17,43 @@ namespace Barotrauma private float intensityGraphUpdateInterval; private float lastIntensityUpdate; + private Event? pinnedEvent; + private Vector2 pinnedPosition = new Vector2(256, 128); + private bool isDragging; + public void DebugDraw(SpriteBatch spriteBatch) { - foreach (ScriptedEvent ev in activeEvents) + foreach (Event ev in activeEvents) { Vector2 drawPos = ev.DebugDrawPos; drawPos.Y = -drawPos.Y; var textOffset = new Vector2(-150, 0); - ShapeExtensions.DrawCircle(spriteBatch, drawPos, 600, 6, Color.White, thickness: 20); + spriteBatch.DrawCircle(drawPos, 600, 6, Color.White, thickness: 20); GUI.DrawString(spriteBatch, drawPos + textOffset, ev.ToString(), Color.White, Color.Black, 0, GUI.LargeFont); } } public void DebugDrawHUD(SpriteBatch spriteBatch, int y) { + foreach (ScriptedEvent scriptedEvent in activeEvents.Where(ev => !ev.IsFinished && ev is ScriptedEvent).Cast()) + { + DrawEventTargetTags(spriteBatch, scriptedEvent); + } + 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: " + eventCoolDown, 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(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(15, y + 65), "AvgHealth: " + (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), "AvgHullIntegrity: " + (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), "FloodingAmount: " + (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), "FireAmount: " + (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), "EnemyDanger: " + (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 + 65), "AvgHealth: " + (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), "AvgHullIntegrity: " + (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), "FloodingAmount: " + (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), "FireAmount: " + (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), "EnemyDanger: " + (int) Math.Round(enemyDanger * 100), Color.Lerp(GUI.Style.Green, GUI.Style.Red, enemyDanger), Color.Black * 0.6f, 0, GUI.SmallFont); #if DEBUG - if (PlayerInput.KeyDown(Microsoft.Xna.Framework.Input.Keys.LeftAlt) && + if (PlayerInput.KeyDown(Microsoft.Xna.Framework.Input.Keys.LeftAlt) && PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.T)) { eventCoolDown = 1.0f; @@ -56,7 +71,7 @@ namespace Barotrauma { intensityGraph.Update(currentIntensity); targetIntensityGraph.Update(targetIntensity); - lastIntensityUpdate = (float)Timing.TotalTime; + lastIntensityUpdate = (float) Timing.TotalTime; } Rectangle graphRect = new Rectangle(15, y + 150, 150, 50); @@ -72,20 +87,19 @@ namespace Barotrauma y = graphRect.Bottom + 20; if (eventCoolDown > 0.0f) { - GUI.DrawString(spriteBatch, new Vector2(graphRect.X, y), "Event cooldown active: " + (int)eventCoolDown, Color.LightGreen * 0.8f, null, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(graphRect.X, y), "Event cooldown active: " + (int) eventCoolDown, Color.LightGreen * 0.8f, null, 0, GUI.SmallFont); y += 15; } else if (currentIntensity > eventThreshold) { GUI.DrawString(spriteBatch, new Vector2(graphRect.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, GUI.SmallFont); y += 15; } - foreach (ScriptedEventSet eventSet in pendingEventSets) + + foreach (EventSet eventSet in pendingEventSets) { - float distanceTraveled = MathHelper.Clamp( - (Submarine.MainSub.WorldPosition.X - level.StartPosition.X) / (level.EndPosition.X - level.StartPosition.X), - 0.0f, 1.0f); + if (Submarine.MainSub == null) { break; } GUI.DrawString(spriteBatch, new Vector2(graphRect.X, y), "New event (ID " + eventSet.DebugIdentifier + ") after: ", Color.Orange * 0.8f, null, 0, GUI.SmallFont); y += 12; @@ -94,36 +108,421 @@ namespace Barotrauma roundDuration < eventSet.MinMissionTime) { GUI.DrawString(spriteBatch, new Vector2(graphRect.X, y), - " " + (int)(eventSet.MinDistanceTraveled * 100.0f) + "% travelled (current: " + (int)(distanceTraveled * 100.0f) + " %)", + " " + (int) (eventSet.MinDistanceTraveled * 100.0f) + "% travelled (current: " + (int) (distanceTraveled * 100.0f) + " %)", Color.Orange * 0.8f, null, 0, GUI.SmallFont); y += 12; } + if (CurrentIntensity < eventSet.MinIntensity || CurrentIntensity > eventSet.MaxIntensity) { GUI.DrawString(spriteBatch, new Vector2(graphRect.X, y), - " intensity between " + ((int)eventSet.MinIntensity) + " and " + ((int)eventSet.MaxIntensity), + " intensity between " + ((int) eventSet.MinIntensity) + " and " + ((int) eventSet.MaxIntensity), Color.Orange * 0.8f, null, 0, GUI.SmallFont); y += 12; } + if (roundDuration < eventSet.MinMissionTime) { GUI.DrawString(spriteBatch, new Vector2(graphRect.X, y), - " " + (int)(eventSet.MinMissionTime - roundDuration) + " s", + " " + (int) (eventSet.MinMissionTime - roundDuration) + " s", Color.Orange * 0.8f, null, 0, GUI.SmallFont); } y += 15; } - GUI.DrawString(spriteBatch, new Vector2(graphRect.X, y), "Current events: ", Color.White * 0.9f, null, 0, GUI.SmallFont); - y += 12; - foreach (ScriptedEvent scriptedEvent in activeEvents) + y += 15; + + foreach (Event ev in activeEvents.Where(ev => !ev.IsFinished || PlayerInput.IsShiftDown())) { - if (scriptedEvent.IsFinished) { continue; } - GUI.DrawString(spriteBatch, new Vector2(graphRect.X + 5, y), scriptedEvent.ToString(), Color.White * 0.8f, null, 0, GUI.SmallFont); - y += 12; + GUI.DrawString(spriteBatch, new Vector2(graphRect.X + 5, y), ev.ToString(), (!ev.IsFinished ? Color.White : Color.Red) * 0.8f, null, 0, GUI.SmallFont); + + Rectangle rect = new Rectangle(new Point(graphRect.X + 5, y), GUI.SmallFont.MeasureString(ev.ToString()).ToPoint()); + + Rectangle outlineRect = new Rectangle(rect.Location, rect.Size); + outlineRect.Inflate(4, 4); + + if (pinnedEvent == ev) { GUI.DrawRectangle(spriteBatch, outlineRect, Color.White); } + + if (rect.Contains(PlayerInput.MousePosition)) + { + GUI.MouseCursor = CursorState.Hand; + GUI.DrawRectangle(spriteBatch, outlineRect, Color.White); + + if (ev != pinnedEvent) + { + DrawEvent(spriteBatch, ev, rect); + } + else if (PlayerInput.SecondaryMouseButtonHeld() || PlayerInput.SecondaryMouseButtonDown()) + { + pinnedEvent = null; + } + + if (PlayerInput.PrimaryMouseButtonHeld() || PlayerInput.PrimaryMouseButtonDown()) + { + pinnedEvent = ev; + } + } + + y += 18; + } + } + + public void DrawPinnedEvent(SpriteBatch spriteBatch) + { + if (pinnedEvent != null) + { + Rectangle rect = DrawEvent(spriteBatch, pinnedEvent, null); + + if (rect != Rectangle.Empty) + { + if (rect.Contains(PlayerInput.MousePosition) && !isDragging) + { + GUI.MouseCursor = CursorState.Move; + if (PlayerInput.PrimaryMouseButtonDown() || PlayerInput.PrimaryMouseButtonHeld()) + { + isDragging = true; + } + + if (PlayerInput.SecondaryMouseButtonClicked() || PlayerInput.SecondaryMouseButtonHeld()) + { + pinnedEvent = null; + isDragging = false; + } + } + } + + if (isDragging) + { + GUI.MouseCursor = CursorState.Dragging; + pinnedPosition = PlayerInput.MousePosition - (new Vector2(rect.Width / 2.0f, -24)); + if (!PlayerInput.PrimaryMouseButtonHeld()) + { + isDragging = false; + } + } + } + } + + private static void DrawEventTargetTags(SpriteBatch spriteBatch, ScriptedEvent scriptedEvent) + { + if (Screen.Selected is GameScreen screen) + { + Camera cam = screen.Cam; + Dictionary> tagsDictionary = new Dictionary>(); + foreach ((string key, List value) in scriptedEvent.Targets) + { + foreach (Entity entity in value) + { + if (tagsDictionary.ContainsKey(entity)) + { + tagsDictionary[entity].Add(key); + } + else + { + tagsDictionary.Add(entity, new List { key }); + } + } + } + + string identifier = scriptedEvent.Prefab.Identifier; + + 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}"; } + + List richTextData = RichTextData.GetRichTextData(text, out text); + + Vector2 entityPos = cam.WorldToScreen(entity.WorldPosition); + Vector2 infoSize = GUI.SmallFont.MeasureString(text); + + Vector2 infoPos = entityPos + new Vector2(128 * cam.Zoom, -(128 * cam.Zoom)); + infoPos.Y -= infoSize.Y / 2; + + Rectangle infoRect = new Rectangle(infoPos.ToPoint(), infoSize.ToPoint()); + infoRect.Inflate(4, 4); + + 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.DrawLine(spriteBatch, entityPos, new Vector2(infoRect.Location.X, infoRect.Location.Y + infoRect.Height / 2), Color.White); + } + } + } + + private readonly struct DebugLine + { + public readonly Vector2 Position; + public readonly Color Color; + + public DebugLine(Vector2 position, Color color) + { + Position = position; + Color = color; + } + } + + private Rectangle DrawEvent(SpriteBatch spriteBatch, Event ev, Rectangle? parentRect = null) + { + return ev switch + { + ScriptedEvent scriptedEvent => DrawScriptedEvent(spriteBatch, scriptedEvent, parentRect), + ArtifactEvent artifactEvent => DrawArtifactEvent(spriteBatch, artifactEvent, parentRect), + MonsterEvent monsterEvent => DrawMonsterEvent(spriteBatch, monsterEvent, parentRect), + _ => Rectangle.Empty + }; + } + + private Rectangle DrawScriptedEvent(SpriteBatch spriteBatch, ScriptedEvent scriptedEvent, Rectangle? parentRect = null) + { + EventAction? currentEvent = !scriptedEvent.IsFinished ? scriptedEvent.Actions[scriptedEvent.CurrentActionIndex] : null; + + List positions = new List(); + + string text = $"Finished: {scriptedEvent.IsFinished.ColorizeObject()}\n" + + $"Action index: {scriptedEvent.CurrentActionIndex.ColorizeObject()}\n" + + $"Current action: {currentEvent?.ToDebugString() ?? ToolBox.ColorizeObject(null)}\n"; + + text += "All actions:\n"; + text += FindActions(scriptedEvent).Aggregate(string.Empty, (current, action) => current + $"{new string(' ', action.Item1 * 6)}{action.Item2.ToDebugString()}\n"); + + text += "Targets:\n"; + foreach (var (key, value) in scriptedEvent.Targets) + { + text += $" {key.ColorizeObject()}: {value.Aggregate(string.Empty, (current, entity) => current + $"{entity.ColorizeObject()} ")}\n"; + } + + if (scriptedEvent.Targets != null) + { + foreach ((_, List entities) in scriptedEvent.Targets) + { + if (entities == null || !entities.Any()) { continue; } + + foreach (var entity in entities) + { + positions.Add(new DebugLine(entity.WorldPosition, Color.White)); + } + } + } + + return DrawInfoRectangle(spriteBatch, scriptedEvent, text, parentRect, positions); + } + + private Rectangle DrawArtifactEvent(SpriteBatch spriteBatch, ArtifactEvent artifactEvent, Rectangle? parentRect = null) + { + List positions = new List(); + + string text = $"Finished: {artifactEvent.IsFinished.ColorizeObject()}\n" + + $"Item: {artifactEvent.Item.ColorizeObject()}\n" + + $"Spawn pending: {artifactEvent.SpawnPending.ColorizeObject()}\n" + + $"Spawn position: {artifactEvent.SpawnPos.ColorizeObject()}\n"; + + if (artifactEvent.Item != null) + { + Vector2 pos = artifactEvent.Item.WorldPosition; + positions.Add(new DebugLine(pos, Color.White)); + } + + return DrawInfoRectangle(spriteBatch, artifactEvent, text, parentRect, positions); + } + + private Rectangle DrawMonsterEvent(SpriteBatch spriteBatch, MonsterEvent monsterEvent, Rectangle? parentRect = null) + { + List positions = new List(); + + string text = $"Finished: {monsterEvent.IsFinished.ColorizeObject()}\n" + + $"Amount: {monsterEvent.MinAmount.ColorizeObject()} - {monsterEvent.MaxAmount.ColorizeObject()}\n" + + $"Spawn pending: {monsterEvent.SpawnPending.ColorizeObject()}\n" + + $"Spawn position: {monsterEvent.SpawnPos.ColorizeObject()}\n"; + + if (monsterEvent.SpawnPos != null && Submarine.MainSub != null) + { + Vector2 pos = monsterEvent.SpawnPos.Value; + text += $"Distance from submarine: {Vector2.Distance(pos, Submarine.MainSub.WorldPosition).ColorizeObject()}\n"; + positions.Add(new DebugLine(pos, Color.White)); + } + + if (monsterEvent.Monsters != null) + { + text += !monsterEvent.Monsters.Any() ? $"Monsters: {"None".ColorizeObject()}" : "Monsters:\n"; + + foreach (Character monster in monsterEvent.Monsters) + { + text += $" {monster.ColorizeObject()} -> (Dead: {monster.IsDead.ColorizeObject()}, Health: {monster.HealthPercentage.ColorizeObject()}%, AIState: {(monster.AIController?.State).ColorizeObject()})\n"; + positions.Add(new DebugLine(monster.WorldPosition, Color.Red)); + } + } + + return DrawInfoRectangle(spriteBatch, monsterEvent, text, parentRect, positions); + } + + private Rectangle DrawInfoRectangle(SpriteBatch spriteBatch, Event @event, string text, Rectangle? parentRect = null, List? drawPoints = null) + { + text = text.TrimEnd('\r', '\n'); + + string identifier = @event.Prefab.Identifier; + if (!string.IsNullOrWhiteSpace(identifier)) + { + text = $"Identifier: {identifier.ColorizeObject()}\n{text}"; + } + + List richTextData = RichTextData.GetRichTextData(text, out text); + + Vector2 size = GUI.SmallFont.MeasureString(text); + Vector2 pos = pinnedPosition; + Rectangle infoRect; + Rectangle? infoBarRect = null; + + if (parentRect != null) + { + Rectangle rect = parentRect.Value; + pos = new Vector2(350, GameMain.GraphicsHeight / 2.0f - size.Y / 2); + infoRect = new Rectangle(pos.ToPoint(), size.ToPoint()); + infoRect.Inflate(8, 8); + + GUI.DrawLine(spriteBatch, new Vector2(rect.Right, rect.Y + rect.Height / 2), new Vector2(infoRect.X, infoRect.Y + infoRect.Height / 2), Color.White); + } + else + { + infoRect = new Rectangle(pos.ToPoint(), size.ToPoint()); + infoRect.Inflate(8, 8); + + Rectangle barRect = new Rectangle(infoRect.Left, infoRect.Top - 32, infoRect.Width, 32); + 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.DrawRectangle(spriteBatch, barRect, Color.White); + infoBarRect = barRect; + } + + if (drawPoints != null && drawPoints.Any() && Screen.Selected?.Cam != null) + { + foreach (DebugLine line in drawPoints) + { + if (line.Position != Vector2.Zero) + { + float xPos = infoRect.Right; + + if (parentRect == null && pinnedPosition.X + infoRect.Width / 2.0f > GameMain.GraphicsWidth / 2.0f) + { + xPos = infoRect.Left; + } + + GUI.DrawLine(spriteBatch, new Vector2(xPos, infoRect.Top + infoRect.Height / 2), Screen.Selected.Cam.WorldToScreen(line.Position), line.Color); + } + } + } + + 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(); + return infoBarRect ?? infoRect; + } + + public void ClientRead(IReadMessage msg) + { + NetworkEventType eventType = (NetworkEventType)msg.ReadByte(); + switch (eventType) + { + case NetworkEventType.STATUSEFFECT: + string eventIdentifier = msg.ReadString(); + UInt16 actionIndex = msg.ReadUInt16(); + UInt16 targetCount = msg.ReadUInt16(); + List targets = new List(); + for (int i = 0; i < targetCount; i++) + { + UInt16 targetID = msg.ReadUInt16(); + Entity target = Entity.FindEntityByID(targetID); + if (target != null) { targets.Add(target); } + } + + var eventPrefab = EventSet.GetEventPrefab(eventIdentifier); + if (eventPrefab == null) { return; } + int j = 0; + foreach (XElement element in eventPrefab.ConfigElement.Descendants()) + { + if (j != actionIndex) + { + j++; + continue; + } + foreach (XElement subElement in element.Elements()) + { + if (!subElement.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase)) { continue; } + StatusEffect effect = StatusEffect.Load(subElement, $"EventManager.ClientRead ({eventIdentifier})"); + foreach (Entity target in targets) + { + effect.Apply(effect.type, 1.0f, target, target as ISerializableEntity); + } + } + break; + } + break; + case NetworkEventType.CONVERSATION: + UInt16 identifier = msg.ReadUInt16(); + string eventSprite = msg.ReadString(); + byte dialogType = msg.ReadByte(); + bool continueConversation = msg.ReadBoolean(); + UInt16 speakerId = msg.ReadUInt16(); + string text = msg.ReadString(); + bool fadeToBlack = msg.ReadBoolean(); + byte optionCount = msg.ReadByte(); + List options = new List(); + for (int i = 0; i < optionCount; i++) + { + options.Add(msg.ReadString()); + } + + byte endCount = msg.ReadByte(); + int[] endings = new int[endCount]; + for (int i = 0; i < endCount; i++) + { + endings[i] = msg.ReadByte(); + } + + if (string.IsNullOrEmpty(text) && optionCount == 0) + { + GUIMessageBox.MessageBoxes.ForEachMod(mb => + { + if (mb.UserData is Pair pair && pair.First == "ConversationAction" && pair.Second == identifier) + { + (mb as GUIMessageBox)?.Close(); + } + }); + } + else + { + ConversationAction.CreateDialog(text, Entity.FindEntityByID(speakerId) as Character, options, endings, eventSprite, identifier, fadeToBlack, (ConversationAction.DialogTypes)dialogType, continueConversation); + } + if (Entity.FindEntityByID(speakerId) is Character speaker) + { + speaker.CampaignInteractionType = CampaignMode.InteractionType.None; + speaker.SetCustomInteract(null, null); + } + break; + case NetworkEventType.MISSION: + string missionIdentifier = msg.ReadString(); + + MissionPrefab? prefab = MissionPrefab.List.Find(mp => mp.Identifier.Equals(missionIdentifier, StringComparison.OrdinalIgnoreCase)); + 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)) + { + IconColor = prefab.IconColor + }; + } + break; } } } -} +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs index 267103d4f..204452ec4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs @@ -6,6 +6,7 @@ namespace Barotrauma { public override void ClientReadInitial(IReadMessage msg) { + items.Clear(); ushort itemCount = msg.ReadUInt16(); for (int i = 0; i < itemCount; i++) { @@ -17,7 +18,7 @@ namespace Barotrauma } if (items.Count != itemCount) { - throw new System.Exception("Error in CargoMission.ClientReadInitial: item count does not match the server count (" + itemCount + " != " + items.Count + "mission: " + Prefab.Identifier + ")"); + throw new System.Exception("Error in CargoMission.ClientReadInitial: item count does not match the server count (" + itemCount + " != " + items.Count + ", mission: " + Prefab.Identifier + ")"); } if (requiredDeliveryAmount == 0) { requiredDeliveryAmount = items.Count; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs index b5f44164c..a9b5421c7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs @@ -1,4 +1,5 @@ using Barotrauma.Networking; +using System.Collections.Generic; namespace Barotrauma { @@ -13,10 +14,20 @@ namespace Barotrauma string header = messageIndex < Headers.Count ? Headers[messageIndex] : ""; string message = messageIndex < Messages.Count ? Messages[messageIndex] : ""; + CoroutineManager.StartCoroutine(ShowMessageBoxAfterRoundSummary(header, message)); + } + + private IEnumerable ShowMessageBoxAfterRoundSummary(string header, string message) + { + while (GUIMessageBox.VisibleBox?.UserData is RoundSummary) + { + yield return new WaitForSeconds(1.0f); + } new GUIMessageBox(header, message, buttons: new string[0], type: GUIMessageBox.Type.InGame, icon: Prefab.Icon) { IconColor = Prefab.IconColor }; + yield return CoroutineStatus.Success; } public void ClientRead(IReadMessage msg) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs index c16ab84b2..f6dad566f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs @@ -492,7 +492,7 @@ namespace Barotrauma } } - public Vector2 MeasureString(string text) + public Vector2 MeasureString(string text, bool removeExtraSpacing = false) { if (text == null) { @@ -501,7 +501,16 @@ namespace Barotrauma float currentLineX = 0.0f; Vector2 retVal = Vector2.Zero; - retVal.Y = baseHeight * 1.8f; + + if (!removeExtraSpacing) + { + retVal.Y = baseHeight * 1.8f; + } + else + { + retVal.Y = baseHeight; + } + for (int i = 0; i < text.Length; i++) { if (text[i] == '\n') diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs index 9069091ef..07e7ef7f8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs @@ -1,6 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; namespace Barotrauma @@ -144,6 +145,15 @@ namespace Barotrauma GetSize(element); } + public Sprite GetDefaultSprite() + { + return GetSprite(GUIComponent.ComponentState.None); + } + public Sprite GetSprite(GUIComponent.ComponentState state) + { + return Sprites.ContainsKey(state) ? Sprites[state]?.First()?.Sprite : null; + } + public void GetSize(XElement element) { Point size = new Point(0, 0); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs new file mode 100644 index 000000000..159c2218d --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs @@ -0,0 +1,729 @@ +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Barotrauma.Networking; + +namespace Barotrauma +{ + class CrewManagement + { + private CampaignMode campaign => campaignUI.Campaign; + private readonly CampaignUI campaignUI; + private readonly GUIComponent parentComponent; + + private GUIListBox hireableList, pendingList, crewList; + private GUIFrame characterPreviewFrame; + private GUIDropDown sortingDropDown; + private GUITextBlock totalBlock; + private GUIButton validateHiresButton; + private GUIButton clearAllButton; + + private List PendingHires => campaign.Map?.CurrentLocation?.HireManager?.PendingHires; + private bool HasPermission => campaignUI.Campaign.AllowedToManageCampaign(); + + private Point resolutionWhenCreated; + + private enum SortingMethod + { + AlphabeticalAsc, + JobAsc, + PriceAsc, + PriceDesc, + SkillAsc, + SkillDesc + } + + public CrewManagement(CampaignUI campaignUI, GUIComponent parentComponent) + { + this.campaignUI = campaignUI; + this.parentComponent = parentComponent; + + CreateUI(); + UpdateLocationView(campaignUI.Campaign.Map.CurrentLocation, true); + + campaignUI.Campaign.Map.OnLocationChanged += (prevLocation, newLocation) => UpdateLocationView(newLocation, true, prevLocation); + } + + public void RefreshPermissions() + { + RefreshCrewFrames(hireableList); + RefreshCrewFrames(crewList); + RefreshCrewFrames(pendingList); + if (clearAllButton != null) { clearAllButton.Enabled = HasPermission; } + } + + private void RefreshCrewFrames(GUIListBox listBox) + { + if (listBox == null) { return; } + listBox.CanBeFocused = HasPermission; + foreach (GUIComponent child in listBox.Content.Children) + { + if (child.FindChild(c => c is GUIButton && c.UserData is CharacterInfo, true) is GUIButton buyButton) + { + buyButton.Enabled = HasPermission; + } + } + } + + private void CreateUI() + { + if (parentComponent.FindChild(c => c.UserData as string == "glow") is GUIComponent glowChild) + { + parentComponent.RemoveChild(glowChild); + } + if (parentComponent.FindChild(c => c.UserData as string == "container") is GUIComponent containerChild) + { + parentComponent.RemoveChild(containerChild); + } + + new GUIFrame(new RectTransform(new Vector2(1.25f, 1.25f), parentComponent.RectTransform, Anchor.Center), + style: "OuterGlow", color: Color.Black * 0.7f) + { + UserData = "glow" + }; + new GUIFrame(new RectTransform(new Vector2(0.95f), parentComponent.RectTransform, anchor: Anchor.Center), + style: null) + { + CanBeFocused = false, + UserData = "container" + }; + + var availableMainGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).RectTransform) + { + MaxSize = new Point(560, campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Rect.Height) + }) + { + Stretch = true, + RelativeSpacing = 0.02f + }; + + // Header ------------------------------------------------ + var headerGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.75f / 14.0f), availableMainGroup.RectTransform), isHorizontal: true) + { + RelativeSpacing = 0.005f + }; + 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) + { + CanBeFocused = false, + ForceUpperCase = true + }; + + var hireablesGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), anchor: Anchor.Center, + parent: new GUIFrame(new RectTransform(new Vector2(1.0f, 13.25f / 14.0f), availableMainGroup.RectTransform)).RectTransform)) + { + RelativeSpacing = 0.015f, + Stretch = true + }; + + var sortGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.04f), hireablesGroup.RectTransform), isHorizontal: true) + { + RelativeSpacing = 0.015f + }; + new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), sortGroup.RectTransform), text: TextManager.Get("campaignstore.sortby")); + sortingDropDown = new GUIDropDown(new RectTransform(new Vector2(0.4f, 1.0f), sortGroup.RectTransform), elementCount: 5) + { + OnSelected = (child, userData) => + { + SortCharacters(hireableList, (SortingMethod)userData); + return true; + } + }; + var tag = "sortingmethod."; + sortingDropDown.AddItem(TextManager.Get(tag + SortingMethod.JobAsc), userData: SortingMethod.JobAsc); + sortingDropDown.AddItem(TextManager.Get(tag + SortingMethod.SkillAsc), userData: SortingMethod.SkillAsc); + sortingDropDown.AddItem(TextManager.Get(tag + SortingMethod.SkillDesc), userData: SortingMethod.SkillDesc); + sortingDropDown.AddItem(TextManager.Get(tag + SortingMethod.PriceAsc), userData: SortingMethod.PriceAsc); + sortingDropDown.AddItem(TextManager.Get(tag + SortingMethod.PriceDesc), userData: SortingMethod.PriceDesc); + + hireableList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.96f), + hireablesGroup.RectTransform, + anchor: Anchor.Center)) + { + Spacing = 1 + }; + + var pendingAndCrewMainGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).RectTransform, anchor: Anchor.TopRight) + { + MaxSize = new Point(560, campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Rect.Height) + }) + { + Stretch = true, + RelativeSpacing = 0.02f + }; + + var playerBalanceContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.75f / 14.0f), pendingAndCrewMainGroup.RectTransform), childAnchor: Anchor.TopRight) + { + 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) + { + AutoScaleVertical = true, + ForceUpperCase = true + }; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), playerBalanceContainer.RectTransform), + "", font: GUI.SubHeadingFont, textAlignment: Alignment.TopRight) + { + AutoScaleVertical = true, + TextScale = 1.1f, + TextGetter = () => FormatCurrency(campaign.Money) + }; + + var pendingAndCrewGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), anchor: Anchor.Center, + parent: new GUIFrame(new RectTransform(new Vector2(1.0f, 13.25f / 14.0f), pendingAndCrewMainGroup.RectTransform) + { + MaxSize = new Point(560, campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Rect.Height) + }).RectTransform)); + + float height = 0.05f; + new GUITextBlock(new RectTransform(new Vector2(1.0f, height), pendingAndCrewGroup.RectTransform), TextManager.Get("campaigncrew.pending"), font: GUI.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); + crewList = new GUIListBox(new RectTransform(new Vector2(1.0f, (8)* height), pendingAndCrewGroup.RectTransform)) + { + Spacing = 1 + }; + + 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) + { + TextScale = 1.1f + }; + group = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, height), pendingAndCrewGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.TopRight) + { + RelativeSpacing = 0.01f + }; + validateHiresButton = new GUIButton(new RectTransform(new Vector2(1.0f / 3.0f, 1.0f), group.RectTransform), text: TextManager.Get("campaigncrew.validate")) + { + ForceUpperCase = true, + OnClicked = (b, o) => ValidatePendingHires(true) + }; + clearAllButton = new GUIButton(new RectTransform(new Vector2(1.0f / 3.0f, 1.0f), group.RectTransform), text: TextManager.Get("campaignstore.clearall")) + { + ForceUpperCase = true, + Enabled = HasPermission, + OnClicked = (b, o) => RemoveAllPendingHires() + }; + + resolutionWhenCreated = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); + } + + private void UpdateLocationView(Location location, bool removePending, Location prevLocation = null) + { + if (prevLocation != null && prevLocation == location && GameMain.NetworkMember != null) { return; } + + if (characterPreviewFrame != null) + { + characterPreviewFrame.Parent?.RemoveChild(characterPreviewFrame); + characterPreviewFrame = null; + } + UpdateHireables(location); + if (pendingList != null) + { + if (removePending) + { + PendingHires?.Clear(); + pendingList.Content.ClearChildren(); + } + else + { + PendingHires?.ForEach(ci => AddPendingHire(ci)); + } + SetTotalHireCost(); + } + UpdateCrew(); + } + + private void UpdateHireables(Location location) + { + if (hireableList != null) + { + hireableList.Content.Children.ToList().ForEach(c => hireableList.RemoveChild(c)); + var hireableCharacters = location.GetHireableCharacters(); + if (hireableCharacters.None()) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), hireableList.Content.RectTransform), TextManager.Get("HireUnavailable"), textAlignment: Alignment.Center) + { + CanBeFocused = false + }; + } + else + { + foreach (CharacterInfo c in hireableCharacters) + { + if (c == null) { continue; } + CreateCharacterFrame(c, hireableList); + } + } + sortingDropDown.SelectItem(SortingMethod.JobAsc); + hireableList.UpdateScrollBarSize(); + } + } + + public void SetHireables(Location location, List availableHires) + { + HireManager hireManager = location.HireManager; + if (hireManager == null) { return; } + int hireVal = hireManager.AvailableCharacters.Aggregate(0, (curr, hire) => curr + hire.GetIdentifier()); + int newVal = availableHires.Aggregate(0, (curr, hire) => curr + hire.GetIdentifier()); + if (hireVal != newVal) + { + location.HireManager.AvailableCharacters = availableHires; + UpdateHireables(location); + } + } + + public void UpdateCrew() + { + crewList.Content.Children.ToList().ForEach(c => crewList.Content.RemoveChild(c)); + foreach (CharacterInfo c in GameMain.GameSession.CrewManager.GetCharacterInfos()) + { + if (c == null || !((c.Character?.IsBot ?? true) || campaign is SinglePlayerCampaign)) { continue; } + CreateCharacterFrame(c, crewList); + } + SortCharacters(crewList, SortingMethod.JobAsc); + crewList.UpdateScrollBarSize(); + } + + private void SortCharacters(GUIListBox list, SortingMethod sortingMethod) + { + 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)); + } + 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)); + } + 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)); + 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)); + if (sortingMethod == SortingMethod.SkillDesc) { list.Content.RectTransform.ReverseChildren(); } + } + } + + private void CreateCharacterFrame(CharacterInfo characterInfo, GUIListBox listBox) + { + Skill skill = null; + Color? jobColor = null; + if (characterInfo.Job != null) + { + skill = characterInfo.Job?.PrimarySkill ?? characterInfo.Job.Skills.OrderByDescending(s => s.Level).FirstOrDefault(); + jobColor = characterInfo.Job.Prefab.UIColor; + } + + GUIFrame frame = new GUIFrame(new RectTransform(new Point(listBox.Content.Rect.Width, 55), parent: listBox.Content.RectTransform), "ListBoxElement") + { + UserData = new Tuple(characterInfo, skill != null ? 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) + { + Stretch = true + }; + + float portraitWidth = (0.8f * mainGroup.Rect.Height) / mainGroup.Rect.Width; + new GUICustomComponent(new RectTransform(new Vector2(portraitWidth, 0.8f), mainGroup.RectTransform), + onDraw: (sb, component) => characterInfo.DrawIcon(sb, component.Rect.Center.ToVector2(), targetAreaSize: component.Rect.Size.ToVector2())) + { + CanBeFocused = false + }; + + GUILayoutGroup nameAndJobGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.4f - portraitWidth, 0.8f), mainGroup.RectTransform)); + GUITextBlock nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), nameAndJobGroup.RectTransform), + characterInfo.Name, textColor: jobColor, textAlignment: Alignment.BottomLeft) + { + CanBeFocused = false + }; + 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) + { + CanBeFocused = false + }; + jobBlock.Text = ToolBox.LimitString(jobBlock.Text, jobBlock.Font, jobBlock.Rect.Width); + + float width = 0.6f / 3; + if (characterInfo.Job != null) + { + 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) + { + CanBeFocused = false + }; + if (jobColor.HasValue) { skillIcon.Color = jobColor.Value; } + new GUITextBlock(new RectTransform(new Vector2(1.0f - iconWidth, 1.0f), skillGroup.RectTransform), ((int)skill.Level).ToString(), textAlignment: Alignment.CenterLeft) + { + CanBeFocused = false + }; + } + + if (listBox != crewList) + { + new GUITextBlock(new RectTransform(new Vector2(width, 1.0f), mainGroup.RectTransform), FormatCurrency(characterInfo.Salary), textAlignment: Alignment.Center) + { + CanBeFocused = false + }; + } + + if (listBox == hireableList) + { + new GUIButton(new RectTransform(new Vector2(width, 0.9f), mainGroup.RectTransform), style: "CrewManagementAddButton") + { + UserData = characterInfo, + Enabled = HasPermission, + OnClicked = (b, o) => AddPendingHire(o as CharacterInfo) + }; + } + else if (listBox == pendingList) + { + new GUIButton(new RectTransform(new Vector2(width, 0.9f), mainGroup.RectTransform), style: "CrewManagementRemoveButton") + { + UserData = characterInfo, + Enabled = HasPermission, + OnClicked = (b, o) => RemovePendingHire(o as CharacterInfo) + }; + } + else if (listBox == crewList && campaign != null) + { + var currentCrew = GameMain.GameSession.CrewManager.GetCharacterInfos(); + new GUIButton(new RectTransform(new Vector2(width, 0.9f), mainGroup.RectTransform), style: "CrewManagementFireButton") + { + UserData = characterInfo, + //can't fire if there's only one character in the crew + Enabled = currentCrew.Contains(characterInfo) && currentCrew.Count() > 1 && HasPermission, + OnClicked = (btn, obj) => + { + var confirmDialog = new GUIMessageBox( + TextManager.Get("FireWarningHeader"), + TextManager.GetWithVariable("FireWarningText", "[charactername]", ((CharacterInfo)obj).Name), + new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); + confirmDialog.Buttons[0].UserData = (CharacterInfo)obj; + confirmDialog.Buttons[0].OnClicked = FireCharacter; + confirmDialog.Buttons[0].OnClicked += confirmDialog.Close; + confirmDialog.Buttons[1].OnClicked = confirmDialog.Close; + return true; + } + }; + } + } + + private void CreateCharacterPreviewFrame(GUIListBox listBox, GUIFrame characterFrame, CharacterInfo characterInfo) + { + Pivot pivot = listBox == hireableList ? Pivot.TopLeft : Pivot.TopRight; + Point absoluteOffset = new Point( + pivot == Pivot.TopLeft ? listBox.Parent.Parent.Rect.Right + 5 : listBox.Parent.Parent.Rect.Left - 5, + characterFrame.Rect.Top); + int frameSize = (int)(GUI.Scale * 300); + if (GameMain.GraphicsHeight - (absoluteOffset.Y + frameSize) < 0) + { + pivot = listBox == hireableList ? Pivot.BottomLeft : Pivot.BottomRight; + absoluteOffset.Y = characterFrame.Rect.Bottom; + } + characterPreviewFrame = new GUIFrame(new RectTransform(new Point(frameSize), parent: campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Parent.RectTransform, pivot: pivot) + { + AbsoluteOffset = absoluteOffset + }, style: "InnerFrame") + { + UserData = characterInfo + }; + GUILayoutGroup mainGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.95f), characterPreviewFrame.RectTransform, anchor: Anchor.Center)) + { + RelativeSpacing = 0.01f + }; + + // Character info + GUILayoutGroup infoGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.475f), mainGroup.RectTransform), isHorizontal: true); + GUILayoutGroup infoLabelGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), infoGroup.RectTransform)) { Stretch = true }; + GUILayoutGroup infoValueGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.6f, 1.0f), infoGroup.RectTransform)) { Stretch = true }; + float blockHeight = 1.0f / 4; + new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoLabelGroup.RectTransform), TextManager.Get("name")); + new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoValueGroup.RectTransform), characterInfo.Name); + if (characterInfo.HasGenders) + { + 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())); + } + if (characterInfo.Job is Job job) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoLabelGroup.RectTransform), TextManager.Get("tabmenu.job")); + new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoValueGroup.RectTransform), job.Name); + } + 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(" ", ""))); + } + infoLabelGroup.Recalculate(); + infoValueGroup.Recalculate(); + + new GUIImage(new RectTransform(new Vector2(1.0f, 0.05f), mainGroup.RectTransform), "HorizontalLine"); + + // Skills + GUILayoutGroup skillGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.475f), mainGroup.RectTransform), isHorizontal: true); + GUILayoutGroup skillNameGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 1.0f), skillGroup.RectTransform)); + GUILayoutGroup skillLevelGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.2f, 1.0f), skillGroup.RectTransform)); + List characterSkills = characterInfo.Job.Skills; + blockHeight = 1.0f / characterSkills.Count; + foreach (Skill skill in characterSkills) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), skillNameGroup.RectTransform), TextManager.Get("SkillName." + skill.Identifier)); + new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), skillLevelGroup.RectTransform), ((int)skill.Level).ToString(), textAlignment: Alignment.Right); + } + } + + private bool SelectCharacter(GUIListBox listBox, GUIFrame characterFrame, CharacterInfo characterInfo) + { + if (characterPreviewFrame != null && characterPreviewFrame.UserData != characterInfo) + { + characterPreviewFrame.Parent?.RemoveChild(characterPreviewFrame); + characterPreviewFrame = null; + } + + if (listBox == null || characterFrame == null || characterInfo == null) { return false; } + + if (characterPreviewFrame == null) + { + CreateCharacterPreviewFrame(listBox, characterFrame, characterInfo); + } + + return true; + } + + private bool AddPendingHire(CharacterInfo characterInfo, bool createNetworkMessage = true) + { + hireableList.Content.RemoveChild(hireableList.Content.FindChild(c => (c.UserData as Tuple).Item1 == characterInfo)); + hireableList.UpdateScrollBarSize(); + if (!PendingHires.Contains(characterInfo)) { PendingHires.Add(characterInfo); } + CreateCharacterFrame(characterInfo, pendingList); + SortCharacters(pendingList, SortingMethod.JobAsc); + pendingList.UpdateScrollBarSize(); + SetTotalHireCost(); + if (createNetworkMessage) { SendCrewState(true); } + return true; + } + + 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.UpdateScrollBarSize(); + CreateCharacterFrame(characterInfo, hireableList); + SortCharacters(hireableList, (SortingMethod)sortingDropDown.SelectedItemData); + hireableList.UpdateScrollBarSize(); + if (setTotalHireCost) { SetTotalHireCost(); } + if (createNetworkMessage) { SendCrewState(true); } + return true; + } + + private bool RemoveAllPendingHires(bool createNetworkMessage = true) + { + pendingList.Content.Children.ToList().ForEach(c => RemovePendingHire((c.UserData as Tuple).Item1, setTotalHireCost: false, createNetworkMessage)); + SetTotalHireCost(); + return true; + } + + private void SetTotalHireCost() + { + if (pendingList == null || totalBlock == null || validateHiresButton == null) { return; } + int total = 0; + pendingList.Content.Children.ForEach(c => total += (c.UserData as Tuple).Item1.Salary); + totalBlock.Text = FormatCurrency(total); + bool enoughMoney = campaign != null ? total <= campaign.Money : true; + totalBlock.TextColor = enoughMoney ? Color.White : Color.Red; + validateHiresButton.Enabled = enoughMoney && pendingList.Content.RectTransform.Children.Any(); + } + + public bool ValidatePendingHires(bool createNetworkEvent = false) + { + List hires = new List(); + int total = 0; + foreach (GUIComponent c in pendingList.Content.Children.ToList()) + { + if (c.UserData is Tuple info) + { + hires.Add(info.Item1); + total += info.Item1.Salary; + } + } + + if (hires.None() || total > campaign.Money) { return false; } + + bool atLeastOneHired = false; + foreach (CharacterInfo ci in hires) + { + if (campaign.TryHireCharacter(campaign.Map.CurrentLocation, ci)) + { + atLeastOneHired = true; + PendingHires.Remove(ci); + pendingList.Content.RemoveChild(pendingList.Content.FindChild(c => (c.UserData as Tuple).Item1 == ci)); + } + else + { + break; + } + } + + if (atLeastOneHired) + { + UpdateLocationView(campaign.Map.CurrentLocation, true); + SelectCharacter(null, null, null); + var dialog = new GUIMessageBox( + TextManager.Get("newcrewmembers"), + TextManager.GetWithVariable("crewhiredmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.Name), + new string[] { TextManager.Get("Ok") }); + dialog.Buttons[0].OnClicked += dialog.Close; + } + + if (createNetworkEvent) + { + SendCrewState(true, validateHires: true); + } + + return false; + } + + private bool FireCharacter(GUIButton button, object selection) + { + if (!(selection is CharacterInfo characterInfo)) { return false; } + + campaign.CrewManager.FireCharacter(characterInfo); + SelectCharacter(null, null, null); + UpdateCrew(); + + SendCrewState(false, firedCharacter: characterInfo); + return false; + } + + public void Update() + { + if (GameMain.GraphicsWidth != resolutionWhenCreated.X || GameMain.GraphicsHeight != resolutionWhenCreated.Y) + { + CreateUI(); + UpdateLocationView(campaign.Map.CurrentLocation, false); + } + + if ((GUI.MouseOn?.UserData as Tuple)?.Item1 is CharacterInfo characterInfo) + { + if (characterPreviewFrame == null || characterInfo != characterPreviewFrame.UserData) + { + GUIComponent component = GUI.MouseOn; + GUIListBox listBox = null; + do + { + if (component.Parent is GUIListBox) + { + listBox = component.Parent as GUIListBox; + break; + } + else if (component.Parent != null) + { + component = component.Parent; + } + else + { + break; + } + } while (listBox == null); + + if (listBox != null) + { + SelectCharacter(listBox, GUI.MouseOn as GUIFrame, characterInfo); + } + } + else + { + // TODO: Reposition the current preview panel if necessary + // Could happen if we scroll a list while hovering? + } + } + else if (characterPreviewFrame != null) + { + characterPreviewFrame.Parent?.RemoveChild(characterPreviewFrame); + characterPreviewFrame = null; + } + } + + public void SetPendingHires(List characterInfos, Location location) + { + List oldHires = PendingHires.ToList(); + foreach (CharacterInfo pendingHire in oldHires) + { + RemovePendingHire(pendingHire, createNetworkMessage: false); + } + PendingHires.Clear(); + foreach (int identifier in characterInfos) + { + CharacterInfo match = location.HireManager.AvailableCharacters.Find(info => info.GetIdentifier() == identifier); + if (match != null) + { + PendingHires.Add(match); + AddPendingHire(match, createNetworkMessage: false); + } + else + { + DebugConsole.ThrowError("Received a hire that doesn't exist."); + } + } + } + + /// + /// Notify the server of crew changes + /// + /// When set to true will tell the server to update the pending hires + /// When not null tell the server to fire this character + /// When set to true will tell the server to validate pending hires + public void SendCrewState(bool updatePending, CharacterInfo firedCharacter = null, bool validateHires = false) + { + if (campaign is MultiPlayerCampaign) + { + IWriteMessage msg = new WriteOnlyMessage(); + msg.Write((byte)ClientPacketHeader.CREW); + + msg.Write(updatePending); + if (updatePending) + { + msg.Write((ushort)PendingHires.Count); + foreach (CharacterInfo pendingHire in PendingHires) + { + msg.Write(pendingHire.GetIdentifier()); + } + } + + msg.Write(validateHires); + + msg.Write(firedCharacter != null); + if (firedCharacter != null) + { + msg.Write(firedCharacter.GetIdentifier()); + } + + GameMain.Client.ClientPeer?.Send(msg, DeliveryMethod.Reliable); + } + } + + private string FormatCurrency(int currency) => TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", currency)); + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index f2d904791..a1201fd35 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -79,6 +79,8 @@ namespace Barotrauma 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; @@ -126,7 +128,9 @@ namespace Barotrauma private static Texture2D t; private static Sprite[] MouseCursorSprites => Style.CursorSprite; - private static bool debugDrawSounds, debugDrawEvents; + 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; } @@ -287,23 +291,26 @@ namespace Barotrauma /// public static void Draw(Camera cam, SpriteBatch spriteBatch) { - if (ScreenChanged) + lock (mutex) { - updateList.Clear(); - updateListSet.Clear(); - Screen.Selected?.AddToGUIUpdateList(); - ScreenChanged = false; - } - updateList.ForEach(c => c.DrawAuto(spriteBatch)); + if (ScreenChanged) + { + updateList.Clear(); + updateListSet.Clear(); + Screen.Selected?.AddToGUIUpdateList(); + ScreenChanged = false; + } - if (ScreenOverlayColor.A > 0.0f) - { - DrawRectangle( - spriteBatch, - new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), - ScreenOverlayColor, true); - } + updateList.ForEach(c => c.DrawAuto(spriteBatch)); + + if (ScreenOverlayColor.A > 0.0f) + { + DrawRectangle( + spriteBatch, + new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), + ScreenOverlayColor, true); + } #if UNSTABLE string line1 = "Barotrauma Unstable v" + GameMain.Version; @@ -343,293 +350,319 @@ namespace Barotrauma } #endif - if (DisableHUD) { return; } + if (DisableHUD) { return; } - if (GameMain.ShowFPS || GameMain.DebugDraw) - { - DrawString(spriteBatch, new Vector2(10, 10), - "FPS: " + Math.Round(GameMain.PerformanceCounter.AverageFramesPerSecond), - Color.White, Color.Black * 0.5f, 0, SmallFont); - } - - if (GameMain.ShowPerf) - { - int y = 10; - 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", - GUI.Style.Green, Color.Black * 0.8f, font: SmallFont); - y += 15; - GameMain.PerformanceCounter.DrawTimeGraph.Draw(spriteBatch, new Rectangle(300, y, 170, 50), null, 0, GUI.Style.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); - y += 15; - GameMain.PerformanceCounter.UpdateTimeGraph.Draw(spriteBatch, new Rectangle(300, y, 170, 50), null, 0, Color.LightBlue); - GameMain.PerformanceCounter.UpdateIterationsGraph.Draw(spriteBatch, new Rectangle(300, y, 170, 50), 20, 0, GUI.Style.Red); - y += 50; - foreach (string key in GameMain.PerformanceCounter.GetSavedIdentifiers) + if (GameMain.ShowFPS || GameMain.DebugDraw) { - float elapsedMillisecs = GameMain.PerformanceCounter.GetAverageElapsedMillisecs(key); + DrawString(spriteBatch, new Vector2(10, 10), + "FPS: " + Math.Round(GameMain.PerformanceCounter.AverageFramesPerSecond), + Color.White, Color.Black * 0.5f, 0, SmallFont); + } + + if (GameMain.ShowPerf) + { + int y = 10; 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); - + "Draw - Avg: " + GameMain.PerformanceCounter.DrawTimeGraph.Average().ToString("0.00") + " ms" + + " Max: " + GameMain.PerformanceCounter.DrawTimeGraph.LargestValue().ToString("0.00") + " ms", + GUI.Style.Green, Color.Black * 0.8f, font: SmallFont); y += 15; - } + GameMain.PerformanceCounter.DrawTimeGraph.Draw(spriteBatch, new Rectangle(300, y, 170, 50), null, 0, GUI.Style.Green); + y += 50; - 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); - } - } - - if (GameMain.DebugDraw) - { - DrawString(spriteBatch, new Vector2(10, 25), - "Physics: " + GameMain.World.UpdateTime, - Color.White, Color.Black * 0.5f, 0, SmallFont); - - DrawString(spriteBatch, new Vector2(10, 40), - $"Bodies: {GameMain.World.BodyList.Count} ({GameMain.World.BodyList.FindAll(b => b.Awake && b.Enabled).Count} awake, {GameMain.World.BodyList.FindAll(b => b.Awake && b.BodyType == BodyType.Dynamic && b.Enabled).Count} dynamic)", - Color.White, Color.Black * 0.5f, 0, 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); - } - - if (Submarine.MainSub != null) - { - DrawString(spriteBatch, new Vector2(10, 70), - "Sub pos: " + Submarine.MainSub.Position.ToPoint(), - Color.White, Color.Black * 0.5f, 0, 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); - - DrawString(spriteBatch, new Vector2(10, 115), - "Loaded sprites: " + Sprite.LoadedSprites.Count() + "\n(" + Sprite.LoadedSprites.Select(s => s.FilePath).Distinct().Count() + " unique textures)", - Color.White, Color.Black * 0.5f, 0, 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); + 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); y += 15; - - DrawString(spriteBatch, new Vector2(500, y), - "Current playback amplitude: " + GameMain.SoundManager.PlaybackAmplitude.ToString(), Color.White, Color.Black * 0.5f, 0, 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); - - 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); - y += 15; - - for (int i = 0; i < SoundManager.SOURCE_COUNT; i++) + GameMain.PerformanceCounter.UpdateTimeGraph.Draw(spriteBatch, new Rectangle(300, y, 170, 50), null, 0, Color.LightBlue); + GameMain.PerformanceCounter.UpdateIterationsGraph.Draw(spriteBatch, new Rectangle(300, y, 170, 50), 20, 0, GUI.Style.Red); + y += 50; + foreach (string key in GameMain.PerformanceCounter.GetSavedIdentifiers) { - Color clr = Color.White; - string soundStr = i + ": "; - SoundChannel playingSoundChannel = GameMain.SoundManager.GetSoundChannelFromIndex(SoundManager.SourcePoolIndex.Default, i); - if (playingSoundChannel == null) + 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); + + 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); + } + } + + if (GameMain.DebugDraw) + { + DrawString(spriteBatch, new Vector2(10, 25), + "Physics: " + GameMain.World.UpdateTime, + Color.White, Color.Black * 0.5f, 0, SmallFont); + + DrawString(spriteBatch, new Vector2(10, 40), + $"Bodies: {GameMain.World.BodyList.Count} ({GameMain.World.BodyList.FindAll(b => b.Awake && b.Enabled).Count} awake, {GameMain.World.BodyList.FindAll(b => b.Awake && b.BodyType == BodyType.Dynamic && b.Enabled).Count} dynamic)", + Color.White, Color.Black * 0.5f, 0, 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); + } + + if (Submarine.MainSub != null) + { + DrawString(spriteBatch, new Vector2(10, 70), + "Sub pos: " + Submarine.MainSub.Position.ToPoint(), + Color.White, Color.Black * 0.5f, 0, 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); + + DrawString(spriteBatch, new Vector2(10, 115), + "Loaded sprites: " + Sprite.LoadedSprites.Count() + "\n(" + Sprite.LoadedSprites.Select(s => s.FilePath).Distinct().Count() + " unique textures)", + Color.White, Color.Black * 0.5f, 0, 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); + y += 15; + + DrawString(spriteBatch, new Vector2(500, y), + "Current playback amplitude: " + GameMain.SoundManager.PlaybackAmplitude.ToString(), Color.White, Color.Black * 0.5f, 0, 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); + + 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); + y += 15; + + for (int i = 0; i < SoundManager.SOURCE_COUNT; i++) { - soundStr += "none"; - clr *= 0.5f; + Color clr = Color.White; + string soundStr = i + ": "; + SoundChannel playingSoundChannel = GameMain.SoundManager.GetSoundChannelFromIndex(SoundManager.SourcePoolIndex.Default, i); + if (playingSoundChannel == null) + { + soundStr += "none"; + clr *= 0.5f; + } + else + { + soundStr += Path.GetFileNameWithoutExtension(playingSoundChannel.Sound.Filename); + +#if DEBUG + if (PlayerInput.GetKeyboardState.IsKeyDown(Keys.G)) + { + if (PlayerInput.MousePosition.Y >= y && PlayerInput.MousePosition.Y <= y + 12) + { + GameMain.SoundManager.DebugSource(i); + } + } +#endif + + if (playingSoundChannel.Looping) + { + soundStr += " (looping)"; + clr = Color.Yellow; + } + if (playingSoundChannel.IsStream) + { + soundStr += " (streaming)"; + clr = Color.Lime; + } + if (!playingSoundChannel.IsPlaying) + { + soundStr += " (stopped)"; + clr *= 0.5f; + } + else if (playingSoundChannel.Muffled) + { + soundStr += " (muffled)"; + clr = Color.Lerp(clr, Color.LightGray, 0.5f); + } + } + + DrawString(spriteBatch, new Vector2(500, y), soundStr, clr, Color.Black * 0.5f, 0, 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); + } + + + if (debugDrawEvents) + { + DrawString(spriteBatch, new Vector2(10, 300), + "Ctrl+E to hide EventManager debug info", Color.White, Color.Black * 0.5f, 0, 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); + } + + if (GameMain.GameSession?.GameMode is CampaignMode campaignMode) + { + if (debugDrawMetadata) + { + string text = "Ctrl+M to hide campaign metadata debug info\n\n" + + $"Ctrl+1 to {(string.IsNullOrWhiteSpace(ignoredMetadataInfo[0]) ? "hide" : "show")} outpost reputations, \n" + + $"Ctrl+2 to {(string.IsNullOrWhiteSpace(ignoredMetadataInfo[1]) ? "hide" : "show")} faction reputations, \n" + + $"Ctrl+3 to {(string.IsNullOrWhiteSpace(ignoredMetadataInfo[2]) ? "hide" : "show")} upgrade levels, \n" + + $"Ctrl+4 to {(string.IsNullOrWhiteSpace(ignoredMetadataInfo[3]) ? "hide" : "show")} upgrade prices"; + var (x, y) = SmallFont.MeasureString(text); + Vector2 pos = new Vector2(GameMain.GraphicsWidth - (x + 10), 300); + DrawString(spriteBatch, pos, text, Color.White, Color.Black * 0.5f, 0, SmallFont); + pos.Y += y + 8; + campaignMode.CampaignMetadata?.DebugDraw(spriteBatch, pos, debugDrawMetadataOffset, ignoredMetadataInfo); } else { - soundStr += Path.GetFileNameWithoutExtension(playingSoundChannel.Sound.Filename); + 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); + } + } -#if DEBUG - if (PlayerInput.GetKeyboardState.IsKeyDown(Keys.G)) + if (MouseOn != null) + { + RectTransform mouseOnRect = MouseOn.RectTransform; + bool isAbsoluteOffsetInUse = mouseOnRect.AbsoluteOffset != Point.Zero || mouseOnRect.RelativeOffset == Vector2.Zero; + + string selectedString = $"Selected UI Element: {MouseOn.GetType().Name} ({ MouseOn.Style?.Element.Name.LocalName ?? "no style" }, {MouseOn.Rect}"; + string offsetString = $"Relative Offset: {mouseOnRect.RelativeOffset} | Absolute Offset: {(isAbsoluteOffsetInUse ? mouseOnRect.AbsoluteOffset : mouseOnRect.ParentRect.MultiplySize(mouseOnRect.RelativeOffset))}{(isAbsoluteOffsetInUse ? "" : " (Calculated from RelativeOffset)")}"; + string anchorPivotString = $"Anchor: {mouseOnRect.Anchor} | Pivot: {mouseOnRect.Pivot}"; + Vector2 selectedStringSize = SmallFont.MeasureString(selectedString); + Vector2 offsetStringSize = SmallFont.MeasureString(offsetString); + Vector2 anchorPivotStringSize = SmallFont.MeasureString(anchorPivotString); + + int padding = IntScale(10); + int yPos = padding; + + DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)selectedStringSize.X - padding, yPos), selectedString, Color.LightGreen, Color.Black, 0, SmallFont); + yPos += (int)selectedStringSize.Y + padding / 2; + + DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)offsetStringSize.X - padding, yPos), offsetString, Color.LightGreen, Color.Black, 0, SmallFont); + yPos += (int)offsetStringSize.Y + padding / 2; + + DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)anchorPivotStringSize.X - padding, yPos), anchorPivotString, Color.LightGreen, Color.Black, 0, SmallFont); + yPos += (int)anchorPivotStringSize.Y + padding / 2; + } + else + { + string guiScaleString = $"GUI.Scale: {Scale}"; + string guixScaleString = $"GUI.xScale: {xScale}"; + string guiyScaleString = $"GUI.yScale: {yScale}"; + string relativeHorizontalAspectRatioString = $"RelativeHorizontalAspectRatio: {RelativeHorizontalAspectRatio}"; + string relativeVerticalAspectRatioString = $"RelativeVerticalAspectRatio: {RelativeVerticalAspectRatio}"; + Vector2 guiScaleStringSize = SmallFont.MeasureString(guiScaleString); + Vector2 guixScaleStringSize = SmallFont.MeasureString(guixScaleString); + Vector2 guiyScaleStringSize = SmallFont.MeasureString(guiyScaleString); + Vector2 relativeHorizontalAspectRatioStringSize = SmallFont.MeasureString(relativeHorizontalAspectRatioString); + Vector2 relativeVerticalAspectRatioStringSize = SmallFont.MeasureString(relativeVerticalAspectRatioString); + + int padding = IntScale(10); + int yPos = padding; + + DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)guiScaleStringSize.X - padding, yPos), guiScaleString, Color.LightGreen, Color.Black, 0, SmallFont); + yPos += (int)guiScaleStringSize.Y + padding / 2; + + DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)guixScaleStringSize.X - padding, yPos), guixScaleString, Color.LightGreen, Color.Black, 0, SmallFont); + yPos += (int)guixScaleStringSize.Y + padding / 2; + + DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)guiyScaleStringSize.X - padding, yPos), guiyScaleString, Color.LightGreen, Color.Black, 0, SmallFont); + yPos += (int)guiyScaleStringSize.Y + padding / 2; + + DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)relativeHorizontalAspectRatioStringSize.X - padding, yPos), relativeHorizontalAspectRatioString, Color.LightGreen, Color.Black, 0, SmallFont); + yPos += (int)relativeHorizontalAspectRatioStringSize.Y + padding / 2; + + DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)relativeVerticalAspectRatioStringSize.X - padding, yPos), relativeVerticalAspectRatioString, Color.LightGreen, Color.Black, 0, SmallFont); + yPos += (int)relativeVerticalAspectRatioStringSize.Y + padding / 2; + } + } + + GameMain.GameSession?.EventManager?.DrawPinnedEvent(spriteBatch); + + if (HUDLayoutSettings.DebugDraw) HUDLayoutSettings.Draw(spriteBatch); + + if (GameMain.Client != null) GameMain.Client.Draw(spriteBatch); + + if (Character.Controlled?.Inventory != null) + { + if (!Character.Controlled.LockHands && Character.Controlled.Stun < 0.1f && !Character.Controlled.IsDead) + { + Inventory.DrawFront(spriteBatch); + } + } + + DrawMessages(spriteBatch, cam); + + if (MouseOn != null && !string.IsNullOrWhiteSpace(MouseOn.ToolTip)) + { + MouseOn.DrawToolTip(spriteBatch); + } + + if (SubEditorScreen.IsSubEditor()) + { + // Draw our "infinite stack" on the cursor + switch (SubEditorScreen.DraggedItemPrefab) + { + case ItemPrefab itemPrefab: { - if (PlayerInput.MousePosition.Y >= y && PlayerInput.MousePosition.Y <= y + 12) + var sprite = itemPrefab.InventoryIcon ?? itemPrefab.sprite; + sprite?.Draw(spriteBatch, PlayerInput.MousePosition, scale: Math.Min(64 / sprite.size.X, 64 / sprite.size.Y) * Scale); + break; + } + case ItemAssemblyPrefab iPrefab: + { + var (x, y) = PlayerInput.MousePosition; + foreach (var pair in iPrefab.DisplayEntities) { - GameMain.SoundManager.DebugSource(i); + Rectangle dRect = pair.Second; + 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); } + break; } -#endif - - if (playingSoundChannel.Looping) - { - soundStr += " (looping)"; - clr = Color.Yellow; - } - if (playingSoundChannel.IsStream) - { - soundStr += " (streaming)"; - clr = Color.Lime; - } - if (!playingSoundChannel.IsPlaying) - { - soundStr += " (stopped)"; - clr *= 0.5f; - } - else if (playingSoundChannel.Muffled) - { - soundStr += " (muffled)"; - clr = Color.Lerp(clr, Color.LightGray, 0.5f); - } - } - - DrawString(spriteBatch, new Vector2(500, y), soundStr, clr, Color.Black * 0.5f, 0, SmallFont); - y += 15; } } - else + + if (GameMain.WindowActive && !HideCursor) { - DrawString(spriteBatch, new Vector2(500, 0), - "Ctrl+S to show sound debug info", Color.White, Color.Black * 0.5f, 0, SmallFont); - } - - - if (debugDrawEvents) - { - DrawString(spriteBatch, new Vector2(10, 300), - "Ctrl+E to hide EventManager debug info", Color.White, Color.Black * 0.5f, 0, 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); - } - - if (MouseOn != null) - { - RectTransform mouseOnRect = MouseOn.RectTransform; - bool isAbsoluteOffsetInUse = mouseOnRect.AbsoluteOffset != Point.Zero || mouseOnRect.RelativeOffset == Vector2.Zero; - - string selectedString = $"Selected UI Element: {MouseOn.GetType().Name} ({ MouseOn.Style?.Element.Name.LocalName ?? "no style" }, {MouseOn.Rect}"; - string offsetString = $"Relative Offset: {mouseOnRect.RelativeOffset} | Absolute Offset: {(isAbsoluteOffsetInUse ? mouseOnRect.AbsoluteOffset : mouseOnRect.ParentRect.MultiplySize(mouseOnRect.RelativeOffset))}{(isAbsoluteOffsetInUse ? "" : " (Calculated from RelativeOffset)")}"; - string anchorPivotString = $"Anchor: {mouseOnRect.Anchor} | Pivot: {mouseOnRect.Pivot}"; - Vector2 selectedStringSize = SmallFont.MeasureString(selectedString); - Vector2 offsetStringSize = SmallFont.MeasureString(offsetString); - Vector2 anchorPivotStringSize = SmallFont.MeasureString(anchorPivotString); - - int padding = IntScale(10); - int yPos = padding; - - DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)selectedStringSize.X - padding, yPos), selectedString, Color.LightGreen, Color.Black, 0, SmallFont); - yPos += (int)selectedStringSize.Y + padding / 2; - - DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)offsetStringSize.X - padding, yPos), offsetString, Color.LightGreen, Color.Black, 0, SmallFont); - yPos += (int)offsetStringSize.Y + padding / 2; - - DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)anchorPivotStringSize.X - padding, yPos), anchorPivotString, Color.LightGreen, Color.Black, 0, SmallFont); - yPos += (int)anchorPivotStringSize.Y + padding / 2; - } - else - { - string guiScaleString = $"GUI.Scale: {Scale}"; - string guixScaleString = $"GUI.xScale: {xScale}"; - string guiyScaleString = $"GUI.yScale: {yScale}"; - string relativeHorizontalAspectRatioString = $"RelativeHorizontalAspectRatio: {RelativeHorizontalAspectRatio}"; - string relativeVerticalAspectRatioString = $"RelativeVerticalAspectRatio: {RelativeVerticalAspectRatio}"; - Vector2 guiScaleStringSize = SmallFont.MeasureString(guiScaleString); - Vector2 guixScaleStringSize = SmallFont.MeasureString(guixScaleString); - Vector2 guiyScaleStringSize = SmallFont.MeasureString(guiyScaleString); - Vector2 relativeHorizontalAspectRatioStringSize = SmallFont.MeasureString(relativeHorizontalAspectRatioString); - Vector2 relativeVerticalAspectRatioStringSize = SmallFont.MeasureString(relativeVerticalAspectRatioString); - - int padding = IntScale(10); - int yPos = padding; - - DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)guiScaleStringSize.X - padding, yPos), guiScaleString, Color.LightGreen, Color.Black, 0, SmallFont); - yPos += (int)guiScaleStringSize.Y + padding / 2; - - DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)guixScaleStringSize.X - padding, yPos), guixScaleString, Color.LightGreen, Color.Black, 0, SmallFont); - yPos += (int)guixScaleStringSize.Y + padding / 2; - - DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)guiyScaleStringSize.X - padding, yPos), guiyScaleString, Color.LightGreen, Color.Black, 0, SmallFont); - yPos += (int)guiyScaleStringSize.Y + padding / 2; - - DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)relativeHorizontalAspectRatioStringSize.X - padding, yPos), relativeHorizontalAspectRatioString, Color.LightGreen, Color.Black, 0, SmallFont); - yPos += (int)relativeHorizontalAspectRatioStringSize.Y + padding / 2; - - DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)relativeVerticalAspectRatioStringSize.X - padding, yPos), relativeVerticalAspectRatioString, Color.LightGreen, Color.Black, 0, SmallFont); - yPos += (int)relativeVerticalAspectRatioStringSize.Y + padding / 2; + spriteBatch.End(); + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: SamplerStateClamp, rasterizerState: GameMain.ScissorTestEnable); + + var sprite = MouseCursorSprites[(int)MouseCursor] ?? MouseCursorSprites[(int)CursorState.Default]; + sprite.Draw(spriteBatch, PlayerInput.LatestMousePosition, Color.White, sprite.Origin, 0f, Scale / 1.5f); + + spriteBatch.End(); + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: SamplerState, rasterizerState: GameMain.ScissorTestEnable); } + HideCursor = false; } - - if (HUDLayoutSettings.DebugDraw) HUDLayoutSettings.Draw(spriteBatch); - - if (GameMain.Client != null) GameMain.Client.Draw(spriteBatch); - - if (Character.Controlled?.Inventory != null) - { - if (!Character.Controlled.LockHands && Character.Controlled.Stun < 0.1f && !Character.Controlled.IsDead) - { - Inventory.DrawFront(spriteBatch); - } - } - - DrawMessages(spriteBatch, cam); - - if (MouseOn != null && !string.IsNullOrWhiteSpace(MouseOn.ToolTip)) - { - MouseOn.DrawToolTip(spriteBatch); - } - - if (SubEditorScreen.IsSubEditor()) - { - // Draw our "infinite stack" on the cursor - switch (SubEditorScreen.DraggedItemPrefab) - { - case ItemPrefab itemPrefab: - { - var sprite = itemPrefab.InventoryIcon ?? itemPrefab.sprite; - sprite?.Draw(spriteBatch, PlayerInput.MousePosition, scale: Math.Min(64 / sprite.size.X, 64 / sprite.size.Y) * Scale); - break; - } - case ItemAssemblyPrefab iPrefab: - { - var (x, y) = PlayerInput.MousePosition; - foreach (var pair in iPrefab.DisplayEntities) - { - Rectangle dRect = pair.Second; - 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); - } - break; - } - } - } - - if (GameMain.WindowActive && !HideCursor) - { - spriteBatch.End(); - spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: SamplerStateClamp, rasterizerState: GameMain.ScissorTestEnable); - - var sprite = MouseCursorSprites[(int) MouseCursor] ?? MouseCursorSprites[(int)CursorState.Default]; - sprite.Draw(spriteBatch, PlayerInput.LatestMousePosition, Color.White, sprite.Origin, 0f, Scale / 1.5f); - - spriteBatch.End(); - spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: SamplerState, rasterizerState: GameMain.ScissorTestEnable); - } - HideCursor = false; } public static void DrawBackgroundSprite(SpriteBatch spriteBatch, Sprite backgroundSprite, float aberrationStrength = 1.0f) @@ -682,24 +715,27 @@ namespace Barotrauma /// public static void AddToUpdateList(GUIComponent component) { - if (component == null) + lock (mutex) { - DebugConsole.ThrowError("Trying to add a null component on the GUI update list!"); - return; - } - if (!component.Visible) { return; } - if (component.UpdateOrder < 0) - { - first.Add(component); - } - else if (component.UpdateOrder > 0) - { - last.Add(component); - } - else - { - additions.Enqueue(component); - } + if (component == null) + { + DebugConsole.ThrowError("Trying to add a null component on the GUI update list!"); + return; + } + if (!component.Visible) { return; } + if (component.UpdateOrder < 0) + { + first.Add(component); + } + else if (component.UpdateOrder > 0) + { + last.Add(component); + } + else + { + additions.Enqueue(component); + } + } } /// @@ -708,117 +744,138 @@ namespace Barotrauma /// public static void RemoveFromUpdateList(GUIComponent component, bool alsoChildren = true) { - if (updateListSet.Contains(component)) + lock (mutex) { - removals.Enqueue(component); - } - if (alsoChildren) - { - if (component.RectTransform != null) + if (updateListSet.Contains(component)) { - component.RectTransform.Children.ForEach(c => RemoveFromUpdateList(c.GUIComponent)); + removals.Enqueue(component); } - else + if (alsoChildren) { - component.Children.ForEach(c => RemoveFromUpdateList(c)); + if (component.RectTransform != null) + { + component.RectTransform.Children.ForEach(c => RemoveFromUpdateList(c.GUIComponent)); + } + else + { + component.Children.ForEach(c => RemoveFromUpdateList(c)); + } } - } + } } public static void ClearUpdateList() { - if (KeyboardDispatcher.Subscriber is GUIComponent && !updateList.Contains(KeyboardDispatcher.Subscriber as GUIComponent)) + lock (mutex) { - KeyboardDispatcher.Subscriber = null; + if (KeyboardDispatcher.Subscriber is GUIComponent && !updateList.Contains(KeyboardDispatcher.Subscriber as GUIComponent)) + { + KeyboardDispatcher.Subscriber = null; + } + updateList.Clear(); + updateListSet.Clear(); } - updateList.Clear(); - updateListSet.Clear(); } private static void RefreshUpdateList() { - foreach (var component in updateList) + lock (mutex) { - if (!component.Visible) + foreach (var component in updateList) { - RemoveFromUpdateList(component); + if (!component.Visible) + { + RemoveFromUpdateList(component); + } } + ProcessHelperList(first); + ProcessAdditions(); + ProcessHelperList(last); + ProcessRemovals(); } - ProcessHelperList(first); - ProcessAdditions(); - ProcessHelperList(last); - ProcessRemovals(); } private static void ProcessAdditions() { - while (additions.Count > 0) + lock (mutex) { - var component = additions.Dequeue(); - if (!updateListSet.Contains(component)) + while (additions.Count > 0) { - updateList.Add(component); - updateListSet.Add(component); + var component = additions.Dequeue(); + if (!updateListSet.Contains(component)) + { + updateList.Add(component); + updateListSet.Add(component); + } } } } private static void ProcessRemovals() { - while (removals.Count > 0) + lock (mutex) { - var component = removals.Dequeue(); - updateList.Remove(component); - updateListSet.Remove(component); - if (component as IKeyboardSubscriber == KeyboardDispatcher.Subscriber) + while (removals.Count > 0) { - KeyboardDispatcher.Subscriber = null; + var component = removals.Dequeue(); + updateList.Remove(component); + updateListSet.Remove(component); + if (component as IKeyboardSubscriber == KeyboardDispatcher.Subscriber) + { + KeyboardDispatcher.Subscriber = null; + } } } } private static void ProcessHelperList(List list) { - if (list.Count == 0) { return; } - foreach (var item in list) + lock (mutex) { - int index = 0; - if (updateList.Count > 0) + if (list.Count == 0) { return; } + foreach (var item in list) { - index = updateList.Count - 1; - while (updateList[index].UpdateOrder > item.UpdateOrder) + int index = 0; + if (updateList.Count > 0) { - index--; - if (index == 0) { break; } + index = updateList.Count - 1; + while (updateList[index].UpdateOrder > item.UpdateOrder) + { + index--; + if (index == 0) { break; } + } + } + if (!updateListSet.Contains(item)) + { + updateList.Insert(index, item); + updateListSet.Add(item); } } - if (!updateListSet.Contains(item)) - { - updateList.Insert(index, item); - updateListSet.Add(item); - } + list.Clear(); } - list.Clear(); } private static void HandlePersistingElements(float deltaTime) { - GUIMessageBox.AddActiveToGUIUpdateList(); + lock (mutex) + { + GUIMessageBox.AddActiveToGUIUpdateList(); - if (pauseMenuOpen) - { - PauseMenu.AddToGUIUpdateList(); - } - if (settingsMenuOpen) - { - GameMain.Config.SettingsFrame.AddToGUIUpdateList(); - } + if (pauseMenuOpen) + { + PauseMenu.AddToGUIUpdateList(); + } + if (settingsMenuOpen) + { + GameMain.Config.SettingsFrame.AddToGUIUpdateList(); + } - //the "are you sure you want to quit" prompts are drawn on top of everything else - if (GUIMessageBox.VisibleBox?.UserData as string == "verificationprompt" || GUIMessageBox.VisibleBox?.UserData as string == "bugreporter") - { - GUIMessageBox.VisibleBox.AddToGUIUpdateList(); - } + //the "are you sure you want to quit" prompts are drawn on top of everything else + if (GUIMessageBox.VisibleBox?.UserData as string == "verificationprompt" || GUIMessageBox.VisibleBox?.UserData as string == "bugreporter") + { + GUIMessageBox.VisibleBox.AddToGUIUpdateList(); + } + } } #endregion @@ -826,14 +883,20 @@ namespace Barotrauma public static bool IsMouseOn(GUIComponent target) { - if (target == null) { return false; } - //if (MouseOn == null) { return true; } - return target == MouseOn || target.IsParentOf(MouseOn); + lock (mutex) + { + if (target == null) { return false; } + //if (MouseOn == null) { return true; } + return target == MouseOn || target.IsParentOf(MouseOn); + } } public static void ForceMouseOn(GUIComponent c) { - MouseOn = c; + lock (mutex) + { + MouseOn = c; + } } /// @@ -841,223 +904,219 @@ namespace Barotrauma /// public static GUIComponent UpdateMouseOn() { - GUIComponent prevMouseOn = MouseOn; - MouseOn = null; - int inventoryIndex = -1; + lock (mutex) + { + GUIComponent prevMouseOn = MouseOn; + MouseOn = null; + int inventoryIndex = -1; - if (Inventory.IsMouseOnInventory()) - { - inventoryIndex = updateList.IndexOf(CharacterHUD.HUDFrame); - } - - if (!PlayerInput.PrimaryMouseButtonHeld() && !PlayerInput.PrimaryMouseButtonClicked()) - { - for (var i = updateList.Count - 1; i > inventoryIndex; i--) + if (Inventory.IsMouseOnInventory()) { - var c = updateList[i]; - if (!c.CanBeFocused) { continue; } - if (c.MouseRect.Contains(PlayerInput.MousePosition)) + inventoryIndex = updateList.IndexOf(CharacterHUD.HUDFrame); + } + + if (!PlayerInput.PrimaryMouseButtonHeld() && !PlayerInput.PrimaryMouseButtonClicked()) + { + for (var i = updateList.Count - 1; i > inventoryIndex; i--) { - if ((!PlayerInput.PrimaryMouseButtonHeld() && !PlayerInput.PrimaryMouseButtonClicked()) || c == prevMouseOn) + var c = updateList[i]; + if (!c.CanBeFocused) { continue; } + if (c.MouseRect.Contains(PlayerInput.MousePosition)) { - MouseOn = c; + if ((!PlayerInput.PrimaryMouseButtonHeld() && !PlayerInput.PrimaryMouseButtonClicked()) || c == prevMouseOn) + { + MouseOn = c; + } + break; } - break; } } - } - else - { - MouseOn = prevMouseOn; - } + else + { + MouseOn = prevMouseOn; + } - MouseCursor = UpdateMouseCursorState(MouseOn); - return MouseOn; + MouseCursor = UpdateMouseCursorState(MouseOn); + return MouseOn; + } + } private static CursorState UpdateMouseCursorState(GUIComponent c) { - // Waiting and drag cursor override everything else - if (MouseCursor == CursorState.Waiting) { return CursorState.Waiting; } - if (GUIScrollBar.DraggingBar != null) { return GUIScrollBar.DraggingBar.Bar.HoverCursor; } - - if (SubEditorScreen.IsSubEditor() && SubEditorScreen.DraggedItemPrefab != null) { return CursorState.Hand; } - - // Wire cursors - if (Character.Controlled != null) + lock (mutex) { - if (Character.Controlled.SelectedConstruction?.GetComponent() != null) + // Waiting and drag cursor override everything else + if (MouseCursor == CursorState.Waiting) { return CursorState.Waiting; } + if (GUIScrollBar.DraggingBar != null) { return GUIScrollBar.DraggingBar.Bar.HoverCursor; } + + if (SubEditorScreen.IsSubEditor() && SubEditorScreen.DraggedItemPrefab != null) { return CursorState.Hand; } + + // Wire cursors + if (Character.Controlled != null) { - if (Connection.DraggingConnected != null) + if (Character.Controlled.SelectedConstruction?.GetComponent() != null) { - return CursorState.Dragging; - } - else if (ConnectionPanel.HighlightedWire != null) - { - return CursorState.Hand; + if (Connection.DraggingConnected != null) + { + return CursorState.Dragging; + } + else if (ConnectionPanel.HighlightedWire != null) + { + return CursorState.Hand; + } } + if (Wire.DraggingWire != null) { return CursorState.Dragging; } } - if (Wire.DraggingWire != null) { return CursorState.Dragging; } - } - if (c == null || c is GUICustomComponent) - { - switch (Screen.Selected) + if (c == null || c is GUICustomComponent) { - // Character editor limbs - case CharacterEditorScreen editor: - return editor.GetMouseCursorState(); - // Portrait area during gameplay - case GameScreen _ when !(Character.Controlled?.ShouldLockHud() ?? true): - if (HUDLayoutSettings.BottomRightInfoArea.Contains(PlayerInput.MousePosition) || - Rectangle.Union(HUDLayoutSettings.AfflictionAreaLeft, HUDLayoutSettings.HealthBarArea).Contains(PlayerInput.MousePosition)) - { - return CursorState.Hand; - } - break; - // Sub editor drag and highlight - case SubEditorScreen editor: + switch (Screen.Selected) { - // Portrait area - if (editor.WiringMode && HUDLayoutSettings.BottomRightInfoArea.Contains(PlayerInput.MousePosition)) - { - return CursorState.Hand; - } - - foreach (var mapEntity in MapEntity.mapEntityList) - { - if (MapEntity.StartMovingPos != Vector2.Zero) - { - return CursorState.Dragging; - } - if (mapEntity.IsHighlighted) + // Character editor limbs + case CharacterEditorScreen editor: + return editor.GetMouseCursorState(); + // Portrait area during gameplay + case GameScreen _ when !(Character.Controlled?.ShouldLockHud() ?? true): + if (HUDLayoutSettings.BottomRightInfoArea.Contains(PlayerInput.MousePosition) || + Rectangle.Union(HUDLayoutSettings.AfflictionAreaLeft, HUDLayoutSettings.HealthBarArea).Contains(PlayerInput.MousePosition)) { return CursorState.Hand; } + break; + // Sub editor drag and highlight + case SubEditorScreen editor: + { + // Portrait area + if (editor.WiringMode && HUDLayoutSettings.BottomRightInfoArea.Contains(PlayerInput.MousePosition)) + { + return CursorState.Hand; + } + + foreach (var mapEntity in MapEntity.mapEntityList) + { + if (MapEntity.StartMovingPos != Vector2.Zero) + { + return CursorState.Dragging; + } + if (mapEntity.IsHighlighted) + { + return CursorState.Hand; + } + } + break; } - break; - } - - // Campaign map highlighted location - case LobbyScreen lobby: - { - if (lobby.CampaignUI?.Campaign.Map.HighlightedLocation != null) { return CursorState.Hand; } - break; - } - - case NetLobbyScreen lobby: - { - if (lobby.CampaignUI?.Campaign.Map.HighlightedLocation != null) { return CursorState.Hand; } - break; } } - } - if (c != null && c.Visible) - { - // When a button opens a submenu, it increases to the size of the entire screen. - // And this is of course picked up as clickable area. - // There has to be a better way of checking this but for now this works. - var monitorRect = new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight); - - var parent = FindInteractParent(c); - - if (c.Enabled) + if (c != null && c.Visible) { - // Some parent elements take priority - // but not when the child is a GUIButton or GUITickBox - if (!(parent is GUIButton) && !(parent is GUIListBox) || - (c is GUIButton) || (c is GUITickBox)) + if (c.AlwaysOverrideCursor) { return c.HoverCursor; } + + // When a button opens a submenu, it increases to the size of the entire screen. + // And this is of course picked up as clickable area. + // There has to be a better way of checking this but for now this works. + var monitorRect = new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight); + + var parent = FindInteractParent(c); + + if (c.Enabled) { - if (!c.Rect.Equals(monitorRect)) { return c.HoverCursor; } + // Some parent elements take priority + // but not when the child is a GUIButton or GUITickBox + if (!(parent is GUIButton) && !(parent is GUIListBox) || + (c is GUIButton) || (c is GUITickBox)) + { + if (!c.Rect.Equals(monitorRect)) { return c.HoverCursor; } + } + } + + // Children in list boxes can be interacted with despite not having + // a GUIButton inside of them so instead of hard coding we check if + // the children can be interacted with by checking their hover state + if (parent is GUIListBox listBox) + { + if (listBox.DraggedElement != null) { return CursorState.Dragging; } + if (listBox.CanDragElements) { return CursorState.Move; } + + var hoverParent = c; + while (true) + { + if (hoverParent == parent || hoverParent == null) { break; } + if (hoverParent.State == GUIComponent.ComponentState.Hover) { return CursorState.Hand; } + hoverParent = hoverParent.Parent; + } + } + + if (parent != null) + { + if (!parent.Rect.Equals(monitorRect)) { return parent.HoverCursor; } } } - - // Children in list boxes can be interacted with despite not having - // a GUIButton inside of them so instead of hard coding we check if - // the children can be interacted with by checking their hover state - if (parent is GUIListBox listBox) + + if (Inventory.IsMouseOnInventory()) { return Inventory.GetInventoryMouseCursor(); } + + var character = Character.Controlled; + // ReSharper disable once InvertIf + if (character != null) + { + // Health menus + if (character.CharacterHealth.MouseOnElement) { return CursorState.Hand; } + + if (character.SelectedCharacter != null) + { + if (character.SelectedCharacter.CharacterHealth.MouseOnElement) + { + return CursorState.Hand; + } + } + + // Character is hovering over an item placed in the world + if (character.FocusedItem != null) { return CursorState.Hand; } + } + + return CursorState.Default; + + static GUIComponent FindInteractParent(GUIComponent component) { - if (listBox.DraggedElement != null) { return CursorState.Dragging; } - if (listBox.CanDragElements) { return CursorState.Move; } - - var hoverParent = c; while (true) { - if (hoverParent == parent || hoverParent == null) { break; } - if (hoverParent.State == GUIComponent.ComponentState.Hover) { return CursorState.Hand; } - hoverParent = hoverParent.Parent; - } - } - - if (parent != null) - { - if (!parent.Rect.Equals(monitorRect)) { return parent.HoverCursor; } - } - } - - if (Inventory.IsMouseOnInventory()) { return Inventory.GetInventoryMouseCursor(); } + var parent = component.Parent; + if (parent == null) { return null; } - var character = Character.Controlled; - // ReSharper disable once InvertIf - if (character != null) - { - // Health menus - if (character.CharacterHealth.MouseOnElement) { return CursorState.Hand; } - - if (character.SelectedCharacter != null) - { - if (character.SelectedCharacter.CharacterHealth.MouseOnElement) - { - return CursorState.Hand; - } - } - - // Character is hovering over an item placed in the world - if (character.FocusedItem != null) { return CursorState.Hand; } - } - - return CursorState.Default; - - static GUIComponent FindInteractParent(GUIComponent component) - { - while (true) - { - var parent = component.Parent; - if (parent == null) { return null; } - - if (ContainsMouse(parent)) - { - if (parent.Enabled) + if (ContainsMouse(parent)) { - switch (parent) + if (parent.Enabled) { - case GUIButton button: - return button; - case GUITextBox box: - return box; - case GUIListBox list: - return list; - case GUIScrollBar bar: - return bar; + switch (parent) + { + case GUIButton button: + return button; + case GUITextBox box: + return box; + case GUIListBox list: + return list; + case GUIScrollBar bar: + return bar; + } } + component = parent; + } + else + { + return null; } - component = parent; - } - else - { - return null; } } - } - static bool ContainsMouse(GUIComponent component) - { - // If component has a mouse rectangle then use that, if not use it's physical rect - return !component.MouseRect.Equals(Rectangle.Empty) ? - component.MouseRect.Contains(PlayerInput.MousePosition) : - component.Rect.Contains(PlayerInput.MousePosition); - } + static bool ContainsMouse(GUIComponent component) + { + // If component has a mouse rectangle then use that, if not use it's physical rect + return !component.MouseRect.Equals(Rectangle.Empty) ? + component.MouseRect.Contains(PlayerInput.MousePosition) : + component.Rect.Contains(PlayerInput.MousePosition); + } + } } /// @@ -1080,8 +1139,11 @@ namespace Barotrauma public static void ClearCursorWait() { - CoroutineManager.StopCoroutines("WaitCursorTimeout"); - MouseCursor = CursorState.Default; + lock (mutex) + { + CoroutineManager.StopCoroutines("WaitCursorTimeout"); + MouseCursor = CursorState.Default; + } } public static bool HasSizeChanged(Point referenceResolution, float referenceUIScale, float referenceHUDScale) @@ -1092,58 +1154,110 @@ namespace Barotrauma public static void Update(float deltaTime) { - if (PlayerInput.KeyDown(Keys.LeftControl) && PlayerInput.KeyHit(Keys.S)) + lock (mutex) { - debugDrawSounds = !debugDrawSounds; - } - if (PlayerInput.KeyDown(Keys.LeftControl) && PlayerInput.KeyHit(Keys.E)) - { - debugDrawEvents = !debugDrawEvents; - } + if (PlayerInput.KeyDown(Keys.LeftControl) && PlayerInput.KeyHit(Keys.S)) + { + debugDrawSounds = !debugDrawSounds; + } + if (PlayerInput.KeyDown(Keys.LeftControl) && PlayerInput.KeyHit(Keys.E)) + { + debugDrawEvents = !debugDrawEvents; + } + if (PlayerInput.IsCtrlDown() && PlayerInput.KeyHit(Keys.M)) + { + debugDrawMetadata = !debugDrawMetadata; + } - HandlePersistingElements(deltaTime); - RefreshUpdateList(); - UpdateMouseOn(); - Debug.Assert(updateList.Count == updateListSet.Count); - updateList.ForEach(c => c.UpdateAuto(deltaTime)); - UpdateMessages(deltaTime); + if (debugDrawMetadata) + { + if (PlayerInput.KeyHit(Keys.Up)) + { + debugDrawMetadataOffset--; + } + + if (PlayerInput.KeyHit(Keys.Down)) + { + debugDrawMetadataOffset++; + } + + if (PlayerInput.IsCtrlDown()) + { + if (PlayerInput.KeyHit(Keys.D1)) + { + ignoredMetadataInfo[0] = ignoredMetadataInfo[0] == string.Empty ? "reputation.location" : string.Empty; + debugDrawMetadataOffset = 0; + } + + if (PlayerInput.KeyHit(Keys.D2)) + { + ignoredMetadataInfo[1] = ignoredMetadataInfo[1] == string.Empty ? "reputation.faction" : string.Empty; + debugDrawMetadataOffset = 0; + } + + if (PlayerInput.KeyHit(Keys.D3)) + { + ignoredMetadataInfo[2] = ignoredMetadataInfo[2] == string.Empty ? "upgrade." : string.Empty; + debugDrawMetadataOffset = 0; + } + + if (PlayerInput.KeyHit(Keys.D4)) + { + ignoredMetadataInfo[3] = ignoredMetadataInfo[3] == string.Empty ? "upgradeprice." : string.Empty; + debugDrawMetadataOffset = 0; + } + } + + } + + HandlePersistingElements(deltaTime); + RefreshUpdateList(); + UpdateMouseOn(); + Debug.Assert(updateList.Count == updateListSet.Count); + updateList.ForEach(c => c.UpdateAuto(deltaTime)); + UpdateMessages(deltaTime); + } } private static void UpdateMessages(float deltaTime) { - foreach (GUIMessage msg in messages) + lock (mutex) { - if (msg.WorldSpace) continue; - msg.Timer -= deltaTime; + foreach (GUIMessage msg in messages) + { + if (msg.WorldSpace) continue; + msg.Timer -= deltaTime; - if (msg.Size.X > HUDLayoutSettings.MessageAreaTop.Width) - { - msg.Pos = Vector2.Lerp(Vector2.Zero, new Vector2(-HUDLayoutSettings.MessageAreaTop.Width - msg.Size.X, 0), 1.0f - msg.Timer / msg.LifeTime); - } - else - { - //enough space to show the full message, position it at the center of the msg area - if (msg.Timer > 1.0f) + if (msg.Size.X > HUDLayoutSettings.MessageAreaTop.Width) { - msg.Pos = Vector2.Lerp(msg.Pos, new Vector2(-HUDLayoutSettings.MessageAreaTop.Width / 2 - msg.Size.X / 2, 0), Math.Min(deltaTime * 10.0f, 1.0f)); + msg.Pos = Vector2.Lerp(Vector2.Zero, new Vector2(-HUDLayoutSettings.MessageAreaTop.Width - msg.Size.X, 0), 1.0f - msg.Timer / msg.LifeTime); } else { - msg.Pos = Vector2.Lerp(msg.Pos, new Vector2(-HUDLayoutSettings.MessageAreaTop.Width - msg.Size.X, 0), deltaTime * 10.0f); + //enough space to show the full message, position it at the center of the msg area + if (msg.Timer > 1.0f) + { + msg.Pos = Vector2.Lerp(msg.Pos, new Vector2(-HUDLayoutSettings.MessageAreaTop.Width / 2 - msg.Size.X / 2, 0), Math.Min(deltaTime * 10.0f, 1.0f)); + } + else + { + msg.Pos = Vector2.Lerp(msg.Pos, new Vector2(-HUDLayoutSettings.MessageAreaTop.Width - msg.Size.X, 0), deltaTime * 10.0f); + } } + //only the first message (the currently visible one) is updated at a time + break; } - //only the first message (the currently visible one) is updated at a time - break; + + foreach (GUIMessage msg in messages) + { + if (!msg.WorldSpace) continue; + msg.Timer -= deltaTime; + msg.Pos += msg.Velocity * deltaTime; + } + + messages.RemoveAll(m => m.Timer <= 0.0f); } - foreach (GUIMessage msg in messages) - { - if (!msg.WorldSpace) continue; - msg.Timer -= deltaTime; - msg.Pos += msg.Velocity * deltaTime; - } - - messages.RemoveAll(m => m.Timer <= 0.0f); } #region Element drawing @@ -2012,9 +2126,13 @@ namespace Barotrauma }; msgBox.Buttons[0].OnClicked = (_, userdata) => { + if (GameMain.GameSession.RoundSummary?.Frame != null) + { + GUIMessageBox.MessageBoxes.Remove(GameMain.GameSession.RoundSummary.Frame); + } + TogglePauseMenu(btn, userData); - GameMain.GameSession.LoadPrevious(); - GameMain.LobbyScreen.Select(); + GameMain.GameSession.LoadPreviousSave(); return true; }; msgBox.Buttons[0].OnClicked += msgBox.Close; @@ -2026,15 +2144,20 @@ namespace Barotrauma }; return true; }; + button = new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonContainer.RectTransform), TextManager.Get("PauseMenuSaveQuit")) + { + UserData = "save" + }; + button.OnClicked += QuitClicked; + button.OnClicked += TogglePauseMenu; } - else if (GameMain.GameSession.GameMode is SubTestMode) + else if (GameMain.GameSession.GameMode is TestGameMode) { button = new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonContainer.RectTransform), text: TextManager.Get("PauseMenuReturnToEditor")) { OnClicked = (btn, userdata) => { - GameMain.GameSession.GameMode.End(""); - + GameMain.GameSession.EndRound(""); return true; } }; @@ -2042,14 +2165,17 @@ namespace Barotrauma } else if (!GameMain.GameSession.GameMode.IsSinglePlayer && GameMain.Client != null && GameMain.Client.HasPermission(ClientPermissions.ManageRound)) { - new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonContainer.RectTransform), text: TextManager.Get("EndRound")) + new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonContainer.RectTransform), + text: TextManager.Get(GameMain.GameSession.GameMode is CampaignMode ? "ReturnToServerlobby": "EndRound")) { OnClicked = (btn, userdata) => { if (!GameMain.Client.HasPermission(ClientPermissions.ManageRound)) { return false; } - if (!Submarine.MainSub.AtStartPosition && !Submarine.MainSub.AtEndPosition) + if (GameMain.GameSession.GameMode is CampaignMode || (!Submarine.MainSub.AtStartPosition && !Submarine.MainSub.AtEndPosition)) { - var msgBox = new GUIMessageBox("", TextManager.Get("EndRoundSubNotAtLevelEnd"), new string[] { TextManager.Get("Yes"), TextManager.Get("No") }) + var msgBox = new GUIMessageBox("", + TextManager.Get(GameMain.GameSession.GameMode is CampaignMode ? "PauseMenuReturnToServerLobbyVerification" : "EndRoundSubNotAtLevelEnd"), + new string[] { TextManager.Get("Yes"), TextManager.Get("No") }) { UserData = "verificationprompt" }; @@ -2072,20 +2198,7 @@ namespace Barotrauma }; } } - - if (Screen.Selected == GameMain.LobbyScreen) - { - if (GameMain.GameSession.GameMode is SinglePlayerCampaign spMode) - { - button = new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonContainer.RectTransform), TextManager.Get("PauseMenuSaveQuit")) - { - UserData = "save" - }; - button.OnClicked += QuitClicked; - button.OnClicked += TogglePauseMenu; - } - } - + button = new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonContainer.RectTransform), TextManager.Get("PauseMenuQuit")); button.OnClicked += (btn, userData) => { @@ -2118,6 +2231,8 @@ namespace Barotrauma } return true; }; + + GUITextBlock.AutoScaleAndNormalize(buttonContainer.Children.Where(c => c is GUIButton).Select(c => ((GUIButton)c).TextBlock)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs index 42bcfd05b..878dca710 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using System; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace Barotrauma @@ -145,6 +146,11 @@ namespace Barotrauma textBlock.ToolTip = value; } } + + public bool Pulse { get; set; } + private float pulseTimer; + private float pulseExpand; + private bool flashed; public GUIButton(RectTransform rectT, string text = "", Alignment textAlignment = Alignment.Center, string style = "", Color? color = null) : base(style, rectT) { @@ -196,7 +202,14 @@ namespace Barotrauma protected override void Draw(SpriteBatch spriteBatch) { - //do nothing + if (Pulse && pulseTimer > 1.0f) + { + Rectangle expandRect = Rect; + 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)); + } } protected override void Update(float deltaTime) @@ -244,13 +257,41 @@ namespace Barotrauma } else { - State = Selected ? ComponentState.Selected : ComponentState.None; + if (!ExternalHighlight) + { + State = Selected ? ComponentState.Selected : ComponentState.None; + } + else + { + State = ComponentState.Hover; + } } foreach (GUIComponent child in Children) { child.State = State; } + + if (Pulse) + { + pulseTimer += deltaTime; + if (pulseTimer > 1.0f) + { + if (!flashed) + { + flashed = true; + Frame.Flash(Color.White * 0.2f, 0.8f, true); + } + + pulseExpand += deltaTime; + if (pulseExpand > 1.0f) + { + pulseTimer = 0.0f; + pulseExpand = 0.0f; + flashed = false; + } + } + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs index 8595c032e..a88ea3e89 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs @@ -20,7 +20,7 @@ namespace Barotrauma _instance = new GUICanvas(); if (GameMain.Instance != null) { - GameMain.Instance.OnResolutionChanged += RecalculateSize; + GameMain.Instance.ResolutionChanged += RecalculateSize; } _instance.ItemComponentHolder = new GUIFrame(new RectTransform(Vector2.One, _instance, Anchor.Center)).RectTransform; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIColorSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIColorSettings.cs deleted file mode 100644 index 7b402000f..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIColorSettings.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Microsoft.Xna.Framework; - -namespace Barotrauma -{ - public class GUIColorSettings - { - // Inventory - public static Color EquipmentSlotIconColor = new Color(99, 70, 64); - - // Health HUD - public static Color BuffColorLow = Color.LightGreen; - public static Color BuffColorMedium = Color.Green; - public static Color BuffColorHigh = Color.DarkGreen; - - public static Color DebuffColorLow = Color.DarkSalmon; - public static Color DebuffColorMedium = Color.Red; - public static Color DebuffColorHigh = Color.DarkRed; - - public static Color HealthBarColorLow = Color.Red; - public static Color HealthBarColorMedium = Color.Orange; - public static Color HealthBarColorHigh = new Color(78, 114, 88); - - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index 76f412e71..e944fe410 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -11,12 +11,16 @@ using System.Net; namespace Barotrauma { + public enum SlideDirection { Up, Down, Left, Right } + public abstract class GUIComponent { #region Hierarchy public GUIComponent Parent => RectTransform.Parent?.GUIComponent; public CursorState HoverCursor = CursorState.Default; + + public bool AlwaysOverrideCursor = false; public delegate bool SecondaryButtonDownHandler(GUIComponent component, object userData); public SecondaryButtonDownHandler OnSecondaryClicked; @@ -75,7 +79,7 @@ namespace Barotrauma public virtual void RemoveChild(GUIComponent child) { - if (child == null) return; + if (child == null) { return; } child.RectTransform.Parent = null; } @@ -139,6 +143,11 @@ namespace Barotrauma public bool AutoUpdate { get; set; } = true; public bool AutoDraw { get; set; } = true; public int UpdateOrder { get; set; } + + public bool Bounce { get; set; } + private float bounceTimer; + private float bounceJump; + private bool bounceDown; public Action OnAddedToGUIUpdateList; @@ -158,6 +167,8 @@ namespace Barotrauma protected Color disabledColor; protected Color pressedColor; + public bool GlowOnSelect { get; set; } + private CoroutineHandle pulsateCoroutine; protected Color flashColor; @@ -326,6 +337,11 @@ namespace Barotrauma { get { return RectTransform.CountChildren; } } + + /// + /// Currently only used for the fade effect in GUIListBox, should be set to the same value as Color but only assigned once + /// + public Color DefaultColor { get; set; } public virtual Color Color { @@ -462,6 +478,36 @@ namespace Barotrauma OnSecondaryClicked?.Invoke(this, userData); } } + + if (Bounce) + { + if (bounceTimer > 3.0f || bounceDown) + { + RectTransform.ScreenSpaceOffset = new Point(RectTransform.ScreenSpaceOffset.X, (int) -(bounceJump * 10f)); + if (!bounceDown) + { + bounceJump += deltaTime * 4; + if (bounceJump > 0.5f) + { + bounceDown = true; + } + } + else + { + bounceJump -= deltaTime * 4; + if (bounceJump <= 0.0f) + { + bounceJump = 0.0f; + bounceTimer = 0.0f; + bounceDown = false; + } + } + } + else + { + bounceTimer += deltaTime; + } + } if (flashTimer > 0.0f) { @@ -526,12 +572,14 @@ namespace Barotrauma protected virtual Color GetColor(ComponentState state) { if (!Enabled) { return DisabledColor; } + if (ExternalHighlight) { return HoverColor; } + return state switch { ComponentState.Hover => HoverColor, ComponentState.HoverSelected => HoverColor, ComponentState.Pressed => PressedColor, - ComponentState.Selected => SelectedColor, + ComponentState.Selected when !GlowOnSelect => SelectedColor, _ => Color, }; } @@ -619,6 +667,11 @@ namespace Barotrauma } } + if (GlowOnSelect && State == ComponentState.Selected) + { + GUI.UIGlow.Draw(spriteBatch, Rect, SelectedColor); + } + if (flashTimer > 0.0f) { //the number of flashes depends on the duration, 1 flash per 1 full second @@ -690,6 +743,7 @@ namespace Barotrauma protected virtual void SetAlpha(float a) { color = new Color(color.R / 255.0f, color.G / 255.0f, color.B / 255.0f, a); + hoverColor = new Color(hoverColor.R / 255.0f, hoverColor.G / 255.0f, hoverColor.B / 255.0f, a);; } public virtual void Flash(Color? color = null, float flashDuration = 1.5f, bool useRectangleFlash = false, bool useCircularFlash = false, Vector2? flashRectInflate = null) @@ -707,10 +761,77 @@ namespace Barotrauma CoroutineManager.StartCoroutine(LerpAlpha(0.0f, duration, removeAfter)); } - private IEnumerable LerpAlpha(float to, float duration, bool removeAfter) + public void FadeIn(float wait, float duration) + { + SetAlpha(0.0f); + CoroutineManager.StartCoroutine(LerpAlpha(1.0f, duration, false, wait)); + } + + public void SlideIn(float wait, float duration, int amount, SlideDirection direction) + { + RectTransform.ScreenSpaceOffset = direction switch + { + SlideDirection.Up => new Point(0, amount), + SlideDirection.Down => new Point(0, -amount), + SlideDirection.Left => new Point(amount, 0), + SlideDirection.Right => new Point(-amount, 0), + _ => RectTransform.ScreenSpaceOffset + }; + CoroutineManager.StartCoroutine(SlideToPosition(duration, wait, Vector2.Zero)); + } + + public void SlideOut(float duration, int amount, SlideDirection direction) + { + RectTransform.ScreenSpaceOffset = Point.Zero; + + Vector2 targetPos = direction switch + { + SlideDirection.Up => new Vector2(0, amount), + SlideDirection.Down => new Vector2(0, -amount), + SlideDirection.Left => new Vector2(amount, 0), + SlideDirection.Right => new Vector2(-amount, 0), + _ => Vector2.Zero + }; + + CoroutineManager.StartCoroutine(SlideToPosition(duration, 0.0f, targetPos)); + } + + private IEnumerable SlideToPosition(float duration, float wait, Vector2 target) { float t = 0.0f; - float startA = color.A; + var (startX, startY) = RectTransform.ScreenSpaceOffset.ToVector2(); + var (endX, endY) = target; + while (t < wait) + { + t += CoroutineManager.DeltaTime; + yield return CoroutineStatus.Running; + } + t = 0.0f; + + while (t < duration) + { + t += CoroutineManager.DeltaTime; + RectTransform.ScreenSpaceOffset = new Point((int)MathHelper.Lerp(startX, endX, t / duration), (int)MathHelper.Lerp(startY, endY, t / duration)); + yield return CoroutineStatus.Running; + } + + RectTransform.ScreenSpaceOffset = new Point(0, 0); + + yield return CoroutineStatus.Success; + } + + private IEnumerable LerpAlpha(float to, float duration, bool removeAfter, float wait = 0.0f) + { + State = ComponentState.None; + float t = 0.0f; + float startA = color.A / 255.0f; + + while (t < wait) + { + t += CoroutineManager.DeltaTime; + yield return CoroutineStatus.Running; + } + t = 0.0f; while (t < duration) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIFrame.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIFrame.cs index 1f56aa1f0..0e5d5ca42 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIFrame.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIFrame.cs @@ -6,6 +6,8 @@ namespace Barotrauma { public class GUIFrame : GUIComponent { + public int OutlineThickness { get; set; } + public GUIFrame(RectTransform rectT, string style = "", Color? color = null) : base(style, rectT) { Enabled = true; @@ -26,7 +28,7 @@ namespace Barotrauma if (OutlineColor != Color.Transparent) { - GUI.DrawRectangle(spriteBatch, Rect, OutlineColor * (OutlineColor.A/255.0f), false); + GUI.DrawRectangle(spriteBatch, Rect, OutlineColor * (OutlineColor.A/255.0f), false, thickness: OutlineThickness); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs index e8a601c8d..1a2ce4052 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs @@ -42,17 +42,32 @@ namespace Barotrauma { return crop; } - set + } + + public void SetCrop(bool state, bool center = true) + { + crop = state; + if (crop && sprite != null) { - crop = value; - if (crop) - { - sourceRect.Width = Math.Min(sprite.SourceRect.Width, Rect.Width); - sourceRect.Height = Math.Min(sprite.SourceRect.Height, Rect.Height); + sourceRect.Width = Math.Min(sprite.SourceRect.Width, (int)(Rect.Width / Scale)); + sourceRect.Height = Math.Min(sprite.SourceRect.Height, (int)(Rect.Height / Scale)); + + if (center) + { + sourceRect.X = (sprite.SourceRect.Width - sourceRect.Width) / 2; + sourceRect.Y = (sprite.SourceRect.Height - sourceRect.Height) / 2; } + + origin = sourceRect.Size.ToVector2() / 2; + } + else + { + origin = sprite == null ? Vector2.Zero : sprite.size / 2; } } + private Vector2 origin; + public float Scale { get; @@ -72,7 +87,8 @@ namespace Barotrauma { if (sprite == value) return; sprite = value; - sourceRect = sprite.SourceRect; + sourceRect = value == null ? Rectangle.Empty : value.SourceRect; + origin = value == null ? Vector2.Zero : value.size / 2; if (scaleToFit) RecalculateScale(); } } @@ -134,7 +150,8 @@ namespace Barotrauma { loadingTextures = true; loading = true; - TaskPool.Add(LoadTextureAsync(), (Task) => + TaskPool.Add("LoadTextureAsync", + LoadTextureAsync(), (Task) => { loading = false; lazyLoaded = true; @@ -178,7 +195,7 @@ namespace Barotrauma } else if (sprite?.Texture != null) { - spriteBatch.Draw(sprite.Texture, Rect.Center.ToVector2(), sourceRect, currentColor * (currentColor.A / 255.0f), Rotation, sprite.size / 2, + 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 e2a351874..d6e4efb6e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs @@ -134,7 +134,7 @@ namespace Barotrauma (RectTransform.Children.Count(c => !c.GUIComponent.IgnoreLayoutGroups) - 1) * (absoluteSpacing + relativeSpacing * thisSize); - stretchFactor = totalSize <= 0.0f || minSize >= thisSize ? + stretchFactor = totalSize <= 0.0f || minSize >= thisSize || totalSize == minSize ? 1.0f : (thisSize - minSize) / (totalSize - minSize); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs index 3852ecca1..9fd0993dc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs @@ -34,7 +34,7 @@ namespace Barotrauma public GUIScrollBar ScrollBar { get; private set; } private Dictionary childVisible = new Dictionary(); - + private int totalSize; private bool childrenNeedsRecalculation; private bool scrollBarNeedsRecalculation; @@ -59,6 +59,37 @@ namespace Barotrauma private bool useGridLayout; + private float targetScroll; + + private GUIComponent pendingScroll; + + public bool AllowMouseWheelScroll { get; set; } = true; + + /// + /// Scrolls the list smoothly + /// + public bool SmoothScroll { get; set; } + + /// + /// Whether to only allow scrolling from one element to the next when smooth scrolling is enabled + /// + public bool ClampScrollToElements { get; set; } + + /// + /// When set to true elements at the bottom of the list are gradually faded + /// + public bool FadeElements { get; set; } + + /// + /// Adds enough extra padding to the bottom so the end of the scroll will only contain the last element + /// + public bool PadBottom { get; set; } + + /// + /// When set to true always selects the topmost item on the list + /// + public bool SelectTop { get; set; } + public bool UseGridLayout { get { return useGridLayout; } @@ -189,10 +220,13 @@ namespace Barotrauma private Point draggedReferenceOffset; public GUIComponent DraggedElement => draggedElement; + + private bool scheduledScroll = false; /// For horizontal listbox, default side is on the bottom. For vertical, it's on the right. public GUIListBox(RectTransform rectT, bool isHorizontal = false, Color? color = null, string style = "", bool isScrollBarOnDefaultSide = true, bool useMouseDownToSelect = false) : base(style, rectT) { + HoverCursor = CursorState.Hand; CanBeFocused = true; selected = new List(); this.useMouseDownToSelect = useMouseDownToSelect; @@ -363,6 +397,49 @@ namespace Barotrauma } } } + + /// + /// Scrolls the list to the specific element, currently only works when smooth scrolling and PadBottom are enabled. + /// + /// + public void ScrollToElement(GUIComponent component) + { + GUI.PlayUISound(GUISoundType.Click); + List children = Content.Children.ToList(); + int index = children.IndexOf(component); + if (index < 0) { return; } + + targetScroll = MathHelper.Clamp(MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, (children.Count - 0.9f), index)), ScrollBar.MinValue, ScrollBar.MaxValue); + } + + public void ScrollToEnd(float duration) + { + CoroutineManager.StartCoroutine(ScrollCoroutine()); + + IEnumerable ScrollCoroutine() + { + if (BarSize >= 1.0f) + { + yield return CoroutineStatus.Success; + } + float t = 0.0f; + float startScroll = BarScroll * BarSize; + float distanceToTravel = ScrollBar.MaxValue - startScroll; + float progress = startScroll; + float speed = distanceToTravel / duration; + + while (t < duration && !MathUtils.NearlyEqual(ScrollBar.MaxValue, progress)) + { + t += CoroutineManager.DeltaTime; + progress += speed * CoroutineManager.DeltaTime; + BarScroll = progress; + yield return CoroutineStatus.Running; + } + + yield return CoroutineStatus.Success; + } + } + private void UpdateChildrenRect() { @@ -404,6 +481,32 @@ namespace Barotrauma } } + if (SelectTop) + { + foreach (GUIComponent child in Content.Children) + { + child.CanBeFocused = !selected.Contains(child); + if (!child.CanBeFocused) + { + child.State = ComponentState.None; + } + } + } + + if (SelectTop && Content.Children.Any() && pendingScroll == null) + { + GUIComponent component = Content.Children.FirstOrDefault(c => (c.Rect.Y - Content.Rect.Y) / (float)c.Rect.Height > -0.1f); + + if (component != null && !selected.Contains(component)) + { + int index = Content.Children.ToList().IndexOf(component); + if (index >= 0) + { + Select(index, false, false, takeKeyBoardFocus: true); + } + } + } + for (int i = 0; i < Content.CountChildren; i++) { var child = Content.RectTransform.GetChild(i)?.GUIComponent; @@ -418,7 +521,16 @@ namespace Barotrauma if (mouseDown) { - Select(i, autoScroll: false); + if (SelectTop) + { + pendingScroll = child; + ScrollToElement(child); + Select(i, autoScroll: false, takeKeyBoardFocus: true); + } + else + { + Select(i, autoScroll: false, takeKeyBoardFocus: true); + } } if (CanDragElements && PlayerInput.LeftButtonDown() && GUI.MouseOn == child) @@ -538,10 +650,93 @@ namespace Barotrauma { UpdateScrollBarSize(); } - if ((GUI.IsMouseOn(this) || GUI.IsMouseOn(ScrollBar)) && PlayerInput.ScrollWheelSpeed != 0) + + + if (FadeElements) { - ScrollBar.BarScroll -= (PlayerInput.ScrollWheelSpeed / 500.0f) * BarSize; + foreach (var (component, _) in childVisible) + { + float lerp = 0; + float y = component.Rect.Y; + float contentY = Content.Rect.Y; + float height = component.Rect.Height; + if (y < Content.Rect.Y) + { + float distance = (contentY - y) / height; + lerp = distance; + } + + float centerY = Content.Rect.Y + Content.Rect.Height / 2.0f; + if (y > centerY) + { + float distance = (y - centerY) / (centerY - height); + lerp = distance; + } + + component.Color = component.HoverColor = ToolBox.GradientLerp(lerp, component.DefaultColor, Color.Transparent); + component.DisabledColor = ToolBox.GradientLerp(lerp, component.Style.DisabledColor, Color.Transparent); + component.HoverColor = ToolBox.GradientLerp(lerp, component.Style.HoverColor, Color.Transparent); + + foreach (var child in component.GetAllChildren()) + { + Color gradient = ToolBox.GradientLerp(lerp, child.DefaultColor, Color.Transparent); + child.Color = child.HoverColor = gradient; + if (child is GUITextBlock block) + { + block.TextColor = block.HoverTextColor = gradient; + } + } + } } + + if (SmoothScroll) + { + if (targetScroll > -1) + { + float distance = Math.Abs(targetScroll - BarScroll); + float speed = Math.Max(distance * BarSize, 0.1f); + BarScroll = (1.0f - speed) * BarScroll + speed * targetScroll; + if (MathUtils.NearlyEqual(BarScroll, targetScroll) || GUIScrollBar.DraggingBar != null) + { + targetScroll = -1; + pendingScroll = null; + } + } + } + + if ((GUI.IsMouseOn(this) || GUI.IsMouseOn(ScrollBar)) && AllowMouseWheelScroll && PlayerInput.ScrollWheelSpeed != 0) + { + float speed = PlayerInput.ScrollWheelSpeed / 500.0f * BarSize; + if (SmoothScroll) + { + if (ClampScrollToElements) + { + bool scrollDown = Math.Clamp(PlayerInput.ScrollWheelSpeed, 0, 1) > 0; + + if (scrollDown) + { + SelectPrevious(takeKeyBoardFocus: true); + } + else + { + SelectNext(takeKeyBoardFocus: true); + } + } + else + { + pendingScroll = null; + if (targetScroll < 0) { targetScroll = BarScroll; } + targetScroll -= speed; + targetScroll = Math.Clamp(targetScroll, ScrollBar.MinValue, ScrollBar.MaxValue); + } + } + else + { + ScrollBar.BarScroll -= (PlayerInput.ScrollWheelSpeed / 500.0f) * BarSize; + } + } + + ScrollBar.Enabled = ScrollBarEnabled && BarSize < 1.0f; if (AutoHideScrollBar) { @@ -553,35 +748,47 @@ namespace Barotrauma } } - public void SelectNext(bool force = false, bool autoScroll = true) + public void SelectNext(bool force = false, bool autoScroll = true, bool takeKeyBoardFocus = false) { int index = SelectedIndex + 1; while (index < Content.CountChildren) { - if (Content.GetChild(index).Visible) + GUIComponent child = Content.GetChild(index); + if (child.Visible) { - Select(index, force, autoScroll); + Select(index, force, !SmoothScroll && autoScroll, takeKeyBoardFocus: takeKeyBoardFocus); + if (SmoothScroll) + { + pendingScroll = child; + ScrollToElement(child); + } break; } index++; } } - public void SelectPrevious(bool force = false, bool autoScroll = true) + public void SelectPrevious(bool force = false, bool autoScroll = true, bool takeKeyBoardFocus = false) { int index = SelectedIndex - 1; while (index >= 0) { - if (Content.GetChild(index).Visible) + GUIComponent child = Content.GetChild(index); + if (child.Visible) { - Select(index, force, autoScroll); + Select(index, force, !SmoothScroll && autoScroll, takeKeyBoardFocus: takeKeyBoardFocus); + if (SmoothScroll) + { + pendingScroll = child; + ScrollToElement(child); + } break; } index--; } } - public void Select(int childIndex, bool force = false, bool autoScroll = true) + public void Select(int childIndex, bool force = false, bool autoScroll = true, bool takeKeyBoardFocus = false) { if (childIndex >= Content.CountChildren || childIndex < 0) { return; } @@ -646,7 +853,7 @@ namespace Barotrauma } // If one of the children is the subscriber, we don't want to register, because it will unregister the child. - if (RectTransform.GetAllChildren().None(rt => rt.GUIComponent == GUI.KeyboardDispatcher.Subscriber)) + if (takeKeyBoardFocus && RectTransform.GetAllChildren().None(rt => rt.GUIComponent == GUI.KeyboardDispatcher.Subscriber)) { Selected = true; GUI.KeyboardDispatcher.Subscriber = this; @@ -712,6 +919,14 @@ namespace Barotrauma totalSize += (ScrollBar.IsHorizontal) ? child.Rect.Width : child.Rect.Height; } totalSize += Content.CountChildren * Spacing; + if (PadBottom) + { + GUIComponent last = Content.Children.LastOrDefault(); + if (last != null) + { + totalSize += Rect.Height - last.Rect.Height; + } + } } float minScrollBarSize = 20.0f; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessage.cs index 5f407a916..97ea742db 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessage.cs @@ -57,7 +57,7 @@ namespace Barotrauma public GUIMessage(string text, Color color, float lifeTime, ScalableFont font = null) { - coloredText = new ColoredText(text, color, false); + coloredText = new ColoredText(text, color, false, false); this.lifeTime = lifeTime; Timer = lifeTime; @@ -69,7 +69,7 @@ namespace Barotrauma public GUIMessage(string text, Color color, Vector2 worldPosition, Vector2 velocity, float lifeTime, Alignment textAlignment = Alignment.Center, ScalableFont font = null) { - coloredText = new ColoredText(text, color, false); + coloredText = new ColoredText(text, color, false, false); WorldSpace = true; pos = worldPosition; Timer = lifeTime; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs index 7a48e553c..df67d6172 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs @@ -30,6 +30,7 @@ namespace Barotrauma public GUITextBlock Header { get; private set; } public GUITextBlock Text { get; private set; } public string Tag { get; private set; } + public bool Closed { get; private set; } public GUIImage Icon { @@ -47,9 +48,16 @@ namespace Barotrauma } } - private bool alwaysVisible; + public GUIImage BackgroundIcon { get; private set; } + private GUIImage newBackgroundIcon; + + public bool AutoClose; + + private readonly bool alwaysVisible; private float openState; + private float iconState; + private bool iconSwitching; private bool closing; private Type type; @@ -62,7 +70,7 @@ namespace Barotrauma 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) + 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) : base(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas, Anchor.Center), style: GUI.Style.GetComponentStyle("GUIMessageBox." + type) != null ? "GUIMessageBox." + type : "GUIMessageBox") { int width = (int)(DefaultWidth * (type == Type.Default ? 1.0f : 1.5f)), height = 0; @@ -80,6 +88,15 @@ namespace Barotrauma } } + if (backgroundIcon != null) + { + BackgroundIcon = new GUIImage(new RectTransform(backgroundIcon.size.ToPoint(), RectTransform), backgroundIcon) + { + IgnoreLayoutGroups = true, + Color = Color.Transparent + }; + } + InnerFrame = new GUIFrame(new RectTransform(new Point(width, height), RectTransform, type == Type.InGame ? Anchor.TopCenter : Anchor.Center) { IsFixedSize = false }, style: null); GUI.Style.Apply(InnerFrame, "", this); this.type = type; @@ -145,6 +162,7 @@ namespace Barotrauma InnerFrame.RectTransform.AbsoluteOffset = new Point(0, GameMain.GraphicsHeight); alwaysVisible = true; CanBeFocused = false; + AutoClose = true; GUI.Style.Apply(InnerFrame, "", this); var horizontalLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.95f), InnerFrame.RectTransform, Anchor.Center), @@ -157,6 +175,10 @@ namespace Barotrauma { Icon = new GUIImage(new RectTransform(new Vector2(0.2f, 0.95f), horizontalLayoutGroup.RectTransform), icon, scaleToFit: true); } + else if (iconStyle != string.Empty) + { + Icon = new GUIImage(new RectTransform(new Vector2(0.2f, 0.95f), horizontalLayoutGroup.RectTransform), iconStyle, scaleToFit: true); + } Content = new GUILayoutGroup(new RectTransform(new Vector2(icon != null ? 0.65f : 0.85f, 1.0f), horizontalLayoutGroup.RectTransform)); @@ -182,6 +204,10 @@ namespace Barotrauma 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)) + { + Content.ChildAnchor = Anchor.Center; + } } if (height == 0) @@ -226,6 +252,23 @@ namespace Barotrauma } } + public void SetBackgroundIcon(Sprite icon) + { + if (icon == null) { return; } + GUIImage newIcon = new GUIImage(new RectTransform(icon.size.ToPoint(), RectTransform), icon) + { + IgnoreLayoutGroups = true, + Color = Color.Transparent + }; + + if (newBackgroundIcon != null) + { + RemoveChild(newBackgroundIcon); + newBackgroundIcon = null; + } + newBackgroundIcon = newIcon; + } + protected override void Update(float deltaTime) { if (type == Type.InGame) @@ -246,10 +289,19 @@ namespace Barotrauma if (!closing) { - InnerFrame.RectTransform.AbsoluteOffset = Vector2.SmoothStep(initialPos, defaultPos, openState).ToPoint(); + Point step = Vector2.SmoothStep(initialPos, defaultPos, openState).ToPoint(); + InnerFrame.RectTransform.AbsoluteOffset = step; + if (BackgroundIcon != null) + { + BackgroundIcon.RectTransform.AbsoluteOffset = new Point(InnerFrame.Rect.Location.X - (int) (BackgroundIcon.Rect.Size.X / 1.25f), (int)defaultPos.Y - BackgroundIcon.Rect.Size.Y / 2); + if (!MathUtils.NearlyEqual(openState, 1.0f)) + { + BackgroundIcon.Color = ToolBox.GradientLerp(openState, Color.Transparent, Color.White); + } + } openState = Math.Min(openState + deltaTime * 2.0f, 1.0f); - if (GUI.MouseOn != InnerFrame && !InnerFrame.IsParentOf(GUI.MouseOn)) + if (GUI.MouseOn != InnerFrame && !InnerFrame.IsParentOf(GUI.MouseOn) && AutoClose) { inGameCloseTimer += deltaTime; } @@ -262,13 +314,55 @@ namespace Barotrauma else { openState += deltaTime * 2.0f; - InnerFrame.RectTransform.AbsoluteOffset = Vector2.SmoothStep(defaultPos, endPos, openState - 1.0f).ToPoint(); + Point step = Vector2.SmoothStep(defaultPos, endPos, openState - 1.0f).ToPoint(); + InnerFrame.RectTransform.AbsoluteOffset = step; + if (BackgroundIcon != null) + { + BackgroundIcon.Color *= 0.9f; + } if (openState >= 2.0f) { if (Parent != null) { Parent.RemoveChild(this); } if (MessageBoxes.Contains(this)) { MessageBoxes.Remove(this); } } } + + if (newBackgroundIcon != null) + { + if (!iconSwitching) + { + if (BackgroundIcon != null) + { + BackgroundIcon.Color *= 0.9f; + if (BackgroundIcon.Color.A == 0) + { + BackgroundIcon = null; + iconSwitching = true; + RemoveChild(BackgroundIcon); + } + } + else + { + iconSwitching = true; + } + iconState = 0; + } + else + { + newBackgroundIcon.SetAsFirstChild(); + newBackgroundIcon.RectTransform.AbsoluteOffset = new Point(InnerFrame.Rect.Location.X - (int) (newBackgroundIcon.Rect.Size.X / 1.25f), (int)defaultPos.Y - newBackgroundIcon.Rect.Size.Y / 2); + newBackgroundIcon.Color = ToolBox.GradientLerp(iconState, Color.Transparent, Color.White); + if (newBackgroundIcon.Color.A == 255) + { + BackgroundIcon = newBackgroundIcon; + BackgroundIcon.SetAsFirstChild(); + newBackgroundIcon = null; + iconSwitching = false; + } + + iconState = Math.Min(iconState + deltaTime * 2.0f, 1.0f); + } + } } } @@ -284,6 +378,8 @@ namespace Barotrauma if (Parent != null) { Parent.RemoveChild(this); } if (MessageBoxes.Contains(this)) { MessageBoxes.Remove(this); } } + + Closed = true; } public bool Close(GUIButton button, object obj) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs index b084cbd7f..95f2e3a74 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs @@ -179,7 +179,7 @@ namespace Barotrauma private float pressedDelay = 0.5f; private bool IsPressedTimerRunning { get { return pressedTimer > 0; } } - public GUINumberInput(RectTransform rectT, NumberType inputType, string style = "", Alignment textAlignment = Alignment.Center, float? relativeButtonAreaWidth = null) : base(style, rectT) + public GUINumberInput(RectTransform rectT, NumberType inputType, string style = "", Alignment textAlignment = Alignment.Center, float? relativeButtonAreaWidth = null, bool hidePlusMinusButtons = false) : base(style, rectT) { LayoutGroup = new GUILayoutGroup(new RectTransform(Vector2.One, rectT), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; @@ -235,7 +235,7 @@ namespace Barotrauma return true; }; - if (inputType != NumberType.Int) + if (inputType != NumberType.Int || hidePlusMinusButtons) { HidePlusMinusButtons(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIProgressBar.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIProgressBar.cs index ceeeb5192..0a706871a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIProgressBar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIProgressBar.cs @@ -121,7 +121,7 @@ namespace Barotrauma (int)(Rect.Width - style.Padding.X + style.Padding.Z), (int)(Rect.Height - style.Padding.Y + style.Padding.W)); frame.Visible = showFrame; - slider.Visible = true; + slider.Visible = BarSize > 0.0f; if (showFrame) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs index 75ddeeb79..c05e96d87 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs @@ -37,6 +37,8 @@ namespace Barotrauma public UISprite UIGlow { get; private set; } public UISprite UIGlowCircular { get; private set; } + public UISprite ButtonPulse { get; private set; } + public SpriteSheet FocusIndicator { get; private set; } /// @@ -206,6 +208,9 @@ namespace Barotrauma case "uiglowcircular": UIGlowCircular = new UISprite(subElement); break; + case "endroundbuttonpulse": + ButtonPulse = new UISprite(subElement); + break; case "focusindicator": FocusIndicator = new SpriteSheet(subElement); break; @@ -255,7 +260,8 @@ namespace Barotrauma 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); } - GameMain.Instance.OnResolutionChanged += () => { RescaleElements(); }; + // TODO: Needs to unregister if we ever remove GUIStyles. + GameMain.Instance.ResolutionChanged += RescaleElements; } /// diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs index 0725d9dcd..9a050d022 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs @@ -271,6 +271,8 @@ namespace Barotrauma public OnClickDelegate OnClick; } public List ClickableAreas { get; private set; } = new List(); + + public bool Shadow { get; set; } /// /// This is the new constructor. @@ -320,10 +322,10 @@ namespace Barotrauma hasColorHighlight = richTextData != null; } - public void CalculateHeightFromText(int padding = 0) + public void CalculateHeightFromText(int padding = 0, bool removeExtraSpacing = false) { if (wrappedText == null) { return; } - RectTransform.Resize(new Point(RectTransform.Rect.Width, (int)Font.MeasureString(wrappedText).Y + padding)); + RectTransform.Resize(new Point(RectTransform.Rect.Width, (int)Font.MeasureString(wrappedText, removeExtraSpacing).Y + padding)); } public override void ApplyStyle(GUIComponentStyle componentStyle) @@ -443,8 +445,8 @@ namespace Barotrauma protected override void SetAlpha(float a) { - base.SetAlpha(a); - textColor = new Color(textColor.R, textColor.G, textColor.B, a); + // base.SetAlpha(a); + textColor = new Color(TextColor.R / 255.0f, TextColor.G / 255.0f, TextColor.B / 255.0f, a); } /// @@ -626,12 +628,17 @@ namespace Barotrauma if (!hasColorHighlight) { - Font.DrawString(spriteBatch, - Censor ? censoredText : (Wrap ? wrappedText : text), - pos, - currentTextColor * (currentTextColor.A / 255.0f), - 0.0f, origin, TextScale, - SpriteEffects.None, textDepth); + string textToShow = Censor ? censoredText : (Wrap ? wrappedText : text); + 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, colorToShow, 0.0f, origin, TextScale, SpriteEffects.None, textDepth); + } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index 9be942058..30c29ef1c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -134,6 +134,10 @@ namespace Barotrauma { textBlock.OverflowClip = value != null; maxTextLength = value; + if (Text.Length > MaxTextLength) + { + SetText(textBlock.Text.Substring(0, (int)maxTextLength)); + } } } @@ -360,6 +364,7 @@ namespace Barotrauma } else { + CaretIndex = Math.Min(CaretIndex, textDrawn.Length); textDrawn = Censor ? textBlock.CensoredText : textBlock.Text; Vector2 textSize = Font.MeasureString(textDrawn.Substring(0, CaretIndex)); caretPos = new Vector2(textSize.X, 0) + textBlock.TextPos - textBlock.Origin; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs index 6be9c9283..4dd955f01 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs @@ -75,6 +75,11 @@ namespace Barotrauma get; private set; } + public static Rectangle VotingArea + { + get; private set; + } + public static int Padding { get; private set; @@ -84,7 +89,7 @@ namespace Barotrauma { if (GameMain.Instance != null) { - GameMain.Instance.OnResolutionChanged += CreateAreas; + GameMain.Instance.ResolutionChanged += CreateAreas; GameMain.Config.OnHUDScaleChanged += CreateAreas; CreateAreas(); CharacterInfo.Init(); @@ -144,6 +149,13 @@ namespace Barotrauma int healthWindowY = GameMain.GraphicsHeight / 2 - healthWindowHeight / 2; HealthWindowAreaLeft = new Rectangle(healthWindowX, healthWindowY, healthWindowWidth, healthWindowHeight); + + int votingAreaWidth = (int)(400 * GUI.Scale); + int votingAreaX = GameMain.GraphicsWidth - Padding - votingAreaWidth; + int votingAreaY = Padding + ButtonAreaTop.Height; + + // Height is based on text content + VotingArea = new Rectangle(votingAreaX, votingAreaY, votingAreaWidth, 0); } public static void Draw(SpriteBatch spriteBatch) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs index 79b77f528..eb719fe7f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs @@ -165,7 +165,7 @@ namespace Barotrauma float noiseStrength = (float)PerlinNoise.CalculatePerlin(noiseT, noiseT, 0); float noiseScale = (float)PerlinNoise.CalculatePerlin(noiseT * 5.0f, noiseT * 2.0f, 0) * 4.0f; noiseSprite.DrawTiled(spriteBatch, Vector2.Zero, new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight), - startOffset: new Point(Rand.Range(0, noiseSprite.SourceRect.Width), Rand.Range(0, noiseSprite.SourceRect.Height)), + startOffset: new Vector2(Rand.Range(0.0f, noiseSprite.SourceRect.Width), Rand.Range(0.0f, noiseSprite.SourceRect.Height)), color: Color.White * noiseStrength * 0.1f, textureScale: Vector2.One * noiseScale); @@ -185,7 +185,7 @@ namespace Barotrauma if (LoadState == 100.0f) { #if DEBUG - if (GameMain.Config.AutomaticQuickStartEnabled && GameMain.FirstLoad) + if (GameMain.Config.AutomaticQuickStartEnabled || GameMain.Config.AutomaticCampaignLoadEnabled && GameMain.FirstLoad) { loadText = "QUICKSTARTING ..."; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs new file mode 100644 index 000000000..2d5cf7b96 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -0,0 +1,1089 @@ +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace Barotrauma +{ + class Store + { + private readonly CampaignUI campaignUI; + private readonly GUIComponent parentComponent; + private readonly List storeTabButtons = new List(); + private readonly List itemCategoryButtons = new List(); + private readonly Dictionary tabLists = new Dictionary(); + private readonly Dictionary tabSortingMethods = new Dictionary(); + private readonly List itemsToSell = new List(); + + private StoreTab activeTab = StoreTab.Buy; + private MapEntityCategory? selectedItemCategory; + private bool suppressBuySell; + private int buyTotal, sellTotal; + + private GUITextBlock merchantBalanceBlock; + private GUIDropDown sortingDropDown; + private GUITextBox searchBox; + private GUIListBox storeDealsList, storeBuyList, storeSellList; + + private GUIListBox shoppingCrateBuyList, shoppingCrateSellList; + private GUITextBlock shoppingCrateTotal; + private GUIButton clearAllButton, confirmButton; + + private Point resolutionWhenCreated; + private bool hadPermissions; + + private CargoManager CargoManager => campaignUI.Campaign.CargoManager; + private Location CurrentLocation => campaignUI.Campaign.Map?.CurrentLocation; + private int PlayerMoney => campaignUI.Campaign.Money; + private bool HasPermissions => campaignUI.Campaign.AllowedToManageCampaign(); + private bool IsBuying => activeTab != StoreTab.Sell; + private bool IsSelling => activeTab == StoreTab.Sell; + private GUIListBox ActiveShoppingCrateList => IsBuying ? shoppingCrateBuyList : shoppingCrateSellList; + + private enum StoreTab + { + Deals, + Buy, + Sell + } + + private enum SortingMethod + { + AlphabeticalAsc, + AlphabeticalDesc, + PriceAsc, + PriceDesc, + CategoryAsc + } + + public Store(CampaignUI campaignUI, GUIComponent parentComponent) + { + this.campaignUI = campaignUI; + this.parentComponent = parentComponent; + + hadPermissions = HasPermissions; + + CreateUI(); + + campaignUI.Campaign.Map.OnLocationChanged += UpdateLocation; + campaignUI.Campaign.CargoManager.OnItemsInBuyCrateChanged += RefreshBuying; + campaignUI.Campaign.CargoManager.OnPurchasedItemsChanged += RefreshBuying; + campaignUI.Campaign.CargoManager.OnItemsInSellCrateChanged += RefreshSelling; + campaignUI.Campaign.CargoManager.OnSoldItemsChanged += () => + { + RefreshItemsToSell(); + RefreshSelling(); + }; + } + + public void Refresh() + { + hadPermissions = HasPermissions; + RefreshBuying(); + RefreshSelling(); + } + + private void RefreshBuying() + { + RefreshShoppingCrateBuyList(); + //RefreshStoreDealsList(); + RefreshStoreBuyList(); + var hasPermissions = HasPermissions; + //storeDealsList.Enabled = hasPermissions; + storeBuyList.Enabled = hasPermissions; + shoppingCrateBuyList.Enabled = hasPermissions; + } + + private void RefreshSelling() + { + RefreshShoppingCrateSellList(); + RefreshStoreSellList(); + var hasPermissions = HasPermissions; + storeSellList.Enabled = hasPermissions; + shoppingCrateSellList.Enabled = hasPermissions; + } + + private void CreateUI() + { + if (parentComponent.FindChild(c => c.UserData as string == "glow") is GUIComponent glowChild) + { + parentComponent.RemoveChild(glowChild); + } + if (parentComponent.FindChild(c => c.UserData as string == "container") is GUIComponent containerChild) + { + parentComponent.RemoveChild(containerChild); + } + + new GUIFrame(new RectTransform(new Vector2(1.25f, 1.25f), parentComponent.RectTransform, Anchor.Center), style: "OuterGlow", color: Color.Black * 0.7f) + { + CanBeFocused = false, + UserData = "glow" + }; + new GUIFrame(new RectTransform(new Vector2(0.95f), parentComponent.RectTransform, anchor: Anchor.Center), style: null) + { + CanBeFocused = false, + UserData = "container" + }; + + var panelMaxWidth = (int)(GUI.xScale * (GUI.HorizontalAspectRatio < 1.4f ? 650 : 560)); + var storeContent = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 1.0f), campaignUI.GetTabContainer(CampaignMode.InteractionType.Store).RectTransform) + { + MaxSize = new Point(panelMaxWidth, campaignUI.GetTabContainer(CampaignMode.InteractionType.Store).Rect.Height) + }) + { + Stretch = true, + RelativeSpacing = 0.01f + }; + + // Store header ------------------------------------------------ + var headerGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.75f / 14.0f), storeContent.RectTransform), isHorizontal: true) + { + RelativeSpacing = 0.005f + }; + 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) + { + CanBeFocused = false, + ForceUpperCase = true + }; + + // Merchant balance ------------------------------------------------ + var merchantBalanceContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.75f / 14.0f), storeContent.RectTransform)) + { + 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) + { + AutoScaleVertical = true, + ForceUpperCase = true + }; + merchantBalanceBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), merchantBalanceContainer.RectTransform), + "", font: GUI.SubHeadingFont, textAlignment: Alignment.TopLeft) + { + AutoScaleVertical = true, + TextScale = 1.1f, + TextGetter = () => + { + var balance = CurrentLocation != null ? CurrentLocation.StoreCurrentBalance : 0; + if (balance < (int)(0.25f * Location.StoreInitialBalance)) + { + merchantBalanceBlock.TextColor = Color.Red; + } + else if (balance < (int)(0.5f * Location.StoreInitialBalance)) + { + merchantBalanceBlock.TextColor = Color.Orange; + } + else + { + merchantBalanceBlock.TextColor = Color.White; + } + return GetCurrencyFormatted(balance); + } + }; + + // Store mode buttons ------------------------------------------------ + var modeButtonFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.6f / 14.0f), storeContent.RectTransform), style: null); + var modeButtonContainer = new GUILayoutGroup(new RectTransform(Vector2.One, modeButtonFrame.RectTransform), isHorizontal: true); + + var tabs = Enum.GetValues(typeof(StoreTab)); + storeTabButtons.Clear(); + tabSortingMethods.Clear(); + foreach (StoreTab tab in tabs) + { + // TODO: Remove the row below once the deal page is implemented + if (tab == StoreTab.Deals) { continue; } + var tabButton = new GUIButton(new RectTransform(new Vector2(1.0f / (tabs.Length + 1), 1.0f), modeButtonContainer.RectTransform), + text: TextManager.Get("campaignstoretab." + tab), style: "GUITabButton") + { + UserData = tab, + OnClicked = (button, userData) => + { + ChangeStoreTab((StoreTab)userData); + return true; + } + }; + storeTabButtons.Add(tabButton); + tabSortingMethods.Add(tab, SortingMethod.AlphabeticalAsc); + } + + var storeInventoryContainer = new GUILayoutGroup( + new RectTransform( + new Vector2(0.9f, 0.95f), + new GUIFrame(new RectTransform(new Vector2(1.0f, 11.9f / 14.0f), storeContent.RectTransform)).RectTransform, + anchor: Anchor.Center), + isHorizontal: true) + { + RelativeSpacing = 0.015f, + Stretch = true + }; + + // Item category buttons ------------------------------------------------ + var categoryButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.08f, 1.0f), storeInventoryContainer.RectTransform)) + { + RelativeSpacing = 0.02f + }; + + List itemCategories = Enum.GetValues(typeof(MapEntityCategory)).Cast().ToList(); + //don't show categories with no buyable items + itemCategories.RemoveAll(c => !ItemPrefab.Prefabs.Any(ep => ep.Category.HasFlag(c) && ep.CanBeBought)); + itemCategoryButtons.Clear(); + foreach (MapEntityCategory category in itemCategories) + { + var categoryButton = new GUIButton(new RectTransform(new Point(categoryButtonContainer.Rect.Width, categoryButtonContainer.Rect.Width), categoryButtonContainer.RectTransform), + style: "CategoryButton." + category) + { + ToolTip = TextManager.Get("MapEntityCategory." + category), + UserData = category, + OnClicked = (btn, userdata) => + { + MapEntityCategory? newCategory = !btn.Selected ? (MapEntityCategory?)userdata : null; + if (newCategory.HasValue) { searchBox.Text = ""; } + if (newCategory != selectedItemCategory) { tabLists[activeTab].ScrollBar.BarScroll = 0f; } + FilterStoreItems(newCategory, searchBox.Text); + return true; + } + }; + itemCategoryButtons.Add(categoryButton); + categoryButton.RectTransform.SizeChanged += () => + { + var sprite = categoryButton.Frame.sprites[GUIComponent.ComponentState.None].First(); + categoryButton.RectTransform.NonScaledSize = + new Point(categoryButton.Rect.Width, (int)(categoryButton.Rect.Width * ((float)sprite.Sprite.SourceRect.Height / sprite.Sprite.SourceRect.Width))); + }; + } + + GUILayoutGroup sortFilterListContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.92f, 1.0f), storeInventoryContainer.RectTransform)) + { + RelativeSpacing = 0.015f, + Stretch = true + }; + GUILayoutGroup sortFilterGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.08f), sortFilterListContainer.RectTransform), isHorizontal: true) + { + RelativeSpacing = 0.015f, + Stretch = true + }; + + GUILayoutGroup sortGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), sortFilterGroup.RectTransform)) + { + Stretch = true + }; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), sortGroup.RectTransform), text: TextManager.Get("campaignstore.sortby")); + sortingDropDown = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.5f), sortGroup.RectTransform), text: TextManager.Get("campaignstore.sortby"), elementCount: 3) + { + OnSelected = (child, userData) => + { + SortActiveTabItems((SortingMethod)userData); + return true; + } + }; + var tag = "sortingmethod."; + sortingDropDown.AddItem(TextManager.Get(tag + SortingMethod.AlphabeticalAsc), userData: SortingMethod.AlphabeticalAsc); + sortingDropDown.AddItem(TextManager.Get(tag + SortingMethod.PriceAsc), userData: SortingMethod.PriceAsc); + sortingDropDown.AddItem(TextManager.Get(tag + SortingMethod.PriceDesc), userData: SortingMethod.PriceDesc); + + GUILayoutGroup filterGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.6f, 1.0f), sortFilterGroup.RectTransform)) + { + Stretch = true + }; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), filterGroup.RectTransform), TextManager.Get("serverlog.filter")); + searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.5f), filterGroup.RectTransform), createClearButton: true); + searchBox.OnTextChanged += (textBox, text) => { FilterStoreItems(null, text); return true; }; + + var storeItemListContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.92f), sortFilterListContainer.RectTransform), style: null); + storeDealsList = new GUIListBox(new RectTransform(Vector2.One, storeItemListContainer.RectTransform)) + { + AutoHideScrollBar = false, + Visible = false + }; + tabLists.Clear(); + tabLists.Add(StoreTab.Deals, storeDealsList); + storeBuyList = new GUIListBox(new RectTransform(Vector2.One, storeItemListContainer.RectTransform)) + { + AutoHideScrollBar = false, + Visible = false + }; + tabLists.Add(StoreTab.Buy, storeBuyList); + storeSellList = new GUIListBox(new RectTransform(Vector2.One, storeItemListContainer.RectTransform)) + { + AutoHideScrollBar = false, + Visible = false + }; + tabLists.Add(StoreTab.Sell, storeSellList); + + // Shopping Crate ------------------------------------------------------------------------------------------------------------------------------------------ + + var shoppingCrateContent = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 1.0f), campaignUI.GetTabContainer(CampaignMode.InteractionType.Store).RectTransform, anchor: Anchor.TopRight) + { + MaxSize = new Point(panelMaxWidth, campaignUI.GetTabContainer(CampaignMode.InteractionType.Store).Rect.Height) + }) + { + Stretch = true, + RelativeSpacing = 0.01f + }; + + // Shopping crate header ------------------------------------------------ + headerGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.75f / 14.0f), shoppingCrateContent.RectTransform), isHorizontal: true, childAnchor: Anchor.TopRight) + { + RelativeSpacing = 0.005f + }; + 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) + { + CanBeFocused = false, + ForceUpperCase = true + }; + + // Player balance ------------------------------------------------ + var playerBalanceContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.75f / 14.0f), shoppingCrateContent.RectTransform), childAnchor: Anchor.TopRight) + { + 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) + { + AutoScaleVertical = true, + ForceUpperCase = true + }; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), playerBalanceContainer.RectTransform), + "", font: GUI.SubHeadingFont, textAlignment: Alignment.TopRight) + { + AutoScaleVertical = true, + TextScale = 1.1f, + TextGetter = () => GetCurrencyFormatted(PlayerMoney) + }; + + // Divider ------------------------------------------------ + var dividerFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.6f / 14.0f), shoppingCrateContent.RectTransform), style: null); + new GUIImage(new RectTransform(Vector2.One, dividerFrame.RectTransform, anchor: Anchor.BottomCenter), "HorizontalLine"); + + var shoppingCrateInventoryContainer = new GUILayoutGroup( + new RectTransform( + new Vector2(0.9f, 0.95f), + new GUIFrame(new RectTransform(new Vector2(1.0f, 11.9f / 14.0f), shoppingCrateContent.RectTransform)).RectTransform, + anchor: Anchor.Center)) + { + RelativeSpacing = 0.015f, + Stretch = true + }; + var shoppingCrateListContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.85f), shoppingCrateInventoryContainer.RectTransform), style: null); + shoppingCrateBuyList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false }; + shoppingCrateSellList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false }; + + var totalContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), shoppingCrateInventoryContainer.RectTransform), isHorizontal: true) + { + Stretch = true + }; + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), totalContainer.RectTransform), TextManager.Get("campaignstore.total"), font: GUI.Font); + shoppingCrateTotal = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), totalContainer.RectTransform), "", font: GUI.SubHeadingFont, textAlignment: Alignment.Right) + { + TextScale = 1.1f + }; + + 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 + }; + SetConfirmButtonBehavior(); + clearAllButton = new GUIButton(new RectTransform(new Vector2(0.35f, 1.0f), buttonContainer.RectTransform), TextManager.Get("campaignstore.clearall")) + { + Enabled = HasPermissions, + ForceUpperCase = true, + OnClicked = (button, userData) => + { + if (!HasPermissions) { return false; } + var itemsToRemove = new List(IsBuying ? CargoManager.ItemsInBuyCrate : CargoManager.ItemsInSellCrate); + itemsToRemove.ForEach(i => ClearFromShoppingCrate(i)); + return true; + } + }; + + Refresh(); + ChangeStoreTab(activeTab); + resolutionWhenCreated = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); + } + + private void UpdateLocation(Location prevLocation, Location newLocation) + { + if (prevLocation == newLocation) { return; } + + foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) + { + if (itemPrefab.CanBeBoughtAtLocation(CurrentLocation, out PriceInfo _)) + { + ChangeStoreTab(StoreTab.Buy); + return; + } + } + } + + private void ChangeStoreTab(StoreTab tab) + { + activeTab = tab; + foreach (GUIButton tabButton in storeTabButtons) + { + tabButton.Selected = (StoreTab)tabButton.UserData == activeTab; + } + sortingDropDown.SelectItem(tabSortingMethods[tab]); + SetShoppingCrateTotalText(); + SetClearAllButtonStatus(); + SetConfirmButtonBehavior(); + SetConfirmButtonStatus(); + FilterStoreItems(); + if (tab == StoreTab.Deals) + { + storeBuyList.Visible = false; + storeSellList.Visible = false; + storeDealsList.Visible = true; + shoppingCrateSellList.Visible = false; + shoppingCrateBuyList.Visible = true; + } + else if (tab == StoreTab.Buy) + { + storeDealsList.Visible = false; + storeSellList.Visible = false; + storeBuyList.Visible = true; + shoppingCrateSellList.Visible = false; + shoppingCrateBuyList.Visible = true; + } + else if (tab == StoreTab.Sell) + { + storeDealsList.Visible = false; + storeBuyList.Visible = false; + storeSellList.Visible = true; + shoppingCrateBuyList.Visible = false; + shoppingCrateSellList.Visible = true; + } + } + + private void FilterStoreItems(MapEntityCategory? category, string filter) + { + selectedItemCategory = category; + var list = tabLists[activeTab]; + filter = filter?.ToLower(); + foreach (GUIComponent child in list.Content.Children) + { + var item = child.UserData as PurchasedItem; + if (item?.ItemPrefab?.Name == null) { continue; } + child.Visible = + (IsBuying || item.Quantity > 0) && + (!category.HasValue || item.ItemPrefab.Category.HasFlag(category.Value)) && + (string.IsNullOrEmpty(filter) || item.ItemPrefab.Name.ToLower().Contains(filter)); + } + foreach (GUIButton btn in itemCategoryButtons) + { + btn.Selected = category.HasValue && (MapEntityCategory)btn.UserData == selectedItemCategory; + } + list.UpdateScrollBarSize(); + } + + private void FilterStoreItems() + { + //only select a specific category if the search box is empty (items from all categories are shown when searching) + MapEntityCategory? category = string.IsNullOrEmpty(searchBox.Text) ? selectedItemCategory : null; + FilterStoreItems(category, searchBox.Text); + } + + private void RefreshStoreBuyList() + { + float prevBuyListScroll = storeBuyList.BarScroll; + float prevShoppingCrateScroll = shoppingCrateBuyList.BarScroll; + + bool hasPermissions = HasPermissions; + HashSet existingItemFrames = new HashSet(); + foreach (PurchasedItem item in CurrentLocation.StoreStock) + { + if (item.ItemPrefab.CanBeBoughtAtLocation(CurrentLocation, out PriceInfo priceInfo)) + { + var itemFrame = storeBuyList.Content.Children.FirstOrDefault(c => c.UserData is PurchasedItem pi && pi.ItemPrefab == item.ItemPrefab); + var quantity = item.Quantity; + if (CargoManager.PurchasedItems.Find(i => i.ItemPrefab == item.ItemPrefab) is PurchasedItem purchasedItem) + { + quantity = Math.Max(quantity - purchasedItem.Quantity, 0); + } + if (CargoManager.ItemsInBuyCrate.Find(i => i.ItemPrefab == item.ItemPrefab) is PurchasedItem itemInBuyCrate) + { + quantity = Math.Max(quantity - itemInBuyCrate.Quantity, 0); + } + if (itemFrame == null) + { + itemFrame = CreateItemFrame(new PurchasedItem(item.ItemPrefab, quantity), priceInfo, storeBuyList, forceDisable: !hasPermissions); + } + else + { + (itemFrame.UserData as PurchasedItem).Quantity = quantity; + SetQuantityLabelText(StoreTab.Buy, itemFrame); + SetItemFrameStatus(itemFrame, hasPermissions && quantity > 0); + } + existingItemFrames.Add(itemFrame); + } + } + + var removedItemFrames = storeBuyList.Content.Children.Except(existingItemFrames).ToList(); + removedItemFrames.ForEach(f => storeBuyList.Content.RemoveChild(f)); + if (IsBuying) { FilterStoreItems(); } + SortItems(StoreTab.Buy); + + storeBuyList.BarScroll = prevBuyListScroll; + shoppingCrateBuyList.BarScroll = prevShoppingCrateScroll; + } + + private void RefreshStoreSellList() + { + float prevSellListScroll = storeSellList.BarScroll; + float prevShoppingCrateScroll = shoppingCrateSellList.BarScroll; + + bool hasPermissions = HasPermissions; + HashSet existingItemFrames = new HashSet(); + foreach (PurchasedItem item in itemsToSell) + { + PriceInfo priceInfo = item.ItemPrefab.GetPriceInfo(CurrentLocation); + if (priceInfo == null) { continue; } + var itemFrame = storeSellList.Content.FindChild(c => c.UserData is PurchasedItem i && i.ItemPrefab == item.ItemPrefab); + var quantity = item.Quantity; + if (CargoManager.ItemsInSellCrate.Find(i => i.ItemPrefab == item.ItemPrefab) is PurchasedItem itemInSellCrate) + { + quantity = Math.Max(quantity - itemInSellCrate.Quantity, 0); + } + if (itemFrame == null) + { + itemFrame = CreateItemFrame(new PurchasedItem(item.ItemPrefab, quantity), priceInfo, storeSellList, forceDisable: !hasPermissions); + } + else + { + (itemFrame.UserData as PurchasedItem).Quantity = quantity; + SetQuantityLabelText(StoreTab.Sell, itemFrame); + SetItemFrameStatus(itemFrame, hasPermissions); + } + if (quantity < 1) { itemFrame.Visible = false; } + existingItemFrames.Add(itemFrame); + } + + var removedItemFrames = storeSellList.Content.Children.Except(existingItemFrames).ToList(); + removedItemFrames.ForEach(f => storeSellList.Content.RemoveChild(f)); + if (IsSelling) { FilterStoreItems(); } + SortItems(StoreTab.Sell); + + storeSellList.BarScroll = prevSellListScroll; + shoppingCrateSellList.BarScroll = prevShoppingCrateScroll; + } + + public void RefreshItemsToSell() + { + itemsToSell.Clear(); + var playerItems = CargoManager.GetSellableItems(Character.Controlled); + foreach (Item playerItem in playerItems) + { + if (itemsToSell.FirstOrDefault(i => i.ItemPrefab == playerItem.Prefab) is PurchasedItem item) + { + item.Quantity += 1; + } + else if (playerItem.Prefab.GetPriceInfo(CurrentLocation) != 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); + 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); + } + } + } + + private void RefreshShoppingCrateList(List items, GUIListBox listBox) + { + bool hasPermissions = HasPermissions; + HashSet existingItemFrames = new HashSet(); + int totalPrice = 0; + foreach (PurchasedItem item in items) + { + PriceInfo priceInfo = item.ItemPrefab.GetPriceInfo(CurrentLocation); + if (priceInfo == null) { continue; } + + var itemFrame = listBox.Content.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab.Identifier == item.ItemPrefab.Identifier); + GUINumberInput numInput = null; + if (itemFrame == null) + { + itemFrame = CreateItemFrame(item, priceInfo, listBox, forceDisable: !hasPermissions); + numInput = itemFrame.FindChild(c => c is GUINumberInput, recursive: true) as GUINumberInput; + } + else + { + itemFrame.UserData = item; + numInput = itemFrame.FindChild(c => c is GUINumberInput, recursive: true) as GUINumberInput; + if (numInput != null) + { + numInput.UserData = item; + numInput.Enabled = hasPermissions; + } + SetItemFrameStatus(itemFrame, hasPermissions); + } + existingItemFrames.Add(itemFrame); + + suppressBuySell = true; + if (numInput != null) + { + if (numInput.IntValue != item.Quantity) { itemFrame.Flash(GUI.Style.Green); } + numInput.IntValue = item.Quantity; + } + suppressBuySell = false; + + if (priceInfo != null) + { + var price = listBox == shoppingCrateBuyList ? + CurrentLocation.GetAdjustedItemBuyPrice(priceInfo) : + CurrentLocation.GetAdjustedItemSellPrice(priceInfo); + totalPrice += item.Quantity * price; + } + } + + var removedItemFrames = listBox.Content.Children.Except(existingItemFrames).ToList(); + removedItemFrames.ForEach(f => listBox.Content.RemoveChild(f)); + + SortItems(listBox, SortingMethod.CategoryAsc); + listBox.UpdateScrollBarSize(); + if (listBox == shoppingCrateBuyList) + { + buyTotal = totalPrice; + if (IsBuying) { SetShoppingCrateTotalText(); } + } + else + { + sellTotal = totalPrice; + if(IsSelling) { SetShoppingCrateTotalText(); } + } + SetClearAllButtonStatus(); + SetConfirmButtonStatus(); + } + + private void RefreshShoppingCrateBuyList() => RefreshShoppingCrateList(CargoManager.ItemsInBuyCrate, shoppingCrateBuyList); + + private void RefreshShoppingCrateSellList() => RefreshShoppingCrateList(CargoManager.ItemsInSellCrate, shoppingCrateSellList); + + private void SortItems(GUIListBox list, SortingMethod sortingMethod) + { + if (sortingMethod == SortingMethod.AlphabeticalAsc || sortingMethod == SortingMethod.AlphabeticalDesc) + { + list.Content.RectTransform.SortChildren( + (x, y) => (x.GUIComponent.UserData as PurchasedItem).ItemPrefab.Name.CompareTo((y.GUIComponent.UserData as PurchasedItem).ItemPrefab.Name)); + if (sortingMethod == SortingMethod.AlphabeticalDesc) { list.Content.RectTransform.ReverseChildren(); } + } + else if (sortingMethod == SortingMethod.PriceAsc || sortingMethod == SortingMethod.PriceDesc) + { + SortItems(list, SortingMethod.AlphabeticalAsc); + if (list == storeSellList || list == shoppingCrateSellList) + { + list.Content.RectTransform.SortChildren( + (x, y) => CurrentLocation.GetAdjustedItemSellPrice((x.GUIComponent.UserData as PurchasedItem).ItemPrefab).CompareTo( + CurrentLocation.GetAdjustedItemSellPrice((y.GUIComponent.UserData as PurchasedItem).ItemPrefab))); + } + else + { + list.Content.RectTransform.SortChildren( + (x, y) => CurrentLocation.GetAdjustedItemBuyPrice((x.GUIComponent.UserData as PurchasedItem).ItemPrefab).CompareTo( + CurrentLocation.GetAdjustedItemBuyPrice((y.GUIComponent.UserData as PurchasedItem).ItemPrefab))); + } + if (sortingMethod == SortingMethod.PriceDesc) { list.Content.RectTransform.ReverseChildren(); } + } + else if (sortingMethod == SortingMethod.CategoryAsc) + { + SortItems(list, SortingMethod.AlphabeticalAsc); + list.Content.RectTransform.SortChildren((x, y) => + (x.GUIComponent.UserData as PurchasedItem).ItemPrefab.Category.CompareTo((y.GUIComponent.UserData as PurchasedItem).ItemPrefab.Category)); + } + } + + private void SortItems(StoreTab tab, SortingMethod sortingMethod) + { + tabSortingMethods[tab] = sortingMethod; + SortItems(tabLists[tab], sortingMethod); + } + + private void SortItems(StoreTab tab) => SortItems(tab, tabSortingMethods[tab]); + + private void SortActiveTabItems(SortingMethod sortingMethod) => SortItems(activeTab, sortingMethod); + + private GUIComponent CreateItemFrame(PurchasedItem pi, PriceInfo priceInfo, GUIListBox listBox, bool forceDisable = false) + { + GUIFrame frame = new GUIFrame(new RectTransform(new Point(listBox.Content.Rect.Width, (int)(GUI.yScale * 60)), parent: listBox.Content.RectTransform), style: "ListBoxElement") + { + ToolTip = pi.ItemPrefab.Description, + UserData = pi + }; + + GUILayoutGroup mainGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 1.0f), frame.RectTransform, Anchor.Center), + isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + RelativeSpacing = 0.01f, + Stretch = true + }; + + var nameAndIconRelativeWidth = 0.635f; + var iconRelativeWidth = 0.0f; + var priceAndButtonRelativeWidth = 1.0f - nameAndIconRelativeWidth; + + Sprite itemIcon = pi.ItemPrefab.InventoryIcon ?? pi.ItemPrefab.sprite; + if (itemIcon != null) + { + 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) + { + Color = (itemIcon == pi.ItemPrefab.InventoryIcon ? pi.ItemPrefab.InventoryIconColor : pi.ItemPrefab.SpriteColor) * (forceDisable ? 0.5f : 1.0f), + UserData = "icon" + }; + img.RectTransform.MaxSize = img.Rect.Size; + } + + GUILayoutGroup nameAndQuantityGroup = new GUILayoutGroup(new RectTransform(new Vector2(nameAndIconRelativeWidth - iconRelativeWidth, 1.0f), mainGroup.RectTransform)) + { + Stretch = true, + ToolTip = pi.ItemPrefab.Description + }; + GUITextBlock nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), nameAndQuantityGroup.RectTransform), + pi.ItemPrefab.Name, font: GUI.SubHeadingFont, textAlignment: Alignment.BottomLeft) + { + CanBeFocused = false, + TextColor = Color.White * (forceDisable ? 0.5f : 1.0f), + TextScale = 0.85f, + UserData = "name" + }; + GUINumberInput amountInput = null; + if (listBox == storeBuyList || listBox == storeSellList) + { + var block = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), nameAndQuantityGroup.RectTransform), + CreateQuantityLabelText(listBox == storeSellList ? StoreTab.Sell : StoreTab.Buy, pi.Quantity), font: GUI.Font, textAlignment: Alignment.TopLeft) + { + CanBeFocused = false, + TextColor = Color.White * (forceDisable ? 0.5f : 1.0f), + TextScale = 0.85f, + UserData = "quantitylabel" + }; + } + else if (listBox == shoppingCrateBuyList || listBox == shoppingCrateSellList) + { + amountInput = new GUINumberInput(new RectTransform(new Vector2(0.5f), nameAndQuantityGroup.RectTransform), GUINumberInput.NumberType.Int) + { + MinValueInt = 0, + MaxValueInt = GetMaxAvailable(pi.ItemPrefab, listBox == shoppingCrateBuyList ? StoreTab.Buy : StoreTab.Sell), + UserData = pi, + IntValue = pi.Quantity + }; + amountInput.Enabled = !forceDisable; + amountInput.TextBox.OnSelected += (sender, key) => { suppressBuySell = true; }; + amountInput.TextBox.OnDeselected += (sender, key) => { suppressBuySell = false; amountInput.OnValueChanged?.Invoke(amountInput); }; + amountInput.OnValueChanged += (numberInput) => + { + if (suppressBuySell) { return; } + PurchasedItem purchasedItem = numberInput.UserData as PurchasedItem; + if (!HasPermissions) + { + numberInput.IntValue = purchasedItem.Quantity; + return; + } + AddToShoppingCrate(purchasedItem, quantity: numberInput.IntValue - purchasedItem.Quantity); + }; + frame.HoverColor = frame.SelectedColor = Color.Transparent; + } + + var buttonRelativeWidth = (0.9f * mainGroup.Rect.Height) / mainGroup.Rect.Width; + + var priceBlock = new GUITextBlock(new RectTransform(new Vector2(priceAndButtonRelativeWidth - buttonRelativeWidth, 1.0f), mainGroup.RectTransform), "", font: GUI.SubHeadingFont, textAlignment: Alignment.Right) + { + TextColor = Color.White * (forceDisable ? 0.5f : 1.0f), + ToolTip = pi.ItemPrefab.Description, + UserData = "price" + }; + if(listBox == storeSellList || listBox == shoppingCrateSellList) + { + priceBlock.TextGetter = () => GetCurrencyFormatted(CurrentLocation.GetAdjustedItemSellPrice(priceInfo)); + } + else + { + priceBlock.TextGetter = () => GetCurrencyFormatted(CurrentLocation.GetAdjustedItemBuyPrice(priceInfo)); + } + + if (listBox == storeDealsList || listBox == storeBuyList || listBox == storeSellList) + { + new GUIButton(new RectTransform(new Vector2(buttonRelativeWidth, 0.9f), mainGroup.RectTransform), style: "StoreAddToCrateButton") + { + Enabled = !forceDisable && pi.Quantity > 0, + ForceUpperCase = true, + UserData = "addbutton", + OnClicked = (button, userData) => AddToShoppingCrate(pi) + }; + } + else + { + new GUIButton(new RectTransform(new Vector2(buttonRelativeWidth, 0.9f), mainGroup.RectTransform), style: "StoreRemoveFromCrateButton") + { + Enabled = !forceDisable, + ForceUpperCase = true, + UserData = "removebutton", + OnClicked = (button, userData) => ClearFromShoppingCrate(pi) + }; + } + + listBox.RecalculateChildren(); + mainGroup.Recalculate(); + mainGroup.RectTransform.RecalculateChildren(true, true); + amountInput?.LayoutGroup.Recalculate(); + nameBlock.Text = ToolBox.LimitString(nameBlock.Text, nameBlock.Font, nameBlock.Rect.Width); + mainGroup.RectTransform.Children.ForEach(c => c.IsFixedSize = true); + + return frame; + } + + private void SetItemFrameStatus(GUIComponent itemFrame, bool enabled) + { + if (itemFrame == null || !(itemFrame.UserData is PurchasedItem pi)) { return; } + + if (itemFrame.FindChild("icon", recursive: true) is GUIImage icon) + { + if (pi.ItemPrefab?.InventoryIcon != null) + { + icon.Color = pi.ItemPrefab.InventoryIconColor * (enabled ? 1.0f: 0.5f); + } + else if (pi.ItemPrefab?.sprite != null) + { + icon.Color = pi.ItemPrefab.SpriteColor * (enabled ? 1.0f : 0.5f); + } + }; + + var color = Color.White * (enabled ? 1.0f : 0.5f); + + if (itemFrame.FindChild("name", recursive: true) is GUITextBlock name) + { + name.TextColor = color; + } + + if (itemFrame.FindChild("quantitylabel", recursive: true) is GUITextBlock qty) + { + qty.TextColor = color; + } + else if (itemFrame.FindChild(c => c is GUINumberInput, recursive: true) is GUINumberInput numberInput) + { + numberInput.Enabled = enabled; + } + + if (itemFrame.FindChild("price", recursive: true) is GUITextBlock price) + { + price.TextColor = color; + } + + if (itemFrame.FindChild("addbutton", recursive: true) is GUIButton addButton) + { + addButton.Enabled = enabled; + } + else if (itemFrame.FindChild("removebutton", recursive: true) is GUIButton removeButton) + { + removeButton.Enabled = enabled; + } + } + + private void SetQuantityLabelText(StoreTab mode, GUIComponent itemFrame) + { + 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.Sell ? + TextManager.GetWithVariable("campaignstore.quantity", "[amount]", quantity.ToString()) : + TextManager.GetWithVariable("campaignstore.instock", "[amount]", quantity.ToString()); + + private int GetMaxAvailable(ItemPrefab itemPrefab, StoreTab mode) + { + var list = mode == StoreTab.Sell ? itemsToSell : CurrentLocation.StoreStock; + if (list.Find(i => i.ItemPrefab == itemPrefab) is PurchasedItem item) + { + if (mode != StoreTab.Sell) + { + var purchasedItem = CargoManager.PurchasedItems.Find(i => i.ItemPrefab == item.ItemPrefab); + if (purchasedItem != null) { return Math.Max(item.Quantity - purchasedItem.Quantity, 0); } + } + return item.Quantity; + } + else + { + return 0; + } + } + + private string GetCurrencyFormatted(int amount) => + TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", amount)); + + private bool ModifyBuyQuantity(PurchasedItem item, int quantity) + { + if (item == null || item.ItemPrefab == null) { return false; } + if (!HasPermissions) { return false; } + if (quantity > 0) + { + var itemInCrate = CargoManager.ItemsInBuyCrate.Find(i => i.ItemPrefab == item.ItemPrefab); + if (itemInCrate != null && itemInCrate.Quantity >= CargoManager.MaxQuantity) { return false; } + // Make sure there's enough available in the store + var totalQuantityToBuy = itemInCrate != null ? itemInCrate.Quantity + quantity : quantity; + if (totalQuantityToBuy > GetMaxAvailable(item.ItemPrefab, StoreTab.Buy)) { return false; } + } + CargoManager.ModifyItemQuantityInBuyCrate(item.ItemPrefab, quantity); + GameMain.Client?.SendCampaignState(); + return false; + } + + private bool ModifySellQuantity(PurchasedItem item, int quantity) + { + if (item == null || item.ItemPrefab == null) { return false; } + if (!HasPermissions) { return false; } + if (quantity > 0) + { + // Make sure there's enough available to sell + var itemToSell = CargoManager.ItemsInSellCrate.Find(i => i.ItemPrefab == item.ItemPrefab); + var totalQuantityToSell = itemToSell != null ? itemToSell.Quantity + quantity : quantity; + if (totalQuantityToSell > GetMaxAvailable(item.ItemPrefab, StoreTab.Sell)) { return false; } + } + CargoManager.ModifyItemQuantityInSellCrate(item.ItemPrefab, quantity); + //GameMain.Client?.SendCampaignState(); + return false; + } + + private bool AddToShoppingCrate(PurchasedItem item, int quantity = 1) => IsBuying ? + ModifyBuyQuantity(item, quantity) : ModifySellQuantity(item, quantity); + + private bool ClearFromShoppingCrate(PurchasedItem item) => IsBuying ? + ModifyBuyQuantity(item, -item.Quantity) : ModifySellQuantity(item, -item.Quantity); + + private bool BuyItems() + { + if (!HasPermissions) { return false; } + + var itemsToPurchase = new List(CargoManager.ItemsInBuyCrate); + var itemsToRemove = new List(); + var totalPrice = 0; + foreach (PurchasedItem item in itemsToPurchase) + { + if (item?.ItemPrefab == null || !item.ItemPrefab.CanBeBoughtAtLocation(CurrentLocation, out PriceInfo priceInfo)) + { + itemsToRemove.Add(item); + continue; + } + totalPrice += item.Quantity * CurrentLocation.GetAdjustedItemBuyPrice(priceInfo); + } + itemsToRemove.ForEach(i => itemsToPurchase.Remove(i)); + + if (itemsToPurchase.None() || totalPrice > PlayerMoney) { return false; } + + CargoManager.PurchaseItems(itemsToPurchase, true); + GameMain.Client?.SendCampaignState(); + + var dialog = new GUIMessageBox( + TextManager.Get("newsupplies"), + TextManager.GetWithVariable("suppliespurchasedmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.Name), + new string[] { TextManager.Get("Ok") }); + dialog.Buttons[0].OnClicked += dialog.Close; + + return false; + } + + private bool SellItems() + { + if (!HasPermissions) { return false; } + + var itemsToSell = new List(CargoManager.ItemsInSellCrate); + var itemsToRemove = new List(); + var totalValue = 0; + foreach (PurchasedItem item in itemsToSell) + { + if (item?.ItemPrefab == null) + { + itemsToRemove.Add(item); + continue; + } + if (item.ItemPrefab.GetPriceInfo(CurrentLocation) is PriceInfo priceInfo) + { + totalValue += item.Quantity * CurrentLocation.GetAdjustedItemSellPrice(priceInfo); + } + else + { + itemsToRemove.Add(item); + } + } + itemsToRemove.ForEach(i => itemsToSell.Remove(i)); + + if (itemsToSell.None() || totalValue > CurrentLocation.StoreCurrentBalance) { return false; } + + CargoManager.SellItems(itemsToSell); + GameMain.Client?.SendCampaignState(); + + return false; + } + + private void SetShoppingCrateTotalText() + { + if (IsBuying) + { + shoppingCrateTotal.Text = GetCurrencyFormatted(buyTotal); + shoppingCrateTotal.TextColor = buyTotal > PlayerMoney ? Color.Red : Color.White; + } + else + { + shoppingCrateTotal.Text = GetCurrencyFormatted(sellTotal); + shoppingCrateTotal.TextColor = CurrentLocation != null && sellTotal > CurrentLocation.StoreCurrentBalance ? Color.Red : Color.White; + } + } + + private void SetConfirmButtonBehavior() + { + if (IsBuying) + { + confirmButton.Text = TextManager.Get("CampaignStore.Purchase"); + confirmButton.OnClicked = (b, o) => BuyItems(); + } + else + { + confirmButton.Text = TextManager.Get("CampaignStoreTab.Sell"); + confirmButton.OnClicked = (b, o) => + { + var confirmDialog = new GUIMessageBox( + TextManager.Get("FireWarningHeader"), + TextManager.Get("CampaignStore.SellWarningText"), + new string[] { 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; + return true; + }; + } + } + + private void SetConfirmButtonStatus() => confirmButton.Enabled = + HasPermissions && ActiveShoppingCrateList.Content.RectTransform.Children.Any() && + ((IsBuying && buyTotal <= PlayerMoney) || (IsSelling && CurrentLocation != null && sellTotal <= CurrentLocation.StoreCurrentBalance)); + + private void SetClearAllButtonStatus() => clearAllButton.Enabled = + HasPermissions && ActiveShoppingCrateList.Content.RectTransform.Children.Any(); + + public void Update() + { + if (GameMain.GraphicsWidth != resolutionWhenCreated.X || GameMain.GraphicsHeight != resolutionWhenCreated.Y) + { + CreateUI(); + } + else if (hadPermissions != HasPermissions) + { + Refresh(); + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs new file mode 100644 index 000000000..7f80e758f --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -0,0 +1,683 @@ +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using System.Linq; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; + +namespace Barotrauma +{ + class SubmarineSelection + { + private const int submarinesPerPage = 4; + private int currentPage = 1; + private int pageCount; + private bool transferService, purchaseService, initialized; + private int deliveryFee; + private string deliveryLocationName; + + public GUIFrame GuiFrame; + private GUIFrame pageIndicatorHolder; + private GUICustomComponent selectedSubmarineIndicator; + private GUILayoutGroup submarineHorizontalGroup, submarineControlsGroup; + private GUIButton browseLeftButton, browseRightButton, confirmButton, confirmButtonAlt; + private GUIListBox specsFrame; + private GUIImage[] pageIndicators; + private GUITextBlock descriptionTextBlock; + private int selectionIndicatorThickness; + private GUIImage listBackground; + + private List subsToShow; + private SubmarineDisplayContent[] submarineDisplays = new SubmarineDisplayContent[submarinesPerPage]; + private SubmarineInfo selectedSubmarine = null; + private string purchaseAndSwitchText, purchaseOnlyText, deliveryText, currentSubText, deliveryFeeText, priceText, switchText, missingPreviewText, currencyShorthandText, currencyLongText; + private RectTransform parent; + private 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 string[] messageBoxOptions; + + public const int DeliveryFeePerDistanceTravelled = 1000; + public static bool ContentRefreshRequired = false; + + private static readonly Color indicatorColor = new Color(112, 149, 129); + private Point createdForResolution; + + private struct SubmarineDisplayContent + { + public GUIFrame background; + public GUIImage submarineImage; + public SubmarineInfo displayedSubmarine; + public GUITextBlock submarineName; + public GUITextBlock submarineClass; + public GUITextBlock submarineFee; + public GUIButton selectSubmarineButton; + public GUITextBlock middleTextBlock; + } + + public SubmarineSelection(bool transfer, Action closeAction, RectTransform parent) + { + if (GameMain.GameSession.Campaign == null) return; + + transferService = transfer; + purchaseService = !transfer; + this.parent = parent; + this.closeAction = closeAction; + + subsToShow = new List(); + + if (GameMain.Client == null) + { + messageBoxOptions = new string[2] { TextManager.Get("Yes"), TextManager.Get("Cancel") }; + } + else + { + messageBoxOptions = new string[2] { TextManager.Get("Yes") + " " + TextManager.Get("initiatevoting"), TextManager.Get("Cancel") }; + } + + if (Submarine.MainSub?.Info == null) return; + Initialize(); + } + + private void Initialize() + { + initialized = true; + 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"); + } + + currencyShorthandText = TextManager.Get("currencyformat"); + currencyLongText = TextManager.Get("credit").ToLower(); + + UpdateSubmarines(); + missingPreviewText = TextManager.Get("SubPreviewImageNotFound"); + CreateGUI(); + } + + private int CalculateDeliveryFee() + { + int distanceToOutpost = GameMain.GameSession.Map.DistanceToClosestLocationWithOutpost(GameMain.GameSession.Map.CurrentLocation, out Location endLocation); + deliveryLocationName = endLocation.Name; + return DeliveryFeePerDistanceTravelled * distanceToOutpost; + } + + private void CreateGUI() + { + createdForResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); + + GUILayoutGroup content; + GuiFrame = new GUIFrame(new RectTransform(new Vector2(0.75f, 0.7f), parent, Anchor.TopCenter, Pivot.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.02f) }); + selectionIndicatorThickness = HUDLayoutSettings.Padding / 2; + + GUIFrame background = new GUIFrame(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center), color: Color.Black * 0.9f) + { + CanBeFocused = false + }; + + 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); + header.CalculateHeightFromText(0, true); + GUITextBlock credits = new GUITextBlock(new RectTransform(Vector2.One, header.RectTransform), "", font: GUI.SubHeadingFont, textAlignment: Alignment.CenterRight) + { + TextGetter = CampaignUI.GetMoney + }; + + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), content.RectTransform), style: "HorizontalLine"); + + GUILayoutGroup submarineContentGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.4f), content.RectTransform)) { AbsoluteSpacing = HUDLayoutSettings.Padding, Stretch = true }; + submarineHorizontalGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.9f), submarineContentGroup.RectTransform)) { IsHorizontal = true, AbsoluteSpacing = HUDLayoutSettings.Padding, Stretch = true }; + + submarineControlsGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), submarineContentGroup.RectTransform), true, Anchor.TopCenter); + + GUILayoutGroup infoFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.4f), content.RectTransform)) { IsHorizontal = true, Stretch = true, AbsoluteSpacing = HUDLayoutSettings.Padding }; + new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform), style: null, new Color(8, 13, 19)) { IgnoreLayoutGroups = true }; + listBackground = new GUIImage(new RectTransform(new Vector2(0.59f, 1f), infoFrame.RectTransform, Anchor.CenterRight), style: null, true) + { + IgnoreLayoutGroups = true + }; + new GUIListBox(new RectTransform(Vector2.One, infoFrame.RectTransform)) { IgnoreLayoutGroups = true, CanBeFocused = false }; + 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 }; + + GUILayoutGroup buttonFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.075f), content.RectTransform), childAnchor: Anchor.CenterRight) { IsHorizontal = true, AbsoluteSpacing = HUDLayoutSettings.Padding }; + + if (closeAction != null) + { + GUIButton closeButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), buttonFrame.RectTransform), TextManager.Get("Close"), style: "GUIButtonFreeScale") + { + OnClicked = (button, userData) => + { + closeAction(); + return true; + } + }; + } + + if (purchaseService) confirmButtonAlt = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), buttonFrame.RectTransform), purchaseOnlyText, style: "GUIButtonFreeScale"); + confirmButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), buttonFrame.RectTransform), purchaseService ? purchaseAndSwitchText : deliveryFee > 0 ? deliveryText : switchText, style: "GUIButtonFreeScale"); + SetConfirmButtonState(false); + + pageIndicatorHolder = new GUIFrame(new RectTransform(new Vector2(1f, 1.5f), submarineControlsGroup.RectTransform), style: null); + pageIndicator = GUI.Style.GetComponentStyle("GUIPageIndicator").GetDefaultSprite(); + UpdatePaging(); + + for (int i = 0; i < submarineDisplays.Length; i++) + { + SubmarineDisplayContent submarineDisplayElement = new SubmarineDisplayContent(); + submarineDisplayElement.background = new GUIFrame(new RectTransform(new Vector2(1f / submarinesPerPage, 1f), submarineHorizontalGroup.RectTransform), style: null, new Color(8, 13, 19)); + 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.selectSubmarineButton = new GUIButton(new RectTransform(Vector2.One, submarineDisplayElement.background.RectTransform), style: null); + submarineDisplays[i] = submarineDisplayElement; + } + + selectedSubmarineIndicator = new GUICustomComponent(new RectTransform(Point.Zero, submarineHorizontalGroup.RectTransform), onDraw: (sb, component) => DrawSubmarineIndicator(sb, component.Rect)) { IgnoreLayoutGroups = true, CanBeFocused = false }; + } + + private void UpdatePaging() + { + if (pageIndicatorHolder == null) return; + pageIndicatorHolder.ClearChildren(); + if (currentPage > pageCount) currentPage = pageCount; + if (pageCount < 2) return; + + browseLeftButton = new GUIButton(new RectTransform(new Vector2(1.15f, 1.15f), pageIndicatorHolder.RectTransform, Anchor.CenterLeft, Pivot.CenterRight) { AbsoluteOffset = new Point(-HUDLayoutSettings.Padding * 3, 0) }, string.Empty, style: "GUIButtonToggleLeft") + { + IgnoreLayoutGroups = true, + OnClicked = (button, userData) => + { + ChangePage(-1); + return true; + } + }; + + Point indicatorSize = new Point(GUI.IntScale(pageIndicator.SourceRect.Width * 1.5f), GUI.IntScale(pageIndicator.SourceRect.Height * 1.5f)); + pageIndicatorHolder.RectTransform.NonScaledSize = new Point(pageCount * indicatorSize.X + HUDLayoutSettings.Padding * (pageCount - 1), pageIndicatorHolder.RectTransform.NonScaledSize.Y); + + int xPos = 0; + int yPos = pageIndicatorHolder.Rect.Height / 2 - indicatorSize.Y / 2; + + pageIndicators = new GUIImage[pageCount]; + for (int i = 0; i < pageCount; i++) + { + pageIndicators[i] = new GUIImage(new RectTransform(indicatorSize, pageIndicatorHolder.RectTransform) { AbsoluteOffset = new Point(xPos, yPos) }, pageIndicator, null, true); + xPos += indicatorSize.X + HUDLayoutSettings.Padding; + } + + for (int i = 0; i < pageIndicators.Length; i++) + { + pageIndicators[i].Color = i == currentPage - 1 ? Color.White : Color.Gray; + } + + browseRightButton = new GUIButton(new RectTransform(new Vector2(1.15f, 1.15f), pageIndicatorHolder.RectTransform, Anchor.CenterRight, Pivot.CenterLeft) { AbsoluteOffset = new Point(-HUDLayoutSettings.Padding * 3, 0) }, string.Empty, style: "GUIButtonToggleRight") + { + IgnoreLayoutGroups = true, + OnClicked = (button, userData) => + { + ChangePage(1); + return true; + } + }; + + browseLeftButton.Enabled = currentPage > 1; + browseRightButton.Enabled = currentPage < pageCount; + } + + private void DrawSubmarineIndicator(SpriteBatch spriteBatch, Rectangle area) + { + if (area == Rectangle.Empty) return; + GUI.DrawRectangle(spriteBatch, area, indicatorColor, thickness: selectionIndicatorThickness); + } + + public void Update() + { + if (ContentRefreshRequired) + { + RefreshSubmarineDisplay(true); + } + + // Input + if (PlayerInput.KeyHit(Keys.Left)) + { + SelectSubmarine(subsToShow.IndexOf(selectedSubmarine), -1); + } + else if (PlayerInput.KeyHit(Keys.Right)) + { + SelectSubmarine(subsToShow.IndexOf(selectedSubmarine), 1); + } + } + + public void RefreshSubmarineDisplay(bool updateSubs) + { + if (!initialized) Initialize(); + if (GameMain.GraphicsWidth != createdForResolution.X || GameMain.GraphicsHeight != createdForResolution.Y) CreateGUI(); + if (updateSubs) UpdateSubmarines(); + + if (pageIndicators != null) + { + for (int i = 0; i < pageIndicators.Length; i++) + { + pageIndicators[i].Color = i == currentPage - 1 ? Color.White : Color.Gray; + } + } + + int submarineIndex = (currentPage - 1) * submarinesPerPage; + + for (int i = 0; i < submarineDisplays.Length; i++) + { + SubmarineInfo subToDisplay = GetSubToDisplay(submarineIndex); + if (subToDisplay == null) + { + submarineDisplays[i].submarineImage.Sprite = null; + submarineDisplays[i].submarineName.Text = string.Empty; + submarineDisplays[i].submarineFee.Text = string.Empty; + submarineDisplays[i].submarineClass.Text = string.Empty; + submarineDisplays[i].selectSubmarineButton.Enabled = false; + submarineDisplays[i].selectSubmarineButton.OnClicked = null; + submarineDisplays[i].displayedSubmarine = null; + submarineDisplays[i].middleTextBlock.AutoDraw = false; + } + else + { + submarineDisplays[i].displayedSubmarine = subToDisplay; + Sprite previewImage = GetPreviewImage(subToDisplay); + + if (previewImage != null) + { + submarineDisplays[i].submarineImage.Sprite = previewImage; + submarineDisplays[i].middleTextBlock.AutoDraw = false; + } + else + { + submarineDisplays[i].submarineImage.Sprite = null; + submarineDisplays[i].middleTextBlock.Text = missingPreviewText; + submarineDisplays[i].middleTextBlock.AutoDraw = true; + } + + submarineDisplays[i].selectSubmarineButton.Enabled = true; + + int index = i; + submarineDisplays[i].selectSubmarineButton.OnClicked = (button, userData) => + { + SelectSubmarine(subToDisplay, submarineDisplays[index].background.Rect); + return true; + }; + + submarineDisplays[i].submarineName.Text = subToDisplay.DisplayName; + submarineDisplays[i].submarineClass.Text = $"{TextManager.GetWithVariable("submarineclass.classsuffixformat", "[type]", TextManager.Get($"submarineclass.{subToDisplay.SubmarineClass}"))}"; + + if (!GameMain.GameSession.IsSubmarineOwned(subToDisplay)) + { + string amountString = currencyShorthandText.Replace("[credits]", subToDisplay.Price.ToString()); + submarineDisplays[i].submarineFee.Text = priceText.Replace("[amount]", amountString).Replace("[currencyname]", string.Empty).TrimEnd(); + } + else + { + if (subToDisplay.Name != CurrentOrPendingSubmarine().Name) + { + if (deliveryFee > 0) + { + string amountString = currencyShorthandText.Replace("[credits]", deliveryFee.ToString()); + submarineDisplays[i].submarineFee.Text = deliveryFeeText.Replace("[amount]", amountString).Replace("[currencyname]", string.Empty).TrimEnd(); + } + else + { + submarineDisplays[i].submarineFee.Text = string.Empty; + } + } + else + { + submarineDisplays[i].submarineFee.Text = currentSubText; + } + } + + if (transferService && subToDisplay.Name == CurrentOrPendingSubmarine().Name && updateSubs) + { + if (selectedSubmarine == null) + { + CoroutineManager.StartCoroutine(SelectOwnSubmarineWithDelay(subToDisplay, submarineDisplays[i])); + } + else + { + SelectSubmarine(subToDisplay, submarineDisplays[i].background.Rect); + } + } + else if (!transferService && selectedSubmarine == null || !transferService && GameMain.GameSession.IsSubmarineOwned(selectedSubmarine) || subToDisplay == selectedSubmarine) + { + SelectSubmarine(subToDisplay, submarineDisplays[i].background.Rect); + } + } + + submarineIndex++; + } + + if (subsToShow.Count == 0) + { + SelectSubmarine(null, Rectangle.Empty); + } + } + + private void UpdateSubmarines() + { + subsToShow.Clear(); + if (transferService) + { + subsToShow.AddRange(GameMain.GameSession.OwnedSubmarines); + subsToShow.Sort((x, y) => x.SubmarineClass.CompareTo(y.SubmarineClass)); + string currentSubName = CurrentOrPendingSubmarine().Name; + int currentIndex = subsToShow.FindIndex(s => s.Name == currentSubName); + if (currentIndex != -1) + { + currentPage = (int)Math.Ceiling((currentIndex + 1) / (float)submarinesPerPage); + } + } + else + { + if (GameMain.Client == null) + { + subsToShow.AddRange(SubmarineInfo.SavedSubmarines.Where(s => s.IsCampaignCompatible && !GameMain.GameSession.OwnedSubmarines.Any(os => os.Name == s.Name))); + } + else + { + subsToShow.AddRange(GameMain.NetLobbyScreen.CampaignSubmarines.Where(s => !GameMain.GameSession.OwnedSubmarines.Any(os => os.Name == s.Name))); + } + + subsToShow.Sort((x, y) => x.SubmarineClass.CompareTo(y.SubmarineClass)); + } + + if (transferService) SetConfirmButtonState(selectedSubmarine != null && selectedSubmarine.Name != CurrentOrPendingSubmarine().Name); + + subsToShow.Sort((x, y) => x.SubmarineClass.CompareTo(y.SubmarineClass)); + pageCount = Math.Max(1, (int)Math.Ceiling(subsToShow.Count / (float)submarinesPerPage)); + UpdatePaging(); + ContentRefreshRequired = false; + } + + private SubmarineInfo GetSubToDisplay(int index) + { + if (subsToShow.Count <= index || index < 0) return null; + return subsToShow[index]; + } + + private Sprite GetPreviewImage(SubmarineInfo info) + { + Sprite preview = info.PreviewImage; + + if (preview == null) + { + SubmarineInfo potentialMatch; + + if (GameMain.Client == null) + { + potentialMatch = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.EqualityCheckVal == info.EqualityCheckVal); + } + else + { + potentialMatch = GameMain.NetLobbyScreen.CampaignSubmarines.FirstOrDefault(s => s.EqualityCheckVal == info.EqualityCheckVal); + } + + preview = potentialMatch?.PreviewImage; + + // Try from savedsubmarines with name comparison as a backup + if (preview == null) + { + potentialMatch = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == info.Name); + preview = potentialMatch?.PreviewImage; + } + } + + return preview; + } + + // Initial submarine selection needs a slight wait to allow the layoutgroups to place content properly + private IEnumerable SelectOwnSubmarineWithDelay(SubmarineInfo info, SubmarineDisplayContent display) + { + yield return new WaitForSeconds(0.05f); + SelectSubmarine(info, display.background.Rect); + } + + // Selection based on key input + private void SelectSubmarine(int index, int direction) + { + SubmarineInfo nextSub = GetSubToDisplay(index + direction); + if (nextSub == null) return; + + for (int i = 0; i < submarineDisplays.Length; i++) + { + if (submarineDisplays[i].displayedSubmarine == nextSub) + { + SelectSubmarine(nextSub, submarineDisplays[i].background.Rect); + return; + } + } + + ChangePage(direction); + + for (int i = 0; i < submarineDisplays.Length; i++) + { + if (submarineDisplays[i].displayedSubmarine == nextSub) + { + SelectSubmarine(nextSub, submarineDisplays[i].background.Rect); + return; + } + } + } + + private void SelectSubmarine(SubmarineInfo info, Rectangle backgroundRect) + { +#if !DEBUG + if (selectedSubmarine == info) return; +#endif + specsFrame.Content.ClearChildren(); + selectedSubmarine = info; + + if (info != null) + { + bool owned = GameMain.GameSession.IsSubmarineOwned(info); + + if (owned) + { + confirmButton.Text = deliveryFee > 0 ? deliveryText : switchText; + confirmButton.OnClicked = (button, userData) => + { + ShowTransferPrompt(); + return true; + }; + } + else + { + confirmButton.Text = purchaseAndSwitchText; + confirmButton.OnClicked = (button, userData) => + { + ShowBuyPrompt(false); + return true; + }; + + confirmButtonAlt.Text = purchaseOnlyText; + confirmButtonAlt.OnClicked = (button, userData) => + { + ShowBuyPrompt(true); + return true; + }; + } + + SetConfirmButtonState(selectedSubmarine.Name != CurrentOrPendingSubmarine().Name); + + selectedSubmarineIndicator.RectTransform.NonScaledSize = backgroundRect.Size; + selectedSubmarineIndicator.RectTransform.AbsoluteOffset = new Point(backgroundRect.Left - submarineHorizontalGroup.Rect.Left, 0); + + Sprite previewImage = GetPreviewImage(info); + listBackground.Sprite = previewImage; + listBackground.SetCrop(true); + + ScalableFont font = GUI.Font; + info.CreateSpecsWindow(specsFrame, font); + descriptionTextBlock.Text = info.Description; + descriptionTextBlock.CalculateHeightFromText(); + } + else + { + listBackground.Sprite = null; + listBackground.SetCrop(false); + descriptionTextBlock.Text = string.Empty; + selectedSubmarineIndicator.RectTransform.NonScaledSize = Point.Zero; + SetConfirmButtonState(false); + } + } + + private void SetConfirmButtonState(bool state) + { + if (confirmButtonAlt != null) + { + confirmButtonAlt.Enabled = state; + } + + if (confirmButton != null) + { + confirmButton.Enabled = state; + } + } + + public static SubmarineInfo CurrentOrPendingSubmarine() + { + if (GameMain.GameSession?.Campaign?.PendingSubmarineSwitch == null) + { + return Submarine.MainSub.Info; + } + else + { + return GameMain.GameSession.Campaign.PendingSubmarineSwitch; + } + } + + private void ChangePage(int pageChangeDirection) + { + SelectSubmarine(null, Rectangle.Empty); + if (pageChangeDirection < 0 && currentPage > 1) currentPage--; + if (pageChangeDirection > 0 && currentPage < pageCount) currentPage++; + browseLeftButton.Enabled = currentPage > 1; + browseRightButton.Enabled = currentPage < pageCount; + + RefreshSubmarineDisplay(false); + } + + private void ShowTransferPrompt() + { + 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 })); + return; + } + + GUIMessageBox msgBox; + + 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); + } + else + { + msgBox = new GUIMessageBox(TextManager.Get("switchsubmarineheader"), TextManager.GetWithVariables("switchsubmarinetext", SwitchTextVariables, + new string[2] { CurrentOrPendingSubmarine().DisplayName, selectedSubmarine.DisplayName }), messageBoxOptions); + } + + msgBox.Buttons[0].OnClicked = (applyButton, obj) => + { + if (GameMain.Client == null) + { + GameMain.GameSession.SwitchSubmarine(selectedSubmarine, deliveryFee); + GameMain.GameSession.Campaign.UpgradeManager.RefundResetAndReload(selectedSubmarine); + RefreshSubmarineDisplay(true); + } + else + { + GameMain.Client.InitiateSubmarineChange(selectedSubmarine, Networking.VoteType.SwitchSub); + } + return true; + }; + msgBox.Buttons[0].OnClicked += msgBox.Close; + msgBox.Buttons[1].OnClicked = msgBox.Close; + } + + private void ShowBuyPrompt(bool purchaseOnly) + { + if (GameMain.GameSession.Campaign.Money < selectedSubmarine.Price) + { + new GUIMessageBox(TextManager.Get("purchasesubmarineheader"), TextManager.GetWithVariables("notenoughmoneyforpurchasetext", notEnoughCreditsPurchaseTextVariables, + new string[2] { currencyLongText, selectedSubmarine.DisplayName })); + return; + } + + GUIMessageBox msgBox; + + 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.Buttons[0].OnClicked = (applyButton, obj) => + { + if (GameMain.Client == null) + { + GameMain.GameSession.PurchaseSubmarine(selectedSubmarine); + GameMain.GameSession.SwitchSubmarine(selectedSubmarine, 0); + GameMain.GameSession.Campaign.UpgradeManager.RefundResetAndReload(selectedSubmarine); + RefreshSubmarineDisplay(true); + } + else + { + GameMain.Client.InitiateSubmarineChange(selectedSubmarine, Networking.VoteType.PurchaseAndSwitchSub); + } + return true; + }; + } + else + { + msgBox = new GUIMessageBox(TextManager.Get("purchasesubmarineheader"), TextManager.GetWithVariables("purchasesubmarinetext", PurchaseTextVariables, + new string[3] { selectedSubmarine.DisplayName, selectedSubmarine.Price.ToString(), currencyLongText }), messageBoxOptions); + + msgBox.Buttons[0].OnClicked = (applyButton, obj) => + { + if (GameMain.Client == null) + { + GameMain.GameSession.PurchaseSubmarine(selectedSubmarine); + RefreshSubmarineDisplay(true); + } + else + { + GameMain.Client.InitiateSubmarineChange(selectedSubmarine, Networking.VoteType.PurchaseSub); + } + return true; + }; + } + + msgBox.Buttons[0].OnClicked += msgBox.Close; + msgBox.Buttons[1].OnClicked = msgBox.Close; + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index deea61de3..e98683d2a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System.Linq; using Barotrauma.Networking; +using System.Globalization; namespace Barotrauma { @@ -13,7 +14,7 @@ namespace Barotrauma private static bool initialized = false; - private static UISprite spectateIcon, deadIcon, disconnectedIcon; + private static UISprite spectateIcon, disconnectedIcon; private static Sprite ownerIcon, moderatorIcon; private enum InfoFrameTab { Crew, Mission, MyCharacter, Traitor }; @@ -31,7 +32,7 @@ namespace Barotrauma private List teamIDs; private const string inLobbyString = "\u2022 \u2022 \u2022"; - private static Color ownCharacterBGColor = Color.Gold * 0.7f; + public static Color OwnCharacterBGColor = Color.Gold * 0.7f; private class LinkedGUI { @@ -115,10 +116,9 @@ namespace Barotrauma public void Initialize() { spectateIcon = GUI.Style.GetComponentStyle("SpectateIcon").Sprites[GUIComponent.ComponentState.None][0]; - deadIcon = GUI.Style.GetComponentStyle("DeadIcon").Sprites[GUIComponent.ComponentState.None][0]; disconnectedIcon = GUI.Style.GetComponentStyle("DisconnectedIcon").Sprites[GUIComponent.ComponentState.None][0]; - ownerIcon = GUI.Style.GetComponentStyle("OwnerIcon").Sprites[GUIComponent.ComponentState.None][0].Sprite; - moderatorIcon = GUI.Style.GetComponentStyle("ModeratorIcon").Sprites[GUIComponent.ComponentState.None][0].Sprite; + ownerIcon = GUI.Style.GetComponentStyle("OwnerIcon").GetDefaultSprite(); + moderatorIcon = GUI.Style.GetComponentStyle("ModeratorIcon").GetDefaultSprite(); initialized = true; } @@ -279,7 +279,7 @@ namespace Barotrauma teamIDs = crew.Select(c => c.TeamID).Distinct().ToList(); // Show own team first when there's more than one team - if (teamIDs.Count > 1 && GameMain.Client.Character != null) + if (teamIDs.Count > 1 && GameMain.Client?.Character != null) { Character.TeamType ownTeam = GameMain.Client.Character.TeamID; teamIDs = teamIDs.OrderBy(i => i != ownTeam).ThenBy(i => i).ToList(); @@ -408,7 +408,7 @@ namespace Barotrauma GUIFrame frame = new GUIFrame(new RectTransform(new Point(crewListArray[i].Content.Rect.Width, GUI.IntScale(33f)), crewListArray[i].Content.RectTransform), style: "ListBoxElement") { UserData = character, - Color = (Character.Controlled == character) ? ownCharacterBGColor : Color.Transparent + Color = (Character.Controlled == character) ? OwnCharacterBGColor : Color.Transparent }; var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: true) @@ -486,7 +486,7 @@ namespace Barotrauma GUIFrame frame = new GUIFrame(new RectTransform(new Point(crewListArray[i].Content.Rect.Width, GUI.IntScale(33f)), crewListArray[i].Content.RectTransform), style: "ListBoxElement") { UserData = character, - Color = (GameMain.NetworkMember != null && GameMain.Client.Character == character) ? ownCharacterBGColor : Color.Transparent + Color = (GameMain.NetworkMember != null && GameMain.Client.Character == character) ? OwnCharacterBGColor : Color.Transparent }; frame.OnSecondaryClicked += (component, data) => @@ -631,7 +631,7 @@ namespace Barotrauma { if (GameMain.NetworkMember == null || client == null || !client.HasPermissions) return null; - if (!client.AllowKicking) // Owner cannot be kicked + if (client.IsOwner) // Owner cannot be kicked { return ownerIcon; } @@ -649,7 +649,10 @@ namespace Barotrauma } else if (client.Character != null && client.Character.IsDead) { - client.Character.Info.DrawJobIcon(spriteBatch, area); + if (client.Character.Info != null) + { + client.Character.Info.DrawJobIcon(spriteBatch, area); + } } else { @@ -780,7 +783,7 @@ namespace Barotrauma public static void StorePlayerConnectionChangeMessage(ChatMessage message) { - if (!GameMain.GameSession?.GameMode?.IsRunning ?? true) { return; } + if (!GameMain.GameSession?.IsRunning ?? true) { return; } string msg = ChatMessage.GetTimeStamp() + message.TextWithSender; storedMessages.Add(new Pair(msg, message.ChangeType)); @@ -847,15 +850,15 @@ namespace Barotrauma infoFrame.ClearChildren(); GUIFrame missionFrame = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox"); int padding = (int)(0.0245f * missionFrame.Rect.Height); - Location endLocation = GameMain.GameSession.EndLocation; - Sprite portrait = endLocation.Type.GetPortrait(endLocation.PortraitId); + Location location = GameMain.GameSession.EndLocation != null ? GameMain.GameSession.EndLocation : GameMain.GameSession.StartLocation; + Sprite portrait = location.Type.GetPortrait(location.PortraitId); bool hasPortrait = portrait != null && portrait.SourceRect.Width > 0 && portrait.SourceRect.Height > 0; int contentWidth = hasPortrait ? (int)(missionFrame.Rect.Width * 0.951f) : missionFrame.Rect.Width - padding * 2; - Vector2 locationNameSize = GUI.LargeFont.MeasureString(endLocation.Name); - Vector2 locationTypeSize = GUI.SubHeadingFont.MeasureString(endLocation.Name); - GUITextBlock locationNameText = new GUITextBlock(new RectTransform(new Point(contentWidth, (int)locationNameSize.Y), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, padding) }, endLocation.Name, font: GUI.LargeFont); - GUITextBlock locationTypeText = new GUITextBlock(new RectTransform(new Point(contentWidth, (int)locationTypeSize.Y), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, locationNameText.Rect.Height + padding) }, endLocation.Type.Name, font: GUI.SubHeadingFont); + Vector2 locationNameSize = GUI.LargeFont.MeasureString(location.Name); + Vector2 locationTypeSize = GUI.SubHeadingFont.MeasureString(location.Name); + GUITextBlock locationNameText = new GUITextBlock(new RectTransform(new Point(contentWidth, (int)locationNameSize.Y), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, padding) }, location.Name, font: GUI.LargeFont); + GUITextBlock locationTypeText = new GUITextBlock(new RectTransform(new Point(contentWidth, (int)locationTypeSize.Y), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, locationNameText.Rect.Height + padding) }, location.Type.Name, font: GUI.SubHeadingFont); int locationInfoYOffset = locationNameText.Rect.Height + locationTypeText.Rect.Height + padding * 2; @@ -881,7 +884,8 @@ namespace Barotrauma string missionNameString = ToolBox.WrapText(mission.Name, missionTextGroup.Rect.Width, GUI.LargeFont); string missionDescriptionString = ToolBox.WrapText(mission.Description, missionTextGroup.Rect.Width, GUI.Font); - string missionRewardString = ToolBox.WrapText(TextManager.GetWithVariable("MissionReward", "[reward]", mission.Reward.ToString()), missionTextGroup.Rect.Width, GUI.Font); + string rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", mission.Reward)); + string missionRewardString = ToolBox.WrapText(TextManager.GetWithVariable("MissionReward", "[reward]", rewardText), missionTextGroup.Rect.Width, GUI.Font); Vector2 missionNameSize = GUI.LargeFont.MeasureString(missionNameString); Vector2 missionDescriptionSize = GUI.Font.MeasureString(missionDescriptionString); @@ -890,13 +894,15 @@ namespace Barotrauma missionDescriptionHolder.RectTransform.NonScaledSize = new Point(missionDescriptionHolder.RectTransform.NonScaledSize.X, (int)(missionNameSize.Y + missionDescriptionSize.Y + missionRewardSize.Y)); missionTextGroup.RectTransform.NonScaledSize = new Point(missionTextGroup.RectTransform.NonScaledSize.X, missionDescriptionHolder.RectTransform.NonScaledSize.Y); - float iconAspectRatio = mission.Prefab.Icon.SourceRect.Width / mission.Prefab.Icon.SourceRect.Height; - int iconWidth = (int)(0.225f * missionDescriptionHolder.RectTransform.NonScaledSize.X); - int iconHeight = Math.Max(missionTextGroup.RectTransform.NonScaledSize.Y, (int)(iconWidth * iconAspectRatio)); - Point iconSize = new Point(iconWidth, iconHeight); - - new GUIImage(new RectTransform(iconSize, missionDescriptionHolder.RectTransform), mission.Prefab.Icon, null, true) { Color = mission.Prefab.IconColor }; + if (mission.Prefab.Icon != null) + { + float iconAspectRatio = mission.Prefab.Icon.SourceRect.Width / mission.Prefab.Icon.SourceRect.Height; + int iconWidth = (int)(0.225f * missionDescriptionHolder.RectTransform.NonScaledSize.X); + int iconHeight = Math.Max(missionTextGroup.RectTransform.NonScaledSize.Y, (int)(iconWidth * iconAspectRatio)); + Point iconSize = new Point(iconWidth, iconHeight); + new GUIImage(new RectTransform(iconSize, missionDescriptionHolder.RectTransform), mission.Prefab.Icon, null, true) { Color = mission.Prefab.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), missionRewardString); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionDescriptionString); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs index 1425432c1..a6b3ca8dc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs @@ -36,6 +36,12 @@ namespace Barotrauma get; private set; } + + public bool MaintainBorderAspectRatio + { + get; + private set; + } /// /// How much the borders of a sliced sprite are allowed to scale @@ -52,6 +58,7 @@ namespace Barotrauma { Sprite = new Sprite(element); MaintainAspectRatio = element.GetAttributeBool("maintainaspectratio", false); + MaintainBorderAspectRatio = element.GetAttributeBool("maintainborderaspectratio", false); Tile = element.GetAttributeBool("tile", true); CrossFadeIn = element.GetAttributeBool("crossfadein", CrossFadeIn); CrossFadeOut = element.GetAttributeBool("crossfadeout", CrossFadeOut); @@ -120,13 +127,16 @@ namespace Barotrauma { Vector2 pos = new Vector2(rect.X, rect.Y); - float scale = GetSliceBorderScale(rect.Size); + float scale = MaintainBorderAspectRatio ? 1.0f : GetSliceBorderScale(rect.Size); + float aspectScale = MaintainBorderAspectRatio ? Math.Min((float)rect.Width / Sprite.SourceRect.Width, (float)rect.Height / Sprite.SourceRect.Height) : 1.0f; + int centerHeight = rect.Height - (int)((Slices[0].Height + Slices[6].Height) * scale); - int centerWidth = rect.Width - (int)((Slices[0].Width + Slices[2].Width) * scale); + int centerWidth = rect.Width - (int)((Slices[0].Width + Slices[2].Width) * scale * aspectScale); for (int x = 0; x < 3; x++) { - int width = (int)(x == 1 ? centerWidth : Slices[x].Width * scale); + + int width = (int)(x == 1 ? centerWidth : Slices[x].Width * scale * aspectScale); if (width <= 0) { continue; } for (int y = 0; y < 3; y++) { @@ -147,7 +157,7 @@ namespace Barotrauma else if (Tile) { Vector2 startPos = new Vector2(rect.X, rect.Y); - Sprite.DrawTiled(spriteBatch, startPos, new Vector2(rect.Width, rect.Height), null, color); + Sprite.DrawTiled(spriteBatch, startPos, new Vector2(rect.Width, rect.Height), color); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs new file mode 100644 index 000000000..8262bcb86 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -0,0 +1,1142 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using FarseerPhysics; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; +using SpriteBatch = Microsoft.Xna.Framework.Graphics.SpriteBatch; + +namespace Barotrauma +{ + + internal class UpgradeStore + { + private readonly struct CategoryData + { + public readonly UpgradeCategory Category; + public readonly List Prefabs; + public readonly UpgradePrefab SinglePrefab; + + public CategoryData(UpgradeCategory category, List prefabs) + { + Category = category; + Prefabs = prefabs; + SinglePrefab = null; + } + + public CategoryData(UpgradeCategory category, UpgradePrefab prefab) + { + Category = category; + SinglePrefab = prefab; + Prefabs = null; + } + } + + private readonly CampaignUI campaignUI; + private CampaignMode Campaign => campaignUI?.Campaign; + private UpgradeTab selectedUpgradTab = UpgradeTab.Upgrade; + + private GUIMessageBox currectConfirmation; + + public readonly GUIFrame ItemInfoFrame; + private GUIComponent selectedUpgradeCategoryLayout; + private GUILayoutGroup topHeaderLayout; + private GUILayoutGroup mainStoreLayout; + private GUILayoutGroup storeLayout; + private GUILayoutGroup categoryButtonLayout; + private GUILayoutGroup submarineInfoFrame; + private GUIListBox currentStoreLayout; + private GUICustomComponent submarinePreviewComponent; + private GUIFrame subPreviewFrame; + private Submarine drawnSubmarine; + private readonly List applicableCategories = new List(); + private Vector2[][] subHullVerticies = new Vector2[0][]; + private List submarineWalls = new List(); + + public MapEntity HoveredItem; + private bool highlightWalls; + + private readonly Dictionary itemPreviews = new Dictionary(); + + private static readonly Color previewWhite = Color.White * 0.5f; + + private Point screenResolution; + + /// + /// 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. + /// + public static bool WaitForServerUpdate; + + private enum UpgradeTab + { + Upgrade, + Repairs + } + + public UpgradeStore(CampaignUI campaignUI, GUIComponent parent) + { + WaitForServerUpdate = false; + this.campaignUI = campaignUI; + GUIFrame upgradeFrame = new GUIFrame(rectT(1, 1, parent, Anchor.Center), style: "OuterGlow", color: Color.Black * 0.7f) + { + CanBeFocused = false, UserData = "outerglow" + }; + + ItemInfoFrame = new GUIFrame(new RectTransform(new Vector2(0.13f, 0.13f), GUI.Canvas, minSize: new Point(250, 150)), style: "GUIToolTip") + { + CanBeFocused = false + }; + + CreateUI(upgradeFrame); + + Campaign.UpgradeManager.OnUpgradesChanged += RefreshAll; + Campaign.CargoManager.OnPurchasedItemsChanged += RefreshAll; + Campaign.CargoManager.OnSoldItemsChanged += RefreshAll; + } + + public void RefreshAll() + { + switch (selectedUpgradTab) + { + case UpgradeTab.Repairs: + { + SelectTab(UpgradeTab.Repairs); + break; + } + case UpgradeTab.Upgrade: + { + RefreshUpgradeList(); + break; + } + } + } + + private void RefreshUpgradeList() + { + // Updates the progress bar / text and disables the buy button if we reached max level + if (selectedUpgradeCategoryLayout?.Parent != null && selectedUpgradeCategoryLayout.FindChild("prefablist", true) is GUIListBox listBox) + { + foreach (var component in listBox.Content.Children) + { + if (component.UserData is CategoryData data) + { + UpdateUpgradeEntry(component, data.SinglePrefab, data.Category); + } + } + } + + // update the small indicator icons on the list + if (currentStoreLayout?.Parent != null) + { + foreach (GUIComponent component in currentStoreLayout.Content.Children) + { + if (!(component.UserData is CategoryData data)) { continue; } + if (component.FindChild("indicators", true) is { } indicators) + { + UpdateCategoryIndicators(indicators, component, data.Prefabs, data.Category); + } + } + + // reset the order first + foreach (UpgradeCategory category in UpgradeCategory.Categories) + { + GUIComponent component = currentStoreLayout.Content.FindChild(c => c.UserData is CategoryData categoryData && categoryData.Category == category); + component?.SetAsLastChild(); + } + + // send the disabled components to the bottom + List lastChilds = currentStoreLayout.Content.Children.Where(component => !component.Enabled).ToList(); + + foreach (var lastChild in lastChilds) + { + lastChild.SetAsLastChild(); + } + } + } + + /* Rough layout of the upgrade store 0.9 padding + * _____________________________________________________________________________________________________________________ + * | i | Shipyard | balance | + * |---------------------------------------------------------| xxxx mk | + * | upg. | maint. | |_________________________________________________________| + * |---------------------------------------------------------|---------------------------------------------------------| <- header separator + * | upgrade list | | selected category | | sub name | + * | | | | empty space | submarine description | + * | | | | |______________________________| + * | | | __________________|_______________________________________________________________________| + * | | | | | | + * |____________________| |__|_________________| | + * | store layout | | category layout | | + * | | | | | | + * |____________________| | | | | + * | | | | | + * | | | submarine preview layout | + * | | | | | + * | | | | | + * | empty space | | | | + * | | | | | + * | | | | | + * | | | | | + * |______________________|__|_________________|_______________________________________________________________________| + */ + private void CreateUI(GUIComponent parent) + { + selectedUpgradTab = UpgradeTab.Upgrade; + parent.ClearChildren(); + + ItemInfoFrame.ClearChildren(); + + /* TOOLTIP + * |----------------------------| + * | item name | + * |----------------------------| + * | upgrades: | + * |----------------------------| + * | upgrade list | + * | | + * | | + * |----------------------------| + * | X more... | + * |----------------------------| + */ + 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), TextManager.Get("UpgradeUITooltip.UpgradeListHeader")); + new GUIListBox(rectT(1, 0.5f, tooltipLayout), style: null) { ScrollBarVisible = false, AutoHideScrollBar = false, UserData = "upgradelist"}; + new GUITextBlock(rectT(1, 0, tooltipLayout), string.Empty) { UserData = "moreindicator" }; + ItemInfoFrame.Children.ForEach(c => { c.CanBeFocused = false; c.Children.ForEach(c2 => c2.CanBeFocused = false); }); + + GUIFrame paddedLayout = new GUIFrame(rectT(0.95f, GUI.IsFourByThree() ? 0.98f : 0.95f, parent, Anchor.Center), style: null); + mainStoreLayout = new GUILayoutGroup(rectT(1, 0.9f, paddedLayout, Anchor.BottomLeft), isHorizontal: true) { RelativeSpacing = 0.01f }; + topHeaderLayout = new GUILayoutGroup(rectT(1, 0.1f, paddedLayout, Anchor.TopLeft), isHorizontal: true); + + storeLayout = new GUILayoutGroup(rectT(0.2f, 0.4f, mainStoreLayout), isHorizontal: true) { RelativeSpacing = 0.02f }; + + + /* LEFT HEADER LAYOUT + * |---------------------------------------------------------------------------------------------------| + * | icon | Shipyard | + * |---------------------------------------------------------------------------------------------------| + * | upgrades | maintenance | <- 1/3rd empty space | + * |---------------------------------------------------------------------------------------------------| + */ + 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); + 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 = selectedUpgradTab == UpgradeTab.Upgrade }; + GUIButton repairButton = new GUIButton(rectT(1, 1f, categoryButtonLayout), TextManager.Get("UICategory.Maintenance"), style: "GUITabButton") { UserData = UpgradeTab.Repairs, Selected = selectedUpgradTab == UpgradeTab.Repairs }; + + /* RIGHT HEADER LAYOUT + * |---------------------------------------------------------------------------------------------------| + * | empty space | + * |---------------------------------------------------------------------------------------------------| + * | Balance | + * | XXXX Mk | + * |---------------------------------------------------------------------------------------------------| + * | empty space | horizontal line | + * |---------------------------------------------------------------------------------------------------| + */ + 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(Campaign.Money, format: true), font: GUI.SubHeadingFont, textAlignment: Alignment.Right) { TextGetter = () => FormatCurrency(Campaign.Money, format: true) }; + new GUIFrame(rectT(0.5f, 0.1f, rightLayout, Anchor.BottomRight), style: "HorizontalLine") { IgnoreLayoutGroups = true }; + + repairButton.OnClicked = upgradeButton.OnClicked = (button, o) => + { + if (o is UpgradeTab upgradeTab) + { + if (upgradeTab != selectedUpgradTab || currentStoreLayout == null || currentStoreLayout.Parent != storeLayout) + { + selectedUpgradTab = upgradeTab; + SelectTab(selectedUpgradTab); + storeLayout?.Recalculate(); + } + + repairButton.Selected = (UpgradeTab) repairButton.UserData == selectedUpgradTab; + upgradeButton.Selected = (UpgradeTab) upgradeButton.UserData == selectedUpgradTab; + + return true; + } + + return false; + }; + + // submarine preview + submarinePreviewComponent = new GUICustomComponent(rectT(0.75f, 0.75f, mainStoreLayout, Anchor.BottomRight), onUpdate: UpdateSubmarinePreview, onDraw: DrawSubmarine) + { + IgnoreLayoutGroups = true + }; + + SelectTab(UpgradeTab.Upgrade); + +#if DEBUG + // creates a button that re-creates the UI + CreateRefreshButton(); + void CreateRefreshButton() + { + new GUIButton(rectT(0.2f, 0.1f, parent, Anchor.TopCenter), "Recreate UI - NOT PRESENT IN RELEASE!") + { + OnClicked = (button, o) => + { + CreateUI(parent); + return true; + } + }; + } +#endif + } + + private void SelectTab(UpgradeTab tab) + { + if (currentStoreLayout != null) + { + storeLayout.RemoveChild(currentStoreLayout); + } + + if (selectedUpgradeCategoryLayout != null) + { + mainStoreLayout.RemoveChild(selectedUpgradeCategoryLayout); + } + + switch (tab) + { + case UpgradeTab.Upgrade: + { + CreateUpgradeTab(); + break; + } + case UpgradeTab.Repairs: + { + CreateRepairsTab(); + break; + } + } + } + + private void CreateRepairsTab() + { + highlightWalls = false; + foreach (GUIFrame itemFrame in itemPreviews.Values) + { + itemFrame.OutlineColor = previewWhite; + } + + currentStoreLayout = new GUIListBox(new RectTransform(new Vector2(1.2f, 1.5f), storeLayout.RectTransform) { MinSize = new Point(256, 0) }, style: null) + { + AutoHideScrollBar = false, + ScrollBarVisible = false, + Spacing = 8 + }; + + Location location = Campaign.Map.CurrentLocation; + int hullRepairCost = location?.GetAdjustedMechanicalCost(CampaignMode.HullRepairCost) ?? CampaignMode.HullRepairCost; + int itemRepairCost = location?.GetAdjustedMechanicalCost(CampaignMode.ItemRepairCost) ?? CampaignMode.ItemRepairCost; + int shuttleRetrieveCost = location?.GetAdjustedMechanicalCost(CampaignMode.ShuttleReplaceCost) ?? CampaignMode.ShuttleReplaceCost; + + CreateRepairEntry(currentStoreLayout.Content, TextManager.Get("repairallwalls"), "RepairHullButton", hullRepairCost, (button, o) => + { + if (Campaign.PurchasedHullRepairs) + { + button.Enabled = false; + return false; + } + + if (Campaign.Money >= hullRepairCost) + { + string body = TextManager.GetWithVariable("WallRepairs.PurchasePromptBody", "[amount]", hullRepairCost.ToString()); + currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), body, () => + { + if (Campaign.Money >= hullRepairCost) + { + Campaign.Money -= hullRepairCost; + Campaign.PurchasedHullRepairs = true; + button.Enabled = false; + SelectTab(UpgradeTab.Repairs); + GameMain.Client?.SendCampaignState(); + } + else + { + button.Enabled = false; + } + return true; + }); + } + else + { + button.Enabled = false; + return false; + } + return true; + }, Campaign.PurchasedHullRepairs || !HasPermission, isHovered => + { + highlightWalls = isHovered; + return true; + }); + + CreateRepairEntry(currentStoreLayout.Content, TextManager.Get("repairallitems"), "RepairItemsButton", itemRepairCost, (button, o) => + { + if (Campaign.Money >= itemRepairCost && !Campaign.PurchasedItemRepairs) + { + string body = TextManager.GetWithVariable("ItemRepairs.PurchasePromptBody", "[amount]", itemRepairCost.ToString()); + currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), body, () => + { + if (Campaign.Money >= itemRepairCost && !Campaign.PurchasedItemRepairs) + { + Campaign.Money -= itemRepairCost; + Campaign.PurchasedItemRepairs = true; + button.Enabled = false; + SelectTab(UpgradeTab.Repairs); + GameMain.Client?.SendCampaignState(); + } + else + { + button.Enabled = false; + } + return true; + }); + } + else + { + button.Enabled = false; + return false; + } + + return true; + }, Campaign.PurchasedItemRepairs || !HasPermission, isHovered => + { + foreach (var (item, itemFrame) in itemPreviews) + { + itemFrame.OutlineColor = itemFrame.Color = isHovered && item.GetComponent() == null ? GUI.Style.Orange : previewWhite; + } + return true; + }); + + CreateRepairEntry(currentStoreLayout.Content, TextManager.Get("replacelostshuttles"), "ReplaceShuttlesButton", shuttleRetrieveCost, (button, o) => + { + if (GameMain.GameSession?.SubmarineInfo != null && + GameMain.GameSession.SubmarineInfo.LeftBehindSubDockingPortOccupied) + { + new GUIMessageBox("", TextManager.Get("ReplaceShuttleDockingPortOccupied")); + return false; + } + + if (Campaign.Money >= shuttleRetrieveCost && !Campaign.PurchasedLostShuttles) + { + string body = TextManager.GetWithVariable("ReplaceLostShuttles.PurchasePromptBody", "[amount]", shuttleRetrieveCost.ToString()); + currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), body, () => + { + if (Campaign.Money >= shuttleRetrieveCost && !Campaign.PurchasedLostShuttles) + { + Campaign.Money -= shuttleRetrieveCost; + Campaign.PurchasedLostShuttles = true; + button.Enabled = false; + SelectTab(UpgradeTab.Repairs); + GameMain.Client?.SendCampaignState(); + } + return true; + }); + } + else + { + button.Enabled = false; + return false; + } + + return true; + }, Campaign.PurchasedLostShuttles || !HasPermission || GameMain.GameSession?.SubmarineInfo == null || !GameMain.GameSession.SubmarineInfo.SubsLeftBehind, isHovered => + { + if (!isHovered) { return false; } + + foreach (var (item, itemFrame) in itemPreviews) + { + if (GameMain.GameSession.SubmarineInfo.LeftBehindDockingPortIDs.Contains(item.ID)) + { + itemFrame.OutlineColor = itemFrame.Color = GameMain.GameSession.SubmarineInfo.BlockedDockingPortIDs.Contains(item.ID) ? GUI.Style.Red : GUI.Style.Green; + } + else + { + itemFrame.OutlineColor = itemFrame.Color = previewWhite; + } + } + return true; + }, disableElement: true); + } + + private void CreateRepairEntry(GUIComponent parent, string 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; + + // Kinda hacky? idk, I don't see any other way to bring an Update() function to the campaign store. + new GUICustomComponent(rectT(1, 1, frameChild), onUpdate: UpdateHover) { CanBeFocused = false }; + + /* REPAIR ENTRY + * |-------------------------------------------------| + * | | repair title | | + * | icon |---------------------------| buy btn. | + * | | xxx mk | | + * |-------------------------------------------------| + */ + 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), 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") { Enabled = Campaign.Money >= price && !isDisabled, OnClicked = onPressed }; + contentLayout.Recalculate(); + buyButtonLayout.Recalculate(); + + if (disableElement) + { + frameChild.Enabled = Campaign.Money >= price && !isDisabled; + } + + if (!HasPermission) + { + frameChild.Enabled = false; + } + + void UpdateHover(float deltaTime, GUICustomComponent component) + { + onHover?.Invoke(GUI.MouseOn != null && frameChild.IsParentOf(GUI.MouseOn) || GUI.MouseOn == frameChild); + } + } + + private void CreateUpgradeTab() + { + currentStoreLayout = new GUIListBox(rectT(1.0f, 1.5f, storeLayout), style: null) + { + AutoHideScrollBar = false, + ScrollBarVisible = false, + HideChildrenOutsideFrame = false, + SmoothScroll = true, + FadeElements = true, + PadBottom = true, + SelectTop = true, + ClampScrollToElements = true, + Spacing = 8 + }; + + Dictionary> upgrades = new Dictionary>(); + + foreach (UpgradeCategory category in UpgradeCategory.Categories) + { + foreach (UpgradePrefab prefab in UpgradePrefab.Prefabs) + { + if (prefab.UpgradeCategories.Contains(category)) + { + if (upgrades.ContainsKey(category)) + { + upgrades[category].Add(prefab); + } + else + { + upgrades.Add(category, new List { prefab }); + } + } + } + } + + foreach (var (category, prefabs) in upgrades) + { + var frameChild = new GUIFrame(rectT(1, 0.15f, currentStoreLayout.Content), style: "UpgradeUIFrame") + { + UserData = new CategoryData(category, prefabs), + GlowOnSelect = true + }; + + frameChild.DefaultColor = frameChild.Color; + frameChild.Color = Color.Transparent; + + /* UPGRADE CATEGORY + * |--------------------------------------------------------| + * | | + * | category title |--------------------------| + * | | indicators | + * |-----------------------------|--------------------------| + */ + 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 }; + 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) + { + GUIImage upgradeIndicator = new GUIImage(rectT(0.1f, 1f, indicatorLayout), style: "UpgradeIndicator", scaleToFit: true) { UserData = prefab, CanBeFocused = false }; + upgradeIndicator.DefaultColor = upgradeIndicator.Color; + upgradeIndicator.Color = Color.Transparent; + } + + itemCategoryLabel.DefaultColor = itemCategoryLabel.TextColor; + itemCategoryLabel.TextColor = Color.Transparent; + + contentLayout.Recalculate(); + indicatorLayout.Recalculate(); + } + + selectedUpgradeCategoryLayout = new GUIFrame(rectT(GUI.IsFourByThree() ? 0.3f : 0.25f, 1, mainStoreLayout), style: null) { CanBeFocused = false }; + + RefreshUpgradeList(); + + currentStoreLayout.OnSelected += (component, userData) => + { + if (!component.Enabled) + { + selectedUpgradeCategoryLayout?.ClearChildren(); + foreach (GUIFrame itemFrame in itemPreviews.Values) + { + itemFrame.OutlineColor = itemFrame.Color = previewWhite; + } + return true; + } + + if (userData is CategoryData categoryData && Submarine.MainSub != null) + { + TrySelectCategory(categoryData.Prefabs, categoryData.Category, Submarine.MainSub); + } + + return true; + }; + } + + // This was supposed to have some logic for fancy animations to slide the previous tab out but maybe another time + private void TrySelectCategory(List prefabs, UpgradeCategory category, Submarine submarine) => SelectUpgradeCategory(prefabs, category, submarine); + + private void SelectUpgradeCategory(List prefabs, UpgradeCategory category, Submarine submarine) + { + if (selectedUpgradeCategoryLayout == null || submarine == null) { return; } + + GUIFrame[] categoryFrames = GetFrames(category); + foreach (GUIFrame itemFrame in itemPreviews.Values) + { + itemFrame.OutlineColor = itemFrame.Color = categoryFrames.Contains(itemFrame) ? GUI.Style.Orange : previewWhite; + } + + highlightWalls = category.IsWallUpgrade; + + selectedUpgradeCategoryLayout?.ClearChildren(); + GUIFrame frame = new GUIFrame(rectT(1, 0.4f, selectedUpgradeCategoryLayout)); + GUIListBox prefabList = new GUIListBox(rectT(0.93f, 0.9f, frame, Anchor.Center)) { UserData = "prefablist" }; + foreach (UpgradePrefab prefab in prefabs) + { + CreateUpgradeEntry(prefab, category, prefabList.Content); + } + } + + private void CreateUpgradeEntry(UpgradePrefab prefab, UpgradeCategory category, GUIComponent parent) + { + /* UPGRADE PREFAB ENTRY + * |------------------------------------------------------------------| + * | | title | price | + * | |----------------------------------|_______________| + * | icon | description | | + * | |----------------------------------| buy btn. | + * | | progress bar | x / y | | + * |------------------------------------------------------------------| + */ + GUIFrame prefabFrame = new GUIFrame(rectT(1f, 0.25f, parent), style: "ListBoxElement") { SelectedColor = Color.Transparent, UserData = new CategoryData(category, prefab) }; + GUILayoutGroup prefabLayout = new GUILayoutGroup(rectT(0.98f,0.95f, prefabFrame, Anchor.Center), isHorizontal: true); + 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), prefab.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), prefab.Name, font: GUI.SubHeadingFont) { AutoScaleHorizontal = true, AutoScaleVertical = true, Padding = Vector4.Zero }; + GUILayoutGroup descriptionLayout = new GUILayoutGroup(rectT(1, 0.50f, textLayout)); + var description = new GUITextBlock(rectT(1, 1, descriptionLayout), prefab.Description, font: GUI.SmallFont, wrap: true, textAlignment: Alignment.TopLeft) { Padding = Vector4.Zero }; + GUILayoutGroup 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 }; + GUILayoutGroup buyButtonLayout = new GUILayoutGroup(rectT(0.2f, 1, prefabLayout), childAnchor: Anchor.TopCenter) { UserData = "buybutton" }; + new GUITextBlock(rectT(1, 0.4f, buyButtonLayout), FormatCurrency(prefab.Price.GetBuyprice(Campaign.UpgradeManager.GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation)), textAlignment: Alignment.Center) { Padding = Vector4.Zero }; + var buyButton = new GUIButton(rectT(0.7f, 0.5f, buyButtonLayout), string.Empty, style: "UpgradeBuyButton") { Enabled = false }; + + description.CalculateHeightFromText(); + // 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)); + if (0 >= newString.Length - 4) { break; } + + description.Text = newString.Substring(0, newString.Length - 4) + "..."; + description.CalculateHeightFromText(); + description.ToolTip = prefab.Description; + } + + // Recalculate everything to prevent jumping + if (parent is GUILayoutGroup group) { group.Recalculate(); } + + descriptionLayout.Recalculate(); + prefabLayout.Recalculate(); + imageLayout.Recalculate(); + textLayout.Recalculate(); + progressLayout.Recalculate(); + buyButtonLayout.Recalculate(); + + if (!HasPermission) + { + prefabFrame.Enabled = false; + description.Enabled = false; + name.Enabled = false; + icon.Color = Color.Gray; + } + + 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() }); + currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), promptBody, () => + { + if (GameMain.NetworkMember != null) + { + WaitForServerUpdate = true; + } + Campaign.UpgradeManager.PurchaseUpgrade(prefab, category); + GameMain.Client?.SendCampaignState(); + return true; + }); + + return true; + }; + + UpdateUpgradeEntry(prefabFrame, prefab, category); + } + + private void CreateItemTooltip(MapEntity entity) + { + GUITextBlock itemName = ItemInfoFrame.FindChild("itemname", true) as GUITextBlock; + GUIListBox upgradeList = ItemInfoFrame.FindChild("upgradelist", true) as GUIListBox; + GUITextBlock moreIndicator = ItemInfoFrame.FindChild("moreindicator", true) as GUITextBlock; + GUILayoutGroup layout = ItemInfoFrame.GetChild(); + Debug.Assert(itemName != null && upgradeList != null && moreIndicator != null && layout != null, "One ore more tooltip elements not found"); + + List upgrades = entity.GetUpgrades(); + int upgradesCount = upgrades.Count; + const int maxUpgrades = 4; + + itemName.Text = entity is Item ? entity.Name : TextManager.Get("upgradecategory.walls"); + upgradeList.Content.ClearChildren(); + for (var i = 0; i < upgrades.Count && i < maxUpgrades; i++) + { + Upgrade upgrade = upgrades[i]; + new GUITextBlock(rectT(1, 0.25f, upgradeList.Content), CreateListEntry(upgrade.Prefab.Name, upgrade.Level)) { AutoScaleHorizontal = true, UserData = Tuple.Create(upgrade.Level, upgrade.Prefab) }; + } + + // include pending upgrades into the tooltip + foreach (var (prefab, category, level) in Campaign.UpgradeManager.PendingUpgrades) + { + if (entity is Item item && category.CanBeApplied(item) || entity is Structure && category.IsWallUpgrade) + { + bool found = false; + foreach (GUITextBlock textBlock in upgradeList.Content.Children.Where(c => c is GUITextBlock).Cast()) + { + if (textBlock.UserData is Tuple tuple && tuple.Item2 == prefab) + { + string tooltip = CreateListEntry(tuple.Item2.Name, level + tuple.Item1); + textBlock.Text = tooltip; + found = true; + break; + } + } + + if (!found) + { + upgradesCount++; + if (upgradeList.Content.CountChildren < maxUpgrades) + { + new GUITextBlock(rectT(1, 0.25f, upgradeList.Content), CreateListEntry(prefab.Name, level)) { AutoScaleHorizontal = true }; + } + } + } + } + + if (!upgradeList.Content.Children.Any()) + { + new GUITextBlock(rectT(1, 0.25f, upgradeList.Content), TextManager.Get("UpgradeUITooltip.NoUpgradesElement")) { AutoScaleHorizontal = true }; + } + + moreIndicator.Text = upgradesCount > maxUpgrades ? TextManager.GetWithVariable("upgradeuitooltip.moreindicator", "[amount]", $"{upgradesCount - maxUpgrades}") : string.Empty; + + itemName.CalculateHeightFromText(); + moreIndicator.CalculateHeightFromText(); + layout.Recalculate(); + + static string CreateListEntry(string name, int level) => TextManager.GetWithVariables("upgradeuitooltip.upgradelistelement", new[] { "[upgradename]", "[level]" }, new[] { name, $"{level}" }); + } + + private void UpdateSubmarinePreview(float deltaTime, GUICustomComponent parent) + { + 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; + if (drawnSubmarine != null) + { + CreateSubmarinePreview(drawnSubmarine, parent); + CreateHullBorderVerticies(drawnSubmarine, parent); + + List entitiesOnSub = drawnSubmarine.GetItems(true).Where(i => drawnSubmarine.IsEntityFoundOnThisSub(i, true)).ToList(); + applicableCategories.Clear(); + + foreach (UpgradeCategory category in UpgradeCategory.Categories) + { + if (entitiesOnSub.Any(item => category.CanBeApplied(item) && !item.disallowedUpgrades.Contains(category.Identifier))) + { + applicableCategories.Add(category); + } + } + } + + screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); + // this might be a bit spaghetti, we use the submarine preview's Update() function to refresh the upgrade list when the submarine changes + // 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(); + } + + // accept an active confirmation popup if any + if (PlayerInput.KeyHit(Keys.Enter) && GUIMessageBox.MessageBoxes.Any()) + { + for (int i = GUIMessageBox.MessageBoxes.Count - 1; i >= 0; i--) + { + if (GUIMessageBox.MessageBoxes[i] is GUIMessageBox msgBox && msgBox == currectConfirmation) + { + // first button is the ok button + GUIButton firstButton = msgBox.Buttons.FirstOrDefault(); + if (firstButton == null) { continue; } + + firstButton.OnClicked.Invoke(firstButton, firstButton.UserData); + } + } + } + + if (itemPreviews == null) { return; } + + bool found = false; + foreach (var (item, frame) in itemPreviews) + { + if (GUI.MouseOn == frame) + { + if (HoveredItem != item) { CreateItemTooltip(item); } + HoveredItem = item; + if (PlayerInput.PrimaryMouseButtonClicked() && selectedUpgradTab == UpgradeTab.Upgrade && currentStoreLayout != null) + { + ScrollToCategory(data => data.Category.CanBeApplied(item)); + } + found = true; + break; + } + } + + if (!found) + { + bool isMouseOnStructure = false; + if (GUI.MouseOn == submarinePreviewComponent || GUI.MouseOn == subPreviewFrame) + { + // Every wall should have the same upgrades so we can just display the first one in the tooltip + Structure firstStructure = submarineWalls.FirstOrDefault(); + // use pnpoly algorithm to detect if our mouse is within any of the hull polygons + if (subHullVerticies.Any(hullVertex => ToolBox.PointIntersectsWithPolygon(PlayerInput.MousePosition, hullVertex))) + { + if (HoveredItem != firstStructure) { CreateItemTooltip(firstStructure); } + HoveredItem = firstStructure; + isMouseOnStructure = true; + GUI.MouseCursor = CursorState.Hand; + + if (PlayerInput.PrimaryMouseButtonClicked() && selectedUpgradTab == UpgradeTab.Upgrade && currentStoreLayout != null) + { + ScrollToCategory(data => data.Category.IsWallUpgrade); + } + } + } + + if (!isMouseOnStructure) { HoveredItem = null; } + } + + // flip the tooltip if it is outside of the screen + ItemInfoFrame.RectTransform.ScreenSpaceOffset = (PlayerInput.MousePosition + new Vector2(20, 20)).ToPoint(); + if (ItemInfoFrame.Rect.Right > GameMain.GraphicsWidth) + { + ItemInfoFrame.RectTransform.ScreenSpaceOffset = (PlayerInput.MousePosition - new Vector2(20 + ItemInfoFrame.Rect.Width, -20)).ToPoint(); + } + } + + private void CreateSubmarinePreview(Submarine submarine, GUIComponent parent) + { + if (submarineInfoFrame != null && mainStoreLayout == submarineInfoFrame.Parent) + { + mainStoreLayout.RemoveChild(submarineInfoFrame); + } + + parent.ClearChildren(); + + /* SUBMARINE INFO BOX + * |--------------------------------------------------| + * | name | + * |--------------------------------------------------| + * | class | + * |--------------------------------------------------| + * | description | + * | | + * | | + * |--------------------------------------------------| + */ + 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); + // 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); + 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)); + + description.Padding = new Vector4(description.Padding.X, 24 * GUI.Scale, description.Padding.Z, description.Padding.W); + List pointsOfInterest = (from category in UpgradeCategory.Categories from item in submarine.GetItems(UpgradeManager.UpgradeAlsoConnectedSubs) where category.CanBeApplied(item) && !item.NonInteractable select item).Cast().ToList(); + + List ids = GameMain.GameSession.SubmarineInfo?.LeftBehindDockingPortIDs ?? new List(); + pointsOfInterest.AddRange(submarine.GetItems(UpgradeManager.UpgradeAlsoConnectedSubs).Where(item => ids.Contains(item.ID))); + + submarine.CreateMiniMap(parent, pointsOfInterest, ignoreOutpost: true); + subPreviewFrame = parent.GetChild(); + Rectangle dockedBorders = submarine.GetDockedBorders(); + GUIFrame hullContainer = parent.GetChild(); + if (hullContainer == null) { return; } + itemPreviews.Clear(); + + foreach (Entity entity in pointsOfInterest) + { + GUIComponent component = parent.FindChild(entity, true); + if (component != null && entity is Item item) + { + 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)); + GUIFrame itemFrame = new GUIFrame(rectT(size, component, Anchor.Center), style: "ScanLines") + { + SelectedColor = GUI.Style.Orange, + OutlineColor = previewWhite, + Color = previewWhite, + OutlineThickness = 2, + HoverCursor = CursorState.Hand + }; + if (!itemPreviews.ContainsKey(item)) + { + itemPreviews.Add(item, itemFrame); + } + } + } + } + + /// + /// Creates vertices for the submarine border that we use to draw it and check mouse collision + /// + /// + /// + /// + /// Most of this code is copied from the status terminal but instead of drawing a line from X to Y + /// we create a rotated rectangle instead and store the 4 corners into the array. + /// + private void CreateHullBorderVerticies(Submarine sub, GUIComponent parent) + { + submarineWalls = sub.GetWalls(UpgradeManager.UpgradeAlsoConnectedSubs); + const float lineWidth = 10; + + if (sub.HullVertices == null) { return; } + + Rectangle dockedBorders = sub.GetDockedBorders(); + dockedBorders.Location += sub.WorldPosition.ToPoint(); + + float scale = Math.Min(parent.Rect.Width / (float)dockedBorders.Width, parent.Rect.Height / (float)dockedBorders.Height) * 0.9f; + + float displayScale = ConvertUnits.ToDisplayUnits(scale); + Vector2 offset = (sub.WorldPosition - new Vector2(dockedBorders.Center.X, dockedBorders.Y - dockedBorders.Height / 2)) * scale; + Vector2 center = parent.Rect.Center.ToVector2(); + + subHullVerticies = new Vector2[sub.HullVertices.Count][]; + + for (int i = 0; i < sub.HullVertices.Count; i++) + { + Vector2 start = sub.HullVertices[i] * displayScale + offset; + start.Y = -start.Y; + Vector2 end = sub.HullVertices[(i + 1) % sub.HullVertices.Count] * displayScale + offset; + end.Y = -end.Y; + + Vector2 edge = end - start; + float length = edge.Length(); + float angle = (float)Math.Atan2(edge.Y, edge.X); + Matrix rotate = Matrix.CreateRotationZ(angle); + + subHullVerticies[i] = new[] + { + center + start + Vector2.Transform(new Vector2(length, -lineWidth), rotate), + center + end + Vector2.Transform(new Vector2(-length, -lineWidth), rotate), + center + end + Vector2.Transform(new Vector2(-length, lineWidth), rotate), + center + start + Vector2.Transform(new Vector2(length, lineWidth), rotate), + }; + } + } + + private void DrawSubmarine(SpriteBatch spriteBatch, GUICustomComponent component) + { + foreach (Vector2[] hullVertex in subHullVerticies) + { + // 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); + if (GameMain.DebugDraw) + { + // the "collision box" is a bit bigger than the line we draw so this can be useful data (maybe) + GUI.DrawRectangle(spriteBatch, hullVertex, Color.Red); + } + } + } + + private void UpdateUpgradeEntry(GUIComponent prefabFrame, UpgradePrefab prefab, UpgradeCategory category) + { + int currentLevel = Campaign.UpgradeManager.GetUpgradeLevel(prefab, category); + + string progressText = TextManager.GetWithVariables("upgrades.progressformat", new[] { "[level]", "[maxlevel]" }, new[] { currentLevel.ToString(), 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; + } + + GUITextBlock block = progressParent.GetChild(); + if (block != null) { block.Text = progressText; } + } + + if (prefabFrame.FindChild("buybutton", true) is { } buttonParent) + { + GUITextBlock priceLabel = buttonParent.GetChild(); + int price = prefab.Price.GetBuyprice(Campaign.UpgradeManager.GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation); + + if (priceLabel != null && !WaitForServerUpdate) + { + priceLabel.Text = FormatCurrency(price); + if (currentLevel >= prefab.MaxLevel) + { + priceLabel.Text = TextManager.Get("Upgrade.MaxedUpgrade"); + } + } + + GUIButton button = buttonParent.GetChild(); + if (button != null) + { + button.Enabled = currentLevel < prefab.MaxLevel; + if (WaitForServerUpdate || !HasPermission || price > Campaign.Money) + { + button.Enabled = false; + } + } + } + } + + private void UpdateCategoryIndicators(GUIComponent indicators, GUIComponent parent, List prefabs, UpgradeCategory category) + { + // Disables the parent and only re-enables if the submarine contains valid items + if (!category.IsWallUpgrade && drawnSubmarine != null) + { + if (applicableCategories.Contains(category)) + { + parent.Enabled = true; + parent.SelectedColor = parent.Style.SelectedColor; + } + else + { + parent.Enabled = false; + parent.SelectedColor = GUI.Style.Red * 0.5f; + } + } + + foreach (GUIComponent component in indicators.Children) + { + if (!(component is GUIImage image)) { continue; } + + foreach (UpgradePrefab prefab in prefabs) + { + if (component.UserData != prefab) { continue; } + + Dictionary styles = GUI.Style.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"]; + + if (Campaign.UpgradeManager.GetUpgradeLevel(prefab, category) >= prefab.MaxLevel) + { + // we check this to avoid flickering from re-applying the same style + if (image.Style == onStyle) { continue; } + image.ApplyStyle(onStyle); + } + else if (Campaign.UpgradeManager.GetUpgradeLevel(prefab, category) > 0) + { + if (image.Style == dimStyle) { continue; } + image.ApplyStyle(dimStyle); + } + else + { + if (image.Style == offStyle) { continue; } + image.ApplyStyle(offStyle); + } + } + } + } + + private void ScrollToCategory(Predicate predicate) + { + foreach (GUIComponent child in currentStoreLayout.Content.Children) + { + if (child.UserData is CategoryData data && predicate(data)) + { + currentStoreLayout.ScrollToElement(child); + break; + } + } + } + + /// + /// Gets all "points of interest" GUIFrames on the upgrade preview interface that match the corresponding upgrade category. + /// + /// + /// + private GUIFrame[] GetFrames(UpgradeCategory category) + { + List frames = new List(); + foreach (var (item, guiFrame) in itemPreviews) + { + if (category.CanBeApplied(item)) + { + frames.Add(guiFrame); + } + } + + return frames.ToArray(); + } + + private bool HasPermission => campaignUI.Campaign.AllowedToManageCampaign(); + + private static string 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) + { + return new RectTransform(new Vector2(x, y), parentComponent.RectTransform, anchor); + } + + private static RectTransform rectT(Point point, GUIComponent parentComponent, Anchor anchor = Anchor.TopLeft) + { + return new RectTransform(point, parentComponent.RectTransform, anchor); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs new file mode 100644 index 000000000..11489f117 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs @@ -0,0 +1,235 @@ +using System; +using System.Collections.Generic; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + class VotingInterface + { + public bool VoteRunning = false; + + private GUIFrame frame; + private GUITextBlock votingTextBlock, votedTextBlock, voteCounter; + private GUIProgressBar votingTimer; + private GUIButton yesVoteButton, noVoteButton; + private Action onVoteEnd; + + private int yesVotes, noVotes, maxVotes; + private Func getYesVotes, getNoVotes, getMaxVotes; + private bool votePassed; + + private string votingOnText; + private List votingOnTextData; + private float votingTime = 100f; + private float timer; + private VoteType currentVoteType; + private Color submarineColor => GUI.Style.Orange; + private Point createdForResolution; + + public VotingInterface(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); + + Initialize(starter, type); + } + + private void Initialize(Client starter, VoteType type) + { + currentVoteType = type; + CreateVotingGUI(); + if (starter.ID == GameMain.Client.ID) SetGUIToVotedState(2); + VoteRunning = true; + } + + private void CreateVotingGUI() + { + createdForResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); + + if (frame != null) frame.Parent.RemoveChild(frame); + frame = new GUIFrame(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.VotingArea, GameMain.Client.InGameHUD.RectTransform), style: ""); + + int padding = HUDLayoutSettings.Padding * 2; + int spacing = HUDLayoutSettings.Padding; + 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.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.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); + + yOffset += voteCounter.Rect.Height + spacing; + + votingTimer = new GUIProgressBar(new RectTransform(new Point(paddedWidth, Math.Max(spacing, 8)), frame.RectTransform) { AbsoluteOffset = new Point(padding, yOffset) }, HUDLayoutSettings.Padding); + votingTimer.RectTransform.IsFixedSize = true; + yOffset += votingTimer.Rect.Height + spacing; + + int buttonWidth = (int)(paddedWidth * 0.3f); + yesVoteButton = new GUIButton(new RectTransform(new Point(buttonWidth, 0), frame.RectTransform) { AbsoluteOffset = new Point((int)(frame.Rect.Width / 2f - buttonWidth - spacing), yOffset) }, TextManager.Get("yes")) + { + OnClicked = (applyButton, obj) => + { + SetGUIToVotedState(2); + GameMain.Client.Vote(currentVoteType, 2); + return true; + } + }; + + noVoteButton = new GUIButton(new RectTransform(new Point(buttonWidth, 0), frame.RectTransform) { AbsoluteOffset = new Point(yesVoteButton.RectTransform.AbsoluteOffset.X + yesVoteButton.Rect.Width + padding, yOffset) }, TextManager.Get("no")) + { + OnClicked = (applyButton, obj) => + { + SetGUIToVotedState(1); + GameMain.Client.Vote(currentVoteType, 1); + return true; + } + }; + + votedTextBlock = new GUITextBlock(new RectTransform(new Point(paddedWidth, yesVoteButton.Rect.Height), frame.RectTransform), string.Empty, textAlignment: Alignment.Center); + votedTextBlock.RectTransform.IsFixedSize = true; + votedTextBlock.RectTransform.AbsoluteOffset = new Point(padding, yOffset); + votedTextBlock.Visible = false; + + yOffset += yesVoteButton.Rect.Height; + + frame.RectTransform.NonScaledSize = new Point(frame.Rect.Width, yOffset + padding); + } + + private void SetGUIToVotedState(int vote) + { + yesVoteButton.Visible = noVoteButton.Visible = false; + votedTextBlock.Text = TextManager.Get(vote == 2 ? "yesvoted" : "novoted"); + votedTextBlock.Visible = true; + } + + public void Update(float deltaTime) + { + if (!VoteRunning) return; + if (GameMain.GraphicsWidth != createdForResolution.X || GameMain.GraphicsHeight != createdForResolution.Y) CreateVotingGUI(); + yesVotes = getYesVotes(); + noVotes = getNoVotes(); + maxVotes = getMaxVotes(); + voteCounter.Text = $"({yesVotes + noVotes}/{maxVotes})"; + timer += deltaTime; + votingTimer.BarSize = timer / votingTime; + } + + + public void EndVote(bool passed, int yesVoteFinal, int noVoteFinal) + { + VoteRunning = false; + votePassed = passed; + yesVotes = yesVoteFinal; + noVotes = noVoteFinal; + onVoteEnd?.Invoke(); + } + + #region Submarine Voting + private void SetSubmarineVotingText(Client starter, SubmarineInfo info, VoteType type) + { + string name = starter.Name; + JobPrefab prefab = starter?.Character?.Info?.Job?.Prefab; + Color nameColor = prefab != null ? prefab.UIColor : Color.White; + string characterRichString = $"‖color:{nameColor.R},{nameColor.G},{nameColor.B}‖{name}‖color:end‖"; + string submarineRichString = $"‖color:{submarineColor.R},{submarineColor.G},{submarineColor.B}‖{info.DisplayName}‖color:end‖"; + + 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() }); + 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() }); + 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() }); + } + else + { + votingOnText = TextManager.GetWithVariables("submarineswitchnofeevote", new string[] { "[playername]", "[submarinename]" }, new string[] { characterRichString, submarineRichString }); + } + break; + } + + votingOnTextData = RichTextData.GetRichTextData(votingOnText, out votingOnText); + } + + private int SubmarineYesVotes() + { + return GameMain.NetworkMember.SubmarineVoteYesCount; + } + + private int SubmarineNoVotes() + { + return GameMain.NetworkMember.SubmarineVoteNoCount; + } + + private int SubmarineMaxVotes() + { + return GameMain.NetworkMember.SubmarineVoteMax; + } + + private void SendSubmarineVoteEndMessage(SubmarineInfo info, VoteType type) + { + GameMain.NetworkMember.AddChatMessage(GetSubmarineVoteResultMessage(info, type, yesVotes.ToString(), noVotes.ToString(), votePassed), ChatMessageType.Server); + } + + public static string GetSubmarineVoteResultMessage(SubmarineInfo info, VoteType type, string yesVoteString, string noVoteString, bool votePassed) + { + string 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 }); + 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 }); + 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 }); + } + else + { + result = TextManager.GetWithVariables(votePassed ? "submarineswitchnofeevotepassed" : "submarineswitchnofeevotefailed", new string[] { "[submarinename]", "[yesvotecount]", "[novotecount]" }, new string[] { info.DisplayName, yesVoteString, noVoteString }); + } + break; + default: + break; + } + return result; + } + #endregion + + public void Remove() + { + if (frame != null) + { + frame.Parent.RemoveChild(frame); + frame = null; + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 3d4a8018d..a8a473e59 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -17,6 +17,7 @@ using System.Threading; using Barotrauma.Tutorials; using Barotrauma.Media; using Barotrauma.Extensions; +using System.Threading.Tasks; namespace Barotrauma { @@ -35,7 +36,6 @@ namespace Barotrauma public static GameScreen GameScreen; public static MainMenuScreen MainMenuScreen; - public static LobbyScreen LobbyScreen; public static NetLobbyScreen NetLobbyScreen; public static ServerListScreen ServerListScreen; @@ -45,8 +45,11 @@ namespace Barotrauma public static ParticleEditorScreen ParticleEditorScreen; public static LevelEditorScreen LevelEditorScreen; public static SpriteEditorScreen SpriteEditorScreen; + public static EventEditorScreen EventEditorScreen; public static CharacterEditor.CharacterEditorScreen CharacterEditorScreen; + public static CampaignEndScreen CampaignEndScreen; + public static Lights.LightManager LightManager; public static Sounds.SoundManager SoundManager; @@ -79,6 +82,10 @@ namespace Barotrauma set { if (gameSession == value) { return; } + if (value == null && Screen.Selected == GameScreen && gameSession.GameMode is CampaignMode) + { + DebugConsole.AddWarning("GameSession set to null while in the game screen\n" + Environment.StackTrace); + } if (gameSession?.GameMode != null && gameSession.GameMode != value?.GameMode) { gameSession.GameMode.Remove(); @@ -100,7 +107,7 @@ namespace Barotrauma private CoroutineHandle loadingCoroutine; private bool hasLoaded; - private GameTime fixedTime; + private readonly GameTime fixedTime; public string ConnectName; public string ConnectEndpoint; @@ -110,7 +117,7 @@ namespace Barotrauma private Viewport defaultViewport; - public event Action OnResolutionChanged; + public event Action ResolutionChanged; private bool exiting; @@ -190,7 +197,6 @@ namespace Barotrauma public GameMain(string[] args) { Content.RootDirectory = "Content"; - #if DEBUG && WINDOWS GraphicsAdapter.UseDebugLayers = true; #endif @@ -273,7 +279,7 @@ namespace Barotrauma defaultViewport = GraphicsDevice.Viewport; - OnResolutionChanged?.Invoke(); + ResolutionChanged?.Invoke(); } public void SetWindowMode(WindowMode windowMode) @@ -455,9 +461,10 @@ namespace Barotrauma { bool waitingForWorkshopUpdates = true; bool result = false; - TaskPool.Add(SteamManager.AutoUpdateWorkshopItemsAsync(), (task) => + TaskPool.Add("AutoUpdateWorkshopItemsAsync", + SteamManager.AutoUpdateWorkshopItemsAsync(), (task) => { - result = task.Result; + result = ((Task)task).Result; waitingForWorkshopUpdates = false; }); @@ -521,6 +528,10 @@ namespace Barotrauma yield return CoroutineStatus.Running; + TaskPool.Add("InitRelayNetworkAccess", SteamManager.InitRelayNetworkAccess(), (t) => { }); + + FactionPrefab.LoadFactions(); + NPCSet.LoadSets(); CharacterPrefab.LoadAll(); MissionPrefab.Init(); TraitorMissionPrefab.Init(); @@ -528,8 +539,9 @@ namespace Barotrauma Tutorials.Tutorial.Init(); MapGenerationParams.Init(); LevelGenerationParams.LoadPresets(); + OutpostGenerationParams.LoadPresets(); WreckAIConfig.LoadAll(); - ScriptedEventSet.LoadPrefabs(); + EventSet.LoadPrefabs(); AfflictionPrefab.LoadAll(GetFilesOfType(ContentType.Afflictions)); SkillSettings.Load(GetFilesOfType(ContentType.SkillSettings)); Order.Init(); @@ -544,6 +556,10 @@ namespace Barotrauma ItemPrefab.LoadAll(GetFilesOfType(ContentType.Item)); 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)); @@ -567,7 +583,6 @@ namespace Barotrauma yield return CoroutineStatus.Running; MainMenuScreen = new MainMenuScreen(this); - LobbyScreen = new LobbyScreen(); ServerListScreen = new ServerListScreen(); TitleScreen.LoadState = 70.0f; @@ -594,7 +609,9 @@ namespace Barotrauma LevelEditorScreen = new LevelEditorScreen(); SpriteEditorScreen = new SpriteEditorScreen(); + EventEditorScreen = new EventEditorScreen(); CharacterEditorScreen = new CharacterEditor.CharacterEditorScreen(); + CampaignEndScreen = new CampaignEndScreen(); yield return CoroutineStatus.Running; @@ -633,7 +650,7 @@ namespace Barotrauma foreach (ContentPackage contentPackage in Config.SelectedContentPackages) { var exePaths = contentPackage.GetFilesOfType(ContentType.Executable); - if (exePaths.Any() && AppDomain.CurrentDomain.FriendlyName != exePaths.First()) + if (exePaths.Any() && AppDomain.CurrentDomain.FriendlyName != Path.GetFileNameWithoutExtension(exePaths.First())) { var msgBox = new GUIMessageBox(TextManager.Get("Error"), TextManager.GetWithVariables("IncorrectExe", new string[2] { "[selectedpackage]", "[exename]" }, new string[2] { contentPackage.Name, exePaths.First() }), @@ -763,7 +780,7 @@ namespace Barotrauma //reset accumulator if loading // -> less choppy loading screens because the screen is rendered after each update // -> no pause caused by leftover time in the accumulator when starting a new shift - GameMain.ResetFrameTime(); + ResetFrameTime(); if (!TitleScreen.PlayingSplashScreen) { @@ -777,11 +794,33 @@ namespace Barotrauma } #if DEBUG - if (TitleScreen.LoadState >= 100.0f && !TitleScreen.PlayingSplashScreen && Config.AutomaticQuickStartEnabled && FirstLoad) + if (TitleScreen.LoadState >= 100.0f && !TitleScreen.PlayingSplashScreen && (Config.AutomaticQuickStartEnabled || Config.AutomaticCampaignLoadEnabled) && FirstLoad && !PlayerInput.KeyDown(Keys.LeftShift)) { loadingScreenOpen = false; FirstLoad = false; - MainMenuScreen.QuickStart(); + + if (Config.AutomaticQuickStartEnabled) + { + MainMenuScreen.QuickStart(); + } + else if (Config.AutomaticCampaignLoadEnabled) + { + IEnumerable saveFiles = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Singleplayer); + + if (saveFiles.Count() > 0) + { + saveFiles = saveFiles.OrderBy(file => File.GetLastWriteTime(file)); + try + { + SaveUtil.LoadGame(saveFiles.Last()); + } + catch (Exception e) + { + DebugConsole.ThrowError("Loading save \"" + saveFiles.Last() + "\" failed", e); + return; + } + } + } } #endif @@ -845,6 +884,12 @@ namespace Barotrauma { ((GUIMessageBox)GUIMessageBox.VisibleBox).Close(); } + else if (GUIMessageBox.VisibleBox?.UserData is RoundSummary roundSummary && + roundSummary.ContinueButton != null && + roundSummary.ContinueButton.Visible) + { + GUIMessageBox.MessageBoxes.Remove(GUIMessageBox.VisibleBox); + } else if (Tutorial.Initialized && Tutorial.ContentRunning) { (GameSession.GameMode as TutorialMode).Tutorial.CloseActiveContentGUI(); @@ -868,7 +913,7 @@ namespace Barotrauma GUI.TogglePauseMenu(); } - bool itemHudActive() + static bool itemHudActive() { if (Character.Controlled?.SelectedConstruction == null) { return false; } return @@ -878,7 +923,7 @@ namespace Barotrauma } #if DEBUG - if (GameMain.NetworkMember == null) + if (NetworkMember == null) { if (PlayerInput.KeyHit(Keys.P) && !(GUI.KeyboardDispatcher.Subscriber is GUITextBox)) { @@ -890,6 +935,10 @@ namespace Barotrauma GUI.ClearUpdateList(); Paused = (DebugConsole.IsOpen || GUI.PauseMenuOpen || GUI.SettingsMenuOpen || Tutorial.ContentRunning || DebugConsole.Paused) && (NetworkMember == null || !NetworkMember.GameStarted); + if (GameSession?.GameMode != null && GameSession.GameMode.Paused) + { + Paused = true; + } #if !DEBUG if (NetworkMember == null && !WindowActive && !Paused && true && Screen.Selected != MainMenuScreen && Config.PauseOnFocusLost) @@ -921,7 +970,7 @@ namespace Barotrauma { (GameSession.GameMode as TutorialMode).Update((float)Timing.Step); } - else if (DebugConsole.Paused) + else { if (Screen.Selected.Cam == null) { @@ -929,7 +978,7 @@ namespace Barotrauma } else { - Screen.Selected.Cam.MoveCamera((float)Timing.Step); + Screen.Selected.Cam.MoveCamera((float)Timing.Step, allowMove: DebugConsole.Paused, allowZoom: DebugConsole.Paused); } } @@ -1027,41 +1076,53 @@ namespace Barotrauma msgBox.Buttons[0].OnClicked += msgBox.Close; msgBox.Buttons[1].OnClicked += msgBox.Close; } - } public static void QuitToMainMenu(bool save) { if (save) { - SaveUtil.SaveGame(GameMain.GameSession.SavePath); + if (GameSession.Submarine != null && !GameSession.Submarine.Removed) + { + GameSession.SubmarineInfo = new SubmarineInfo(GameSession.Submarine); + } + + // Update store stock when saving and quitting in an outpost (normally updated when CampaignMode.End() is called) + if (GameSession?.Campaign is SinglePlayerCampaign campaign && Level.IsLoadedOutpost && campaign.Map?.CurrentLocation != null && campaign.CargoManager != null) + { + campaign.Map.CurrentLocation.AddToStock(campaign.CargoManager.SoldItems); + campaign.CargoManager.ClearSoldItemsProjSpecific(); + campaign.Map.CurrentLocation.RemoveFromStock(campaign.CargoManager.PurchasedItems); + } + + SaveUtil.SaveGame(GameSession.SavePath); } - if (GameMain.Client != null) + if (Client != null) { - GameMain.Client.Disconnect(); - GameMain.Client = null; + Client.Disconnect(); + Client = null; } CoroutineManager.StopCoroutines("EndCinematic"); - if (GameMain.GameSession != null) + if (GameSession != null) { if (Tutorial.Initialized) { - ((TutorialMode)GameMain.GameSession.GameMode).Tutorial?.Stop(); + ((TutorialMode)GameSession.GameMode).Tutorial?.Stop(); } if (GameSettings.SendUserStatistics) { - Mission mission = GameMain.GameSession.Mission; + Mission mission = GameSession.Mission; GameAnalyticsManager.AddDesignEvent("QuitRound:" + (save ? "Save" : "NoSave")); GameAnalyticsManager.AddDesignEvent("EndRound:" + (mission == null ? "NoMission" : (mission.Completed ? "MissionCompleted" : "MissionFailed"))); } - GameMain.GameSession = null; } GUIMessageBox.CloseAll(); - GameMain.MainMenuScreen.Select(); + MainMenuScreen.Select(); + GameSession = null; } public void ShowCampaignDisclaimer(Action onContinue = null) @@ -1071,12 +1132,7 @@ namespace Barotrauma msgBox.Buttons[0].OnClicked = (btn, userdata) => { - var roadMap = new GUIMessageBox(TextManager.Get("CampaignRoadMapTitle"), TextManager.Get("CampaignRoadMapText"), - new string[] { TextManager.Get("Back"), TextManager.Get("OK") }); - roadMap.Buttons[0].OnClicked += roadMap.Close; - roadMap.Buttons[0].OnClicked += (_, __) => { ShowCampaignDisclaimer(onContinue); return true; }; - roadMap.Buttons[1].OnClicked += roadMap.Close; - roadMap.Buttons[1].OnClicked += (_, __) => { onContinue?.Invoke(); return true; }; + ShowOpenUrlInWebBrowserPrompt("https://trello.com/b/hBXI8ltN/barotrauma-roadmap-known-issues"); return true; }; msgBox.Buttons[0].OnClicked += msgBox.Close; @@ -1094,9 +1150,9 @@ namespace Barotrauma linkHolder.RectTransform.MaxSize = new Point(int.MaxValue, linkHolder.Rect.Height); List> links = new List>() { - new Pair(TextManager.Get("EditorDisclaimerWikiLink"),TextManager.Get("EditorDisclaimerWikiUrl")), - new Pair(TextManager.Get("EditorDisclaimerDiscordLink"),TextManager.Get("EditorDisclaimerDiscordUrl")), - new Pair(TextManager.Get("EditorDisclaimerForumLink"),TextManager.Get("EditorDisclaimerForumUrl")), + new Pair(TextManager.Get("EditorDisclaimerWikiLink"), TextManager.Get("EditorDisclaimerWikiUrl")), + new Pair(TextManager.Get("EditorDisclaimerDiscordLink"), TextManager.Get("EditorDisclaimerDiscordUrl")), + new Pair(TextManager.Get("EditorDisclaimerForumLink"), TextManager.Get("EditorDisclaimerForumUrl")), }; foreach (var link in links) { @@ -1124,8 +1180,10 @@ namespace Barotrauma return; } - var msgBox = new GUIMessageBox(TextManager.Get("bugreportbutton"), ""); - msgBox.UserData = "bugreporter"; + var msgBox = new GUIMessageBox(TextManager.Get("bugreportbutton"), "") + { + UserData = "bugreporter" + }; var linkHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), msgBox.Content.RectTransform)) { Stretch = true, RelativeSpacing = 0.025f }; linkHolder.RectTransform.MaxSize = new Point(int.MaxValue, linkHolder.Rect.Height); @@ -1189,7 +1247,7 @@ namespace Barotrauma DebugConsole.ThrowError("Error while cleaning unnecessary save files", e); } - if (GameSettings.SendUserStatistics){ GameAnalytics.OnQuit(); } + if (GameSettings.SendUserStatistics) { GameAnalytics.OnQuit(); } if (GameSettings.SaveDebugConsoleLogs) { DebugConsole.SaveLogs(); } base.OnExiting(sender, args); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs new file mode 100644 index 000000000..6138a1f0b --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs @@ -0,0 +1,173 @@ +using Barotrauma.Extensions; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + partial class CargoManager + { + private class SoldEntity + { + public enum SellStatus + { + Confirmed, + Unconfirmed, + Local + } + + public Item Item { get; } + public SellStatus Status { get; set; } + + private SoldEntity(Item item, SellStatus status) + { + Item = item; + Status = status; + } + + public static SoldEntity CreateInSinglePlayer(Item item) => new SoldEntity(item, SellStatus.Confirmed); + public static SoldEntity CreateInMultiPlayer(Item item) => new SoldEntity(item, SellStatus.Local); + } + + private List SoldEntities { get; } = new List(); + + public List GetSellableItems(Character character) + { + if (character == null) { return new List(); } + + // Only consider items which have been: + // a) sold in singleplayer or confirmed by server (SellStatus.Confirmed); or + // b) sold locally in multiplier (SellStatus.Local), but the client has not received a campaing state update yet after selling them + var soldEntities = SoldEntities.Where(se => se.Status != SoldEntity.SellStatus.Unconfirmed); + + var sellables = Item.ItemList.FindAll(i => i?.Prefab != null && !i.Removed && + i.GetRootInventoryOwner() == character && + !i.SpawnedInOutpost && + (i.ContainedItems == null || i.ContainedItems.None() || i.ContainedItems.All(ci => soldEntities.Any(se => se.Item == ci))) && + i.IsFullCondition && soldEntities.None(se => se.Item == i)); + + // Prevent selling things like battery cells from headsets and oxygen tanks from diving masks + var slots = new List() { InvSlotType.Head, InvSlotType.OuterClothes, InvSlotType.Headset }; + foreach (InvSlotType slot in slots) + { + var index = character.Inventory.FindLimbSlot(slot); + if (character.Inventory.Items[index] is Item item && item.ContainedItems != null) + { + foreach (Item containedItem in item.ContainedItems) + { + if (containedItem != null) + { + sellables.Remove(containedItem); + } + } + } + } + + return sellables; + } + + public void SetItemsInBuyCrate(List items) + { + ItemsInBuyCrate.Clear(); + ItemsInBuyCrate.AddRange(items); + OnItemsInBuyCrateChanged?.Invoke(); + } + + public void SetSoldItems(List items) + { + SoldItems.Clear(); + SoldItems.AddRange(items); + + foreach (SoldEntity se in SoldEntities) + { + if (se.Status == SoldEntity.SellStatus.Confirmed) { continue; } + if (SoldItems.Any(si => si.ID == se.Item.ID && si.ItemPrefab == se.Item.Prefab && (GameMain.Client == null || GameMain.Client.ID == si.SellerID))) + { + se.Status = SoldEntity.SellStatus.Confirmed; + } + else + { + se.Status = SoldEntity.SellStatus.Unconfirmed; + } + } + + OnSoldItemsChanged?.Invoke(); + } + + public void ModifyItemQuantityInSellCrate(ItemPrefab itemPrefab, int changeInQuantity) + { + PurchasedItem itemToSell = ItemsInSellCrate.Find(i => i.ItemPrefab == itemPrefab); + if (itemToSell != null) + { + itemToSell.Quantity += changeInQuantity; + if (itemToSell.Quantity < 1) + { + ItemsInSellCrate.Remove(itemToSell); + } + } + else if (changeInQuantity > 0) + { + itemToSell = new PurchasedItem(itemPrefab, changeInQuantity); + ItemsInSellCrate.Add(itemToSell); + } + OnItemsInSellCrateChanged?.Invoke(); + } + + public void SellItems(List itemsToSell) + { + var itemsInInventory = GetSellableItems(Character.Controlled); + var canAddToRemoveQueue = campaign.IsSinglePlayer && Entity.Spawner != null; + var sellerId = GameMain.Client?.ID ?? 0; + + foreach (PurchasedItem item in itemsToSell) + { + var itemValue = GetSellValueAtCurrentLocation(item.ItemPrefab, quantity: item.Quantity); + + // check if the store can afford the item + if (location.StoreCurrentBalance < itemValue) { continue; } + + var matchingItems = itemsInInventory.FindAll(i => i.Prefab == item.ItemPrefab); + if (matchingItems.Count <= item.Quantity) + { + foreach (Item i in matchingItems) + { + SoldItems.Add(new SoldItem(i.Prefab, i.ID, canAddToRemoveQueue, sellerId)); + SoldEntities.Add(campaign.IsSinglePlayer ? SoldEntity.CreateInSinglePlayer(i) : SoldEntity.CreateInMultiPlayer(i)); + if (canAddToRemoveQueue) { Entity.Spawner.AddToRemoveQueue(i); } + } + } + else + { + for (int i = 0; i < item.Quantity; i++) + { + var matchingItem = matchingItems[i]; + SoldItems.Add(new SoldItem(matchingItem.Prefab, matchingItem.ID, canAddToRemoveQueue, sellerId)); + SoldEntities.Add(campaign.IsSinglePlayer ? SoldEntity.CreateInSinglePlayer(matchingItem) : SoldEntity.CreateInMultiPlayer(matchingItem)); + if (canAddToRemoveQueue) { Entity.Spawner.AddToRemoveQueue(matchingItem); } + } + } + + // Exchange money + campaign.Map.CurrentLocation.StoreCurrentBalance -= itemValue; + campaign.Money += itemValue; + + // Remove from the sell crate + if (ItemsInSellCrate.Find(pi => pi.ItemPrefab == item.ItemPrefab) is { } itemToSell) + { + itemToSell.Quantity -= item.Quantity; + if (itemToSell.Quantity < 1) + { + ItemsInSellCrate.Remove(itemToSell); + } + } + } + + OnSoldItemsChanged?.Invoke(); + } + + public void ClearSoldItemsProjSpecific() + { + SoldItems.Clear(); + SoldEntities.Clear(); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 044ec7c39..3a9977d69 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -20,9 +20,6 @@ namespace Barotrauma /// const float CharacterWaitOnSwitch = 10.0f; - private readonly List characterInfos = new List(); - private readonly List characters = new List(); - private Point screenResolution; #region UI @@ -30,6 +27,7 @@ namespace Barotrauma public GUIComponent ReportButtonFrame { get; set; } private GUIFrame guiFrame; + private GUIComponent crewAreaWithButtons; private GUIFrame crewArea; private GUIListBox crewList; private GUIButton commandButton, toggleCrewButton; @@ -72,19 +70,7 @@ namespace Barotrauma public CrewManager(XElement element, bool isSinglePlayer) : this(isSinglePlayer) { - foreach (XElement subElement in element.Elements()) - { - if (!subElement.Name.ToString().Equals("character", StringComparison.OrdinalIgnoreCase)) { continue; } - - var characterInfo = new CharacterInfo(subElement); - characterInfos.Add(characterInfo); - foreach (XElement invElement in subElement.Elements()) - { - if (!invElement.Name.ToString().Equals("inventory", StringComparison.OrdinalIgnoreCase)) { continue; } - characterInfo.InventoryData = invElement; - break; - } - } + AddCharacterElements(element); } partial void InitProjectSpecific() @@ -96,7 +82,7 @@ namespace Barotrauma #region Crew Area - var crewAreaWithButtons = new GUIFrame( + crewAreaWithButtons = new GUIFrame( HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.CrewArea, guiFrame.RectTransform), style: null, color: Color.Transparent) @@ -326,43 +312,6 @@ namespace Barotrauma return characterInfos; } - public void AddCharacter(Character character) - { - if (character.Removed) - { - DebugConsole.ThrowError("Tried to add a removed character to CrewManager!\n" + Environment.StackTrace); - return; - } - if (character.IsDead) - { - DebugConsole.ThrowError("Tried to add a dead character to CrewManager!\n" + Environment.StackTrace); - return; - } - - if (!characters.Contains(character)) - { - characters.Add(character); - } - if (!characterInfos.Contains(character.Info)) - { - characterInfos.Add(character.Info); - } - - AddCharacterToCrewList(character); - DisplayCharacterOrder(character, character.CurrentOrder, character.CurrentOrderOption); - } - - public void AddCharacterInfo(CharacterInfo characterInfo) - { - if (characterInfos.Contains(characterInfo)) - { - DebugConsole.ThrowError("Tried to add the same character info to CrewManager twice.\n" + Environment.StackTrace); - return; - } - - characterInfos.Add(characterInfo); - } - /// /// Remove the character from the crew (and crew menus). /// @@ -379,15 +328,6 @@ namespace Barotrauma if (removeInfo) { characterInfos.Remove(character.Info); } } - /// - /// Remove info of a selected character. The character will not be visible in any menus or the round summary. - /// - /// - public void RemoveCharacterInfo(CharacterInfo characterInfo) - { - characterInfos.Remove(characterInfo); - } - private void AddCharacterToCrewList(Character character) { if (character == null) { return; } @@ -516,7 +456,7 @@ namespace Barotrauma }; new GUIImage( new RectTransform(Vector2.One, soundIcons.RectTransform), - GUI.Style.GetComponentStyle("GUISoundIcon").Sprites[GUIComponent.ComponentState.None].FirstOrDefault().Sprite, + GUI.Style.GetComponentStyle("GUISoundIcon").GetDefaultSprite(), scaleToFit: true) { CanBeFocused = false, @@ -630,6 +570,22 @@ namespace Barotrauma ChatBox.AddMessage(ChatMessage.Create(senderName, text, messageType, sender)); } + public void AddSinglePlayerChatMessage(ChatMessage message) + { + if (!IsSinglePlayer) + { + DebugConsole.ThrowError("Cannot add messages to single player chat box in multiplayer mode!\n" + Environment.StackTrace); + return; + } + if (string.IsNullOrEmpty(message.Text)) { return; } + + if (message.Sender != null) + { + GameMain.GameSession.CrewManager.SetCharacterSpeaking(message.Sender); + } + ChatBox.AddMessage(message); + } + private WifiComponent GetHeadset(Character character, bool requireEquipped) { if (character?.Inventory == null) return null; @@ -861,27 +817,6 @@ namespace Barotrauma return characterComponent?.FindChild(c => c?.UserData is OrderInfo orderInfo && orderInfo.ComponentIdentifier == "previousorder"); } - private struct OrderInfo - { - public string ComponentIdentifier { get; set; } - public Order Order { get; private set; } - public string OrderOption { get; private set; } - - public OrderInfo(Order order, string orderOption) - { - ComponentIdentifier = "currentorder"; - Order = order; - OrderOption = orderOption; - } - - public OrderInfo(OrderInfo orderInfo) - { - ComponentIdentifier = "previousorder"; - Order = orderInfo.Order; - OrderOption = orderInfo.OrderOption; - } - } - #endregion #region Updating and drawing the UI @@ -1123,6 +1058,7 @@ namespace Barotrauma public void AddToGUIUpdateList() { if (GUI.DisableHUD) { return; } + if (CoroutineManager.IsCoroutineRunning("LevelTransition") || CoroutineManager.IsCoroutineRunning("SubmarineTransition")) { return; } commandFrame?.AddToGUIUpdateList(); @@ -1145,6 +1081,8 @@ namespace Barotrauma } } + crewAreaWithButtons.Visible = !(GameMain.GameSession?.GameMode is CampaignMode campaign) || (!campaign.ForceMapUI && !campaign.ShowCampaignUI); + guiFrame.AddToGUIUpdateList(); contextMenu?.AddToGUIUpdateList(false, 1); subContextMenu?.AddToGUIUpdateList(false, 1); @@ -1170,6 +1108,7 @@ namespace Barotrauma private void SelectCharacter(Character character) { + if (ConversationAction.IsDialogOpen) { return; } if (!AllowCharacterSwitch) { return; } //make the previously selected character wait in place for some time //(so they don't immediately start idling and walking away from their station) @@ -1256,7 +1195,7 @@ namespace Barotrauma WasCommandInterfaceDisabledThisUpdate = false; if (PlayerInput.KeyDown(InputType.Command) && (GUI.KeyboardDispatcher.Subscriber == null || GUI.KeyboardDispatcher.Subscriber == crewList) && - commandFrame == null && !clicklessSelectionActive && CanIssueOrders) + commandFrame == null && !clicklessSelectionActive && CanIssueOrders && !(GameMain.GameSession?.Campaign?.ShowCampaignUI ?? false)) { if (PlayerInput.KeyDown(Keys.LeftShift) || PlayerInput.KeyDown(Keys.RightShift)) { @@ -1574,32 +1513,32 @@ namespace Barotrauma get { #if DEBUG - return Character.Controlled == null || Character.Controlled.Info != null && Character.Controlled.SpeechImpediment < 100.0f; -#else - return Character.Controlled?.Info != null && Character.Controlled.SpeechImpediment < 100.0f; + if (Character.Controlled == null) { return true; } #endif + return Character.Controlled?.Info != null && Character.Controlled.SpeechImpediment < 100.0f; + } } private bool CanSomeoneHearCharacter() { #if DEBUG - return true; -#else - return Character.Controlled != null && characters.Any(c => c != Character.Controlled && c.CanHearCharacter(Character.Controlled)); + if (Character.Controlled == null) { return true; } #endif + return Character.Controlled != null && characters.Any(c => c != Character.Controlled && c.CanHearCharacter(Character.Controlled)); } private Entity FindEntityContext() { - if (Character.Controlled?.FocusedCharacter != null) + if (Character.Controlled?.FocusedCharacter is Character focusedCharacter && !focusedCharacter.IsDead && + HumanAIController.IsFriendly(Character.Controlled, focusedCharacter) && Character.Controlled.TeamID == focusedCharacter.TeamID) { if (Character.Controlled?.FocusedItem != null) { Vector2 mousePos = GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition); - if (Vector2.Distance(mousePos, Character.Controlled.FocusedCharacter.WorldPosition) < Vector2.Distance(mousePos, Character.Controlled.FocusedItem.WorldPosition)) + if (Vector2.Distance(mousePos, focusedCharacter.WorldPosition) < Vector2.Distance(mousePos, Character.Controlled.FocusedItem.WorldPosition)) { - return Character.Controlled.FocusedCharacter; + return focusedCharacter; } else { @@ -1608,7 +1547,7 @@ namespace Barotrauma } else { - return Character.Controlled.FocusedCharacter; + return focusedCharacter; } } @@ -1677,24 +1616,22 @@ namespace Barotrauma "CommandNodeContainer", scaleToFit: true) { - Color = characterContext.Info.Job.Prefab.UIColor * nodeColorMultiplier, - HoverColor = characterContext.Info.Job.Prefab.UIColor, + Color = characterContext.Info?.Job?.Prefab != null ? characterContext.Info.Job.Prefab.UIColor * nodeColorMultiplier : Color.White, + HoverColor = characterContext.Info?.Job?.Prefab != null ? characterContext.Info.Job.Prefab.UIColor : Color.White, UserData = "colorsource" }; // Character icon - new GUICustomComponent( + var characterIcon = new GUICustomComponent( new RectTransform(Vector2.One, startNode.RectTransform, anchor: Anchor.Center), (spriteBatch, _) => { - if (!(entityContext is Character character)) { return; } + if (!(entityContext is Character character) || character?.Info == null) { return; } var node = startNode; character.Info.DrawJobIcon(spriteBatch, new Rectangle((int)(node.Rect.X + node.Rect.Width * 0.5f), (int)(node.Rect.Y + node.Rect.Height * 0.1f), (int)(node.Rect.Width * 0.6f), (int)(node.Rect.Height * 0.8f))); character.Info.DrawIcon(spriteBatch, new Vector2(node.Rect.X + node.Rect.Width * 0.35f, node.Center.Y), node.Rect.Size.ToVector2() * 0.7f); - }) - { - ToolTip = characterContext.Info.DisplayName + " (" + characterContext.Info.Job.Name + ")" - }; + }); + SetCharacterTooltip(characterIcon, entityContext as Character); } SetCenterNode(startNode); @@ -1934,7 +1871,7 @@ namespace Barotrauma c.HoverColor = c.Color; c.PressedColor = c.Color; c.SelectedColor = c.Color; - c.ToolTip = characterContext != null ? characterContext.Info.DisplayName + " (" + characterContext.Info.Job.Name + ")" : null; + SetCharacterTooltip(c, characterContext); } node.OnClicked = null; centerNode = node; @@ -2040,7 +1977,7 @@ namespace Barotrauma var 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 - if ((Character.Controlled == null || Character.Controlled.Info.Job.Prefab != JobPrefab.Get("engineer")) && + if ((Character.Controlled == null || Character.Controlled.Info?.Job?.Prefab != JobPrefab.Get("engineer")) && reactorOutput < float.Epsilon && characters.None(c => c.SelectedConstruction == reactor.Item)) { var order = new Order(Order.GetPrefab("operatereactor"), reactor.Item, reactor, Character.Controlled); @@ -2053,7 +1990,7 @@ namespace Barotrauma // 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 (shortcutNodes.Count < maxShorcutNodeCount && (Character.Controlled == null || Character.Controlled.Info.Job.Prefab != JobPrefab.Get("captain")) && + if (shortcutNodes.Count < maxShorcutNodeCount && (Character.Controlled == null || Character.Controlled.Info?.Job?.Prefab != JobPrefab.Get("captain")) && sub.GetItems(false).Find(i => i.HasTag("navterminal") && !i.NonInteractable) is Item nav && characters.None(c => c.SelectedConstruction == nav) && nav.GetComponent() is Steering steering && steering.Voltage > steering.MinVoltage) { @@ -2063,7 +2000,7 @@ namespace Barotrauma // If player is not a security officer AND invaders are reported // --> Create shorcut node for Fight Intruders order - if (shortcutNodes.Count < maxShorcutNodeCount && (Character.Controlled == null || Character.Controlled.Info.Job.Prefab != JobPrefab.Get("securityofficer")) && + if (shortcutNodes.Count < maxShorcutNodeCount && (Character.Controlled == null || Character.Controlled.Info?.Job?.Prefab != JobPrefab.Get("securityofficer")) && (Order.GetPrefab("reportintruders") is Order reportIntruders && ActiveOrders.Any(o => o.First.Prefab == reportIntruders))) { shortcutNodes.Add( @@ -2072,7 +2009,7 @@ namespace Barotrauma // If player is not a mechanic AND a breach has been reported // --> Create shorcut node for Fix Leaks order - if (shortcutNodes.Count < maxShorcutNodeCount && (Character.Controlled == null || Character.Controlled.Info.Job.Prefab != JobPrefab.Get("mechanic")) && + if (shortcutNodes.Count < maxShorcutNodeCount && (Character.Controlled == null || Character.Controlled.Info?.Job?.Prefab != JobPrefab.Get("mechanic")) && (Order.GetPrefab("reportbreach") is Order reportBreach && ActiveOrders.Any(o => o.First.Prefab == reportBreach))) { shortcutNodes.Add( @@ -2081,7 +2018,7 @@ namespace Barotrauma // If player is not an engineer AND broken devices have been reported // --> Create shortcut node for Repair Damaged Systems order - if (shortcutNodes.Count < maxShorcutNodeCount && (Character.Controlled == null || Character.Controlled.Info.Job.Prefab != JobPrefab.Get("engineer")) && + if (shortcutNodes.Count < maxShorcutNodeCount && (Character.Controlled == null || Character.Controlled.Info?.Job?.Prefab != JobPrefab.Get("engineer")) && (Order.GetPrefab("reportbrokendevices") is Order reportBrokenDevices && ActiveOrders.Any(o => o.First.Prefab == reportBrokenDevices))) { shortcutNodes.Add( @@ -2741,11 +2678,11 @@ namespace Barotrauma }; } -#if DEBUG - bool canHear = true; -#else bool canHear = character.CanHearCharacter(Character.Controlled); +#if DEBUG + if (Character.Controlled == null) { canHear = true; } #endif + if (!canHear) { node.CanBeFocused = orderIcon.CanBeFocused = false; @@ -2904,18 +2841,29 @@ namespace Barotrauma return sub; } + 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 + ")"; } + component.ToolTip = tooltip; + } + #region Crew Member Assignment Logic private Character GetCharacterForQuickAssignment(Order order) { + var controllingCharacter = Character.Controlled != null; #if !DEBUG - if (Character.Controlled == null) { return null; } + if (!controllingCharacter) { return null; } #endif - if (order.Category == OrderCategory.Operate && HumanAIController.IsItemOperatedByAnother(null, order.TargetItemComponent, out Character operatingCharacter)) + if (order.Category == OrderCategory.Operate && HumanAIController.IsItemOperatedByAnother(null, order.TargetItemComponent, out Character operatingCharacter) && + (!controllingCharacter || operatingCharacter.CanHearCharacter(Character.Controlled))) { return operatingCharacter; } - return GetCharactersSortedForOrder(order, false).FirstOrDefault() ?? Character.Controlled; + return GetCharactersSortedForOrder(order, false).FirstOrDefault(c => !controllingCharacter || c.CanHearCharacter(Character.Controlled)) ?? Character.Controlled; } private List GetCharactersForManualAssignment(Order order) @@ -2934,11 +2882,17 @@ namespace Barotrauma private IEnumerable GetCharactersSortedForOrder(Order order, bool includeSelf) { return characters.FindAll(c => Character.Controlled == null || ((includeSelf || c != Character.Controlled) && c.TeamID == Character.Controlled.TeamID)) + // 1. Prioritize those who are already ordered to operate the item target of the new 'operate' order .OrderByDescending(c => c.CurrentOrder != null && order.Category == OrderCategory.Operate && c.CurrentOrder.Identifier == order.Identifier && c.CurrentOrder.TargetEntity == order.TargetEntity) + // 2. Prioritize those who are currently dismissed .ThenByDescending(c => c.CurrentOrder == null || c.CurrentOrder.Identifier == dismissedOrderPrefab.Identifier) + // 3. Prioritize those who are not currently assigned with the same type of order (for example, when giving a 'Fix Leak' order, prioritize those who have a different order) .ThenBy(c => c.CurrentOrder != null && c.CurrentOrder.Identifier == order.Identifier && c.CurrentOrder.TargetEntity == order.TargetEntity) + // 4. Prioritize those with the appropriate job for the order .ThenByDescending(c => order.HasAppropriateJob(c)) + // 5. Prioritize those with the lowest "weight" of the current order .ThenBy(c => c.CurrentOrder?.Weight) + // 6. Prioritize those with the best skill for the order .ThenByDescending(c => c.GetSkillLevel(order.AppropriateSkill)); } @@ -3013,40 +2967,7 @@ namespace Barotrauma public void InitSinglePlayerRound() { crewList.ClearChildren(); - characters.Clear(); - - WayPoint[] waypoints = WayPoint.SelectCrewSpawnPoints(characterInfos, Submarine.MainSub); - - for (int i = 0; i < waypoints.Length; i++) - { - Character character; - character = Character.Create(characterInfos[i], waypoints[i].WorldPosition, characterInfos[i].Name); - - if (character.Info != null) - { - if (!character.Info.StartItemsGiven && character.Info.InventoryData != null) - { - DebugConsole.ThrowError($"Error when initializing a single player round: character \"{character.Name}\" has not been given their initial items but has saved inventory data. Using the saved inventory data instead of giving the character new items."); - } - if (character.Info.InventoryData != null) - { - character.Info.SpawnInventoryItems(character.Inventory, character.Info.InventoryData); - } - else if (!character.Info.StartItemsGiven) - { - character.GiveJobItems(waypoints[i]); - } - character.Info.StartItemsGiven = true; - } - - AddCharacter(character); - if (i == 0) - { - Character.Controlled = character; - } - } - - conversationTimer = Rand.Range(5.0f, 10.0f); + InitRound(); } public void EndRound() @@ -3072,10 +2993,9 @@ namespace Barotrauma foreach (CharacterInfo ci in characterInfos) { var infoElement = ci.Save(element); - if (ci.InventoryData != null) - { - infoElement.Add(ci.InventoryData); - } + if (ci.InventoryData != null) { infoElement.Add(ci.InventoryData); } + if (ci.HealthData != null) { infoElement.Add(ci.HealthData); } + if (ci.LastControlled) { infoElement.Add(new XAttribute("lastcontrolled", true)); } } parentElement.Add(element); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index d2343012c..71de2e8f2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -1,10 +1,65 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.Linq; +using System.Threading.Tasks; namespace Barotrauma { abstract partial class CampaignMode : GameMode { + protected bool crewDead; + + protected Color overlayColor; + protected string overlayText, overlayTextBottom; + protected Color overlayTextColor; + protected Sprite overlaySprite; + + protected GUIButton endRoundButton; + + public GUIButton EndRoundButton => endRoundButton; + + protected GUIFrame campaignUIContainer; + public CampaignUI CampaignUI; + + public bool ForceMapUI + { + get; + protected set; + } + + public override bool Paused + { + get { return ForceMapUI || CoroutineManager.IsCoroutineRunning("LevelTransition"); } + } + + private bool showCampaignUI; + private bool wasChatBoxOpen; + public bool ShowCampaignUI + { + get { return showCampaignUI; } + set + { + if (value == showCampaignUI) { return; } + var chatBox = CrewManager?.ChatBox ?? GameMain.Client?.ChatBox; + if (value) + { + if (chatBox != null) + { + wasChatBoxOpen = chatBox.ToggleOpen; + chatBox.ToggleOpen = false; + } + } + else if (chatBox != null) + { + chatBox.ToggleOpen = wasChatBoxOpen; + } + showCampaignUI = value; + } + } + public override void ShowStartMessage() { if (Mission == null) return; @@ -15,5 +70,216 @@ namespace Barotrauma UserData = "missionstartmessage" }; } + + /// + /// 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() + { + //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.ManageCampaign) || + GameMain.Client.ConnectedClients.Count == 1 || + GameMain.Client.IsServerOwner || + GameMain.Client.ConnectedClients.None(c => + c.InGame && (c.IsOwner || c.HasPermission(ClientPermissions.ManageCampaign))); + } + + public override void Draw(SpriteBatch spriteBatch) + { + if (overlayColor.A > 0) + { + if (overlaySprite != null) + { + GUI.DrawRectangle(spriteBatch, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), Color.Black * (overlayColor.A / 255.0f), isFilled: true); + float scale = Math.Max(GameMain.GraphicsWidth / overlaySprite.size.X, GameMain.GraphicsHeight / overlaySprite.size.Y); + overlaySprite.Draw(spriteBatch, new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) / 2, overlayColor, overlaySprite.size / 2, scale: scale); + } + else + { + GUI.DrawRectangle(spriteBatch, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), overlayColor, isFilled: true); + } + if (!string.IsNullOrEmpty(overlayText) && overlayTextColor.A > 0) + { + var backgroundSprite = GUI.Style.GetComponentStyle("CommandBackground").GetDefaultSprite(); + Vector2 centerPos = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) / 2; + backgroundSprite.Draw(spriteBatch, + centerPos, + Color.White * (overlayTextColor.A / 255.0f), + origin: backgroundSprite.size / 2, + rotate: 0.0f, + scale: new Vector2(1.5f, 0.7f) * (GameMain.GraphicsWidth / 3 / backgroundSprite.size.X)); + + string wrappedText = ToolBox.WrapText(overlayText, GameMain.GraphicsWidth / 3, GUI.Font); + Vector2 textSize = GUI.Font.MeasureString(wrappedText); + Vector2 textPos = centerPos - textSize / 2; + GUI.DrawString(spriteBatch, textPos + Vector2.One, wrappedText, Color.Black * (overlayTextColor.A / 255.0f)); + GUI.DrawString(spriteBatch, textPos, wrappedText, overlayTextColor); + + if (!string.IsNullOrEmpty(overlayTextBottom)) + { + Vector2 bottomTextPos = centerPos + new Vector2(0.0f, textSize.Y + 30 * 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); + } + } + } + + if (GUI.DisableHUD || GUI.DisableUpperHUD || ForceMapUI || CoroutineManager.IsCoroutineRunning("LevelTransition")) + { + endRoundButton.Visible = false; + return; + } + if (Submarine.MainSub == null) { return; } + + endRoundButton.Visible = false; + var availableTransition = GetAvailableTransition(out _, out Submarine leavingSub); + string buttonText = ""; + switch (availableTransition) + { + case TransitionType.ProgressToNextLocation: + case TransitionType.ProgressToNextEmptyLocation: + if (Level.Loaded.EndOutpost == null || !Level.Loaded.EndOutpost.DockedTo.Contains(leavingSub)) + { + buttonText = TextManager.GetWithVariable("EnterLocation", "[locationname]", Level.Loaded.EndLocation?.Name ?? "[ERROR]"); + endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI; + } + break; + case TransitionType.LeaveLocation: + // not sure why this can happen at an outpost but it apparently can in multiplayer + buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); + endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI; + break; + case TransitionType.ReturnToPreviousLocation: + case TransitionType.ReturnToPreviousEmptyLocation: + if (Level.Loaded.StartOutpost == null || !Level.Loaded.StartOutpost.DockedTo.Contains(leavingSub)) + { + buttonText = TextManager.GetWithVariable("EnterLocation", "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); + endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI; + } + + break; + 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))) + { + buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); + endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI; + } + else + { + endRoundButton.Visible = false; + } + break; + } + + if (endRoundButton.Visible) + { + endRoundButton.Text = ToolBox.LimitString(buttonText, endRoundButton.Font, endRoundButton.Rect.Width - 5); + if (endRoundButton.Text != buttonText) + { + endRoundButton.ToolTip = buttonText; + } + endRoundButton.Enabled = AllowedToEndRound(); + } + + endRoundButton.DrawManually(spriteBatch); + } + + public Task SelectSummaryScreen(RoundSummary roundSummary, LevelData newLevel, bool mirror, Action action) + { + var roundSummaryScreen = RoundSummaryScreen.Select(overlaySprite, roundSummary); + + GUI.ClearCursorWait(); + + var loadTask = Task.Run(async () => + { + await Task.Yield(); + Rand.ThreadId = System.Threading.Thread.CurrentThread.ManagedThreadId; + GameMain.GameSession.StartRound(newLevel, mirrorLevel: mirror); + Rand.ThreadId = 0; + }); + TaskPool.Add("AsyncCampaignStartRound", loadTask, (t) => + { + overlayColor = Color.Transparent; + action?.Invoke(); + }); + + return loadTask; + } + + partial void NPCInteractProjSpecific(Character npc, Character interactor) + { + if (npc == null || interactor == null) { return; } + + switch (npc.CampaignInteractionType) + { + case InteractionType.None: + case InteractionType.Talk: + return; + case InteractionType.Upgrade when !UpgradeManager.CanUpgradeSub(): + UpgradeManager.CreateUpgradeErrorMessage(TextManager.Get("Dialog.CantUpgrade"), IsSinglePlayer, npc); + return; + case InteractionType.Crew when GameMain.NetworkMember != null: + CampaignUI.CrewManagement.SendCrewState(false); + goto default; + default: + ShowCampaignUI = true; + CampaignUI.SelectTab(npc.CampaignInteractionType); + CampaignUI.UpgradeStore?.RefreshAll(); + break; + } + } + + public override void AddToGUIUpdateList() + { + if (ShowCampaignUI || ForceMapUI) + { + campaignUIContainer?.AddToGUIUpdateList(); + if (CampaignUI?.UpgradeStore?.HoveredItem != null) + { + if (CampaignUI.SelectedTab != InteractionType.Upgrade) { return; } + CampaignUI?.UpgradeStore?.ItemInfoFrame.AddToGUIUpdateList(order: 1); + } + } + base.AddToGUIUpdateList(); + CrewManager.AddToGUIUpdateList(); + endRoundButton.AddToGUIUpdateList(); + } + + public override void Update(float deltaTime) + { + base.Update(deltaTime); + + if (PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Escape)) + { + GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData is RoundSummary); + } + + if (ShowCampaignUI || ForceMapUI) + { + CampaignUI?.Update(deltaTime); + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Data/CampaignMetadata.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Data/CampaignMetadata.cs new file mode 100644 index 000000000..143f88789 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Data/CampaignMetadata.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace Barotrauma +{ + internal partial class CampaignMetadata + { + private const int MaxDrawnElements = 12; + + public void DebugDraw(SpriteBatch spriteBatch, Vector2 pos, int debugDrawMetadataOffset, string[] ignoredMetadataInfo) + { + var campaignData = data; + foreach (string ignored in ignoredMetadataInfo) + { + if (!string.IsNullOrWhiteSpace(ignored)) + { + campaignData = campaignData.Where(pair => !pair.Key.StartsWith(ignored)).ToDictionary(i => i.Key, i => i.Value); + } + } + + int offset = 0;; + if (campaignData.Count > 0) + { + offset = debugDrawMetadataOffset % campaignData.Count; + if (offset < 0) { offset += campaignData.Count; } + } + + var text = "Campaign metadata:\n"; + + int max = 0; + for (int i = offset; i < campaignData.Count + offset; i++) + { + int index = i; + if (index >= campaignData.Count) { index -= campaignData.Count; } + + var (key, value) = campaignData.ElementAt(index); + + if (max < MaxDrawnElements) + { + text += $"{key.ColorizeObject()}: {value.ColorizeObject()}\n"; + max++; + } + else + { + text += "Use arrow keys to scroll"; + break; + } + } + + text = text.TrimEnd('\n'); + + List richTextDatas = RichTextData.GetRichTextData(text, out text) ?? new List(); + + Vector2 size = GUI.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()) + { + GUI.DrawStringWithColors(spriteBatch, infoPos, text, Color.White, richTextDatas, font: GUI.SmallFont); + } + else + { + GUI.DrawString(spriteBatch, infoPos, text, Color.White, font: GUI.SmallFont); + } + + float y = infoRect.Bottom + 16; + if (Campaign.Factions != null) + { + const string factionHeader = "Reputations"; + Vector2 factionHeaderSize = GUI.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); + 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); + y += nameSize.Y + 5; + + Color color = ToolBox.GradientLerp(faction.Reputation.NormalizedValue, Color.Red, Color.Yellow, Color.LightGreen); + GUI.DrawRectangle(spriteBatch, new Rectangle(GameMain.GraphicsWidth - 264, (int) y, (int)(faction.Reputation.NormalizedValue * 255), 10), color, isFilled: true); + GUI.DrawRectangle(spriteBatch, new Rectangle(GameMain.GraphicsWidth - 264, (int) y, 256, 10), Color.White); + y += 15; + } + } + + Location location = Campaign.Map?.CurrentLocation; + 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); + y += nameSize.Y + 5; + + float normalizedReputation = MathUtils.InverseLerp(location.Reputation.MinReputation, location.Reputation.MaxReputation, location.Reputation.Value); + Color color = ToolBox.GradientLerp(normalizedReputation, Color.Red, Color.Yellow, Color.LightGreen); + GUI.DrawRectangle(spriteBatch, new Rectangle(GameMain.GraphicsWidth - 264, (int) y, (int)(normalizedReputation * 255), 10), color, isFilled: true); + GUI.DrawRectangle(spriteBatch, new Rectangle(GameMain.GraphicsWidth - 264, (int) y, 256, 10), Color.White); + } + + 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 06ba7d188..9f509c46a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.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.IO; using System.Linq; using System.Xml.Linq; @@ -11,14 +13,30 @@ namespace Barotrauma { public bool SuppressStateSending = false; - private UInt16 startWatchmanID, endWatchmanID; + private UInt16 pendingSaveID = 1; + public UInt16 PendingSaveID + { + get + { + return pendingSaveID; + } + set + { + pendingSaveID = value; + //pending save ID 0 means "no save received yet" + //save IDs are always above 0, so we should never be waiting for 0 + if (pendingSaveID == 0) { pendingSaveID++; } + } + } + public static void StartCampaignSetup(IEnumerable saveFiles) { var parent = GameMain.NetLobbyScreen.CampaignSetupFrame; parent.ClearChildren(); parent.Visible = true; - GameMain.NetLobbyScreen.HighlightMode(2); + GameMain.NetLobbyScreen.HighlightMode( + GameMain.NetLobbyScreen.ModeList.Content.GetChildIndex(GameMain.NetLobbyScreen.ModeList.Content.GetChildByUserData(GameModePreset.MultiPlayerCampaign))); var layout = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform, Anchor.Center)) { @@ -38,7 +56,7 @@ namespace Barotrauma var newCampaignContainer = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.95f), campaignContainer.RectTransform, Anchor.Center), style: null); var loadCampaignContainer = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.95f), campaignContainer.RectTransform, Anchor.Center), style: null); - var campaignSetupUI = new CampaignSetupUI(true, newCampaignContainer, loadCampaignContainer, null, saveFiles); + GameMain.NetLobbyScreen.CampaignSetupUI = new CampaignSetupUI(true, newCampaignContainer, loadCampaignContainer, null, saveFiles); var newCampaignButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonContainer.RectTransform), TextManager.Get("NewCampaign"), style: "GUITabButton") @@ -68,94 +86,488 @@ namespace Barotrauma loadCampaignContainer.Visible = false; GUITextBlock.AutoScaleAndNormalize(newCampaignButton.TextBlock, loadCampaignButton.TextBlock); + + GameMain.NetLobbyScreen.CampaignSetupUI.StartNewGame = GameMain.Client.SetupNewCampaign; + GameMain.NetLobbyScreen.CampaignSetupUI.LoadGame = GameMain.Client.SetupLoadCampaign; + } + + partial void InitProjSpecific() + { + var buttonContainer = new GUILayoutGroup(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.ButtonAreaTop, GUICanvas.Instance), + isHorizontal: true, childAnchor: Anchor.CenterRight) + { + CanBeFocused = false + }; - campaignSetupUI.StartNewGame = GameMain.Client.SetupNewCampaign; - campaignSetupUI.LoadGame = GameMain.Client.SetupLoadCampaign; + int buttonHeight = (int)(GUI.Scale * 40); + int buttonWidth = GUI.IntScale(200); + + endRoundButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle((GameMain.GraphicsWidth / 2) - (buttonWidth / 2), HUDLayoutSettings.ButtonAreaTop.Center.Y - (buttonHeight / 2), buttonWidth, buttonHeight), GUICanvas.Instance), + TextManager.Get("EndRound"), textAlignment: Alignment.Center, style: "EndRoundButton") + { + Pulse = true, + TextBlock = + { + Shadow = true, + AutoScaleHorizontal = true + }, + OnClicked = (btn, userdata) => + { + var availableTransition = GetAvailableTransition(out _, out _); + if (Character.Controlled != null && + availableTransition == TransitionType.ReturnToPreviousLocation && + Character.Controlled?.Submarine == Level.Loaded?.StartOutpost) + { + GameMain.Client.RequestStartRound(); + } + else if (Character.Controlled != null && + availableTransition == TransitionType.ProgressToNextLocation && + Character.Controlled?.Submarine == Level.Loaded?.EndOutpost) + { + GameMain.Client.RequestStartRound(); + } + else + { + ShowCampaignUI = true; + if (CampaignUI == null) { InitCampaignUI(); } + CampaignUI.SelectTab(InteractionType.Map); + } + return true; + } + }; + buttonContainer.Recalculate(); + } + + private void InitCampaignUI() + { + campaignUIContainer = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: "InnerGlow", color: Color.Black); + CampaignUI = new CampaignUI(this, campaignUIContainer) + { + StartRound = () => + { + GameMain.Client.RequestStartRound(); + } + }; + } + + public override void Start() + { + base.Start(); + CoroutineManager.StartCoroutine(DoInitialCameraTransition(), "MultiplayerCampaign.DoInitialCameraTransition"); + } + + protected override void LoadInitialLevel() + { + //clients should never call this + throw new InvalidOperationException(""); + } + + + private IEnumerable DoInitialCameraTransition() + { + while (GameMain.Instance.LoadingScreenOpen) + { + yield return CoroutineStatus.Running; + } + + if (GameMain.Client.LateCampaignJoin) + { + GameMain.Client.LateCampaignJoin = false; + yield return CoroutineStatus.Success; + } + + Character prevControlled = Character.Controlled; + if (prevControlled?.AIController != null) + { + prevControlled.AIController.Enabled = false; + } + GUI.DisableHUD = true; + if (IsFirstRound) + { + Character.Controlled = null; + + if (prevControlled != null) + { + prevControlled.ClearInputs(); + } + + overlayColor = Color.LightGray; + 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) }); + float fadeInDuration = 1.0f; + float textDuration = 10.0f; + float timer = 0.0f; + while (timer < textDuration) + { + // Try to grab the controlled here to prevent inputs, assigned late on multiplayer + if (Character.Controlled != null) + { + prevControlled = Character.Controlled; + Character.Controlled = null; + prevControlled.ClearInputs(); + } + overlayTextColor = Color.Lerp(Color.Transparent, Color.White, (timer - 1.0f) / fadeInDuration); + timer = Math.Min(timer + CoroutineManager.DeltaTime, textDuration); + yield return CoroutineStatus.Running; + } + var transition = new CameraTransition(prevControlled, GameMain.GameScreen.Cam, + null, null, + fadeOut: false, + duration: 5, + startZoom: 1.5f, endZoom: 1.0f) + { + AllowInterrupt = true, + RemoveControlFromCharacter = false + }; + fadeInDuration = 1.0f; + timer = 0.0f; + overlayTextColor = Color.Transparent; + overlayText = ""; + while (timer < fadeInDuration) + { + overlayColor = Color.Lerp(Color.LightGray, Color.Transparent, timer / fadeInDuration); + timer += CoroutineManager.DeltaTime; + yield return CoroutineStatus.Running; + } + overlayColor = Color.Transparent; + while (transition.Running) + { + yield return CoroutineStatus.Running; + } + + if (prevControlled != null) + { + Character.Controlled = prevControlled; + } + } + else + { + var transition = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, + null, null, + fadeOut: false, + duration: 5, + startZoom: 0.5f, endZoom: 1.0f) + { + AllowInterrupt = true, + RemoveControlFromCharacter = true + }; + while (transition.Running) + { + yield return CoroutineStatus.Running; + } + } + + if (prevControlled != null) + { + prevControlled.SelectedConstruction = null; + if (prevControlled.AIController != null) + { + prevControlled.AIController.Enabled = true; + } + } + GUI.DisableHUD = false; + yield return CoroutineStatus.Success; + } + + protected override IEnumerable DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror, List traitorResults = null) + { + yield return CoroutineStatus.Success; + } + + private IEnumerable DoLevelTransition() + { + SoundPlayer.OverrideMusicType = CrewManager.GetCharacters().Any(c => !c.IsDead) ? "endround" : "crewdead"; + SoundPlayer.OverrideMusicDuration = 18.0f; + + Level prevLevel = Level.Loaded; + + bool success = CrewManager.GetCharacters().Any(c => !c.IsDead); + crewDead = false; + + var continueButton = GameMain.GameSession.RoundSummary?.ContinueButton; + if (continueButton != null) + { + continueButton.Visible = false; + } + + Character.Controlled = null; + + yield return new WaitForSeconds(0.1f); + + GameMain.Client.EndCinematic?.Stop(); + var endTransition = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, null, + Alignment.Center, + fadeOut: false, + duration: EndTransitionDuration); + GameMain.Client.EndCinematic = endTransition; + + Location portraitLocation = Map?.SelectedLocation ?? Map?.CurrentLocation ?? Level.Loaded?.StartLocation; + if (portraitLocation != null) + { + overlaySprite = portraitLocation.Type.GetPortrait(portraitLocation.PortraitId); + } + float fadeOutDuration = endTransition.Duration; + float t = 0.0f; + while (t < fadeOutDuration || endTransition.Running) + { + t += CoroutineManager.UnscaledDeltaTime; + overlayColor = Color.Lerp(Color.Transparent, Color.White, t / fadeOutDuration); + yield return CoroutineStatus.Running; + } + overlayColor = Color.White; + yield return CoroutineStatus.Running; + + //-------------------------------------- + + //wait for the new level to be loaded + DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, seconds: 30); + while (Level.Loaded == prevLevel || Level.Loaded == null) + { + if (DateTime.Now > timeOut || Screen.Selected != GameMain.GameScreen) { break; } + yield return CoroutineStatus.Running; + } + + endTransition.Stop(); + overlayColor = Color.Transparent; + + if (DateTime.Now > timeOut) { GameMain.NetLobbyScreen.Select(); } + if (!(Screen.Selected is RoundSummaryScreen)) + { + if (continueButton != null) + { + continueButton.Visible = true; + } + } + + yield return CoroutineStatus.Success; } public override void Update(float deltaTime) { + if (CoroutineManager.IsCoroutineRunning("LevelTransition") || Level.Loaded == null) { return; } + + if (ShowCampaignUI || ForceMapUI) + { + if (CampaignUI == null) { InitCampaignUI(); } + Character.DisableControls = true; + } + base.Update(deltaTime); - if (startWatchmanID > 0 && startWatchman == null) + if (PlayerInput.RightButtonClicked() || + PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Escape)) { - startWatchman = Entity.FindEntityByID(startWatchmanID) as Character; - if (startWatchman != null) { InitializeWatchman(startWatchman); } + ShowCampaignUI = false; + if (GUIMessageBox.VisibleBox?.UserData is RoundSummary roundSummary && + roundSummary.ContinueButton != null && + roundSummary.ContinueButton.Visible) + { + GUIMessageBox.MessageBoxes.Remove(GUIMessageBox.VisibleBox); + } } - if (endWatchmanID > 0 && endWatchman == null) + + if (!GUI.DisableHUD && !GUI.DisableUpperHUD) { - endWatchman = Entity.FindEntityByID(endWatchmanID) as Character; - if (endWatchman != null) { InitializeWatchman(endWatchman); } + endRoundButton.UpdateManually(deltaTime); + if (CoroutineManager.IsCoroutineRunning("LevelTransition") || ForceMapUI) { return; } + } + + if (Level.Loaded.Type == LevelData.LevelType.Outpost) + { + if (wasDocked) + { + var connectedSubs = Submarine.MainSub.GetConnectedSubs(); + bool isDocked = Level.Loaded.StartOutpost != null && connectedSubs.Contains(Level.Loaded.StartOutpost); + if (!isDocked) + { + //undocked from outpost, need to choose a destination + ForceMapUI = true; + if (CampaignUI == null) { InitCampaignUI(); } + CampaignUI.SelectTab(InteractionType.Map); + } + } + else + { + //wasn't initially docked (sub doesn't have a docking port?) + // -> choose a destination when the sub is far enough from the start outpost + if (!Submarine.MainSub.AtStartPosition) + { + ForceMapUI = true; + if (CampaignUI == null) { InitCampaignUI(); } + CampaignUI.SelectTab(InteractionType.Map); + } + } + + if (CampaignUI == null) { InitCampaignUI(); } } } - - protected override void WatchmanInteract(Character watchman, Character interactor) + public override void End(TransitionType transitionType = TransitionType.None) { - if ((watchman.Submarine == Level.Loaded.StartOutpost && !Submarine.MainSub.AtStartPosition) || - (watchman.Submarine == Level.Loaded.EndOutpost && !Submarine.MainSub.AtEndPosition)) + base.End(transitionType); + ForceMapUI = ShowCampaignUI = false; + UpgradeManager.CanUpgrade = true; + + // remove all event dialogue boxes + GUIMessageBox.MessageBoxes.ForEachMod(mb => { - return; + if (mb is GUIMessageBox msgBox) + { + if (mb.UserData is Pair pair && pair.First.Equals("conversationaction", StringComparison.OrdinalIgnoreCase)) + { + msgBox.Close(); + } + } + }); + + if (transitionType == TransitionType.End) + { + EndCampaign(); + } + else + { + IsFirstRound = false; + CoroutineManager.StartCoroutine(DoLevelTransition(), "LevelTransition"); + } + } + + protected override void EndCampaignProjSpecific() + { + if (GUIMessageBox.VisibleBox?.UserData is RoundSummary roundSummary) + { + GUIMessageBox.MessageBoxes.Remove(GUIMessageBox.VisibleBox); + } + CoroutineManager.StartCoroutine(DoEndCampaignCameraTransition(), "DoEndCampaignCameraTransition"); + GameMain.CampaignEndScreen.OnFinished = () => + { + GameMain.NetLobbyScreen.Select(); + if (GameMain.NetLobbyScreen.ContinueCampaignButton != null) { GameMain.NetLobbyScreen.ContinueCampaignButton.Enabled = false; } + if (GameMain.NetLobbyScreen.QuitCampaignButton != null) { GameMain.NetLobbyScreen.QuitCampaignButton.Enabled = false; } + }; + } + + private IEnumerable DoEndCampaignCameraTransition() + { + Character controlled = Character.Controlled; + if (controlled != null) + { + controlled.AIController.Enabled = false; } - if (GUIMessageBox.MessageBoxes.Any(mbox => mbox.UserData as string == "watchmanprompt")) - { - return; - } + GUI.DisableHUD = true; + ISpatialEntity endObject = Level.Loaded.LevelObjectManager.GetAllObjects().FirstOrDefault(obj => obj.Prefab.SpawnPos == LevelObjectPrefab.SpawnPosType.LevelEnd); + var transition = new CameraTransition(endObject ?? Submarine.MainSub, GameMain.GameScreen.Cam, + null, Alignment.Center, + fadeOut: true, + duration: 10, + startZoom: null, endZoom: 0.2f); - if (GameMain.Client != null && interactor == Character.Controlled) + while (transition.Running) { - var msgBox = new GUIMessageBox("", TextManager.GetWithVariable("CampaignEnterOutpostPrompt", "[locationname]", - Submarine.MainSub.AtStartPosition ? Map.CurrentLocation.Name : Map.SelectedLocation.Name), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }) - { - UserData = "watchmanprompt" - }; - msgBox.Buttons[0].OnClicked = (btn, userdata) => - { - GameMain.Client.RequestRoundEnd(); - return true; - }; - msgBox.Buttons[0].OnClicked += msgBox.Close; - msgBox.Buttons[1].OnClicked += msgBox.Close; + yield return CoroutineStatus.Running; } + GameMain.CampaignEndScreen.Select(); + GUI.DisableHUD = false; + + yield return CoroutineStatus.Success; } public void ClientWrite(IWriteMessage msg) { System.Diagnostics.Debug.Assert(map.Locations.Count < UInt16.MaxValue); + msg.Write(map.CurrentLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.CurrentLocationIndex); msg.Write(map.SelectedLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.SelectedLocationIndex); msg.Write(map.SelectedMissionIndex == -1 ? byte.MaxValue : (byte)map.SelectedMissionIndex); msg.Write(PurchasedHullRepairs); 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, 100); + } + msg.Write((UInt16)CargoManager.PurchasedItems.Count); foreach (PurchasedItem pi in CargoManager.PurchasedItems) { msg.Write(pi.ItemPrefab.Identifier); msg.WriteRangedInteger(pi.Quantity, 0, 100); } + + 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((ushort)UpgradeManager.PurchasedUpgrades.Count); + foreach (var (prefab, category, level) in UpgradeManager.PurchasedUpgrades) + { + msg.Write(prefab.Identifier); + msg.Write(category.Identifier); + msg.Write((byte)level); + } } //static because we may need to instantiate the campaign if it hasn't been done yet public static void ClientRead(IReadMessage msg) { - byte campaignID = msg.ReadByte(); - UInt16 updateID = msg.ReadUInt16(); - UInt16 saveID = msg.ReadUInt16(); - string mapSeed = msg.ReadString(); - UInt16 currentLocIndex = msg.ReadUInt16(); - UInt16 selectedLocIndex = msg.ReadUInt16(); - byte selectedMissionIndex = msg.ReadByte(); + bool isFirstRound = msg.ReadBoolean(); + byte campaignID = msg.ReadByte(); + UInt16 updateID = msg.ReadUInt16(); + UInt16 saveID = msg.ReadUInt16(); + string mapSeed = msg.ReadString(); + UInt16 currentLocIndex = msg.ReadUInt16(); + UInt16 selectedLocIndex = msg.ReadUInt16(); + byte selectedMissionIndex = msg.ReadByte(); + float? reputation = null; + if (msg.ReadBoolean()) { reputation = msg.ReadSingle(); } + + Dictionary factionReps = new Dictionary(); + byte factionsCount = msg.ReadByte(); + for (int i = 0; i < factionsCount; i++) + { + factionReps.Add(msg.ReadString(), msg.ReadSingle()); + } - UInt16 startWatchmanID = msg.ReadUInt16(); - UInt16 endWatchmanID = msg.ReadUInt16(); + bool forceMapUI = msg.ReadBoolean(); int money = msg.ReadInt32(); - bool purchasedHullRepairs = msg.ReadBoolean(); - bool purchasedItemRepairs = msg.ReadBoolean(); - bool purchasedLostShuttles = msg.ReadBoolean(); + bool purchasedHullRepairs = msg.ReadBoolean(); + bool purchasedItemRepairs = msg.ReadBoolean(); + bool purchasedLostShuttles = msg.ReadBoolean(); + + byte missionCount = msg.ReadByte(); + List> availableMissions = new List>(); + for (int i = 0; i < missionCount; i++) + { + string missionIdentifier = msg.ReadString(); + byte connectionIndex = msg.ReadByte(); + availableMissions.Add(new Pair(missionIdentifier, connectionIndex)); + } + + UInt16? storeBalance = null; + if (msg.ReadBoolean()) + { + storeBalance = msg.ReadUInt16(); + } + + 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)); + } UInt16 purchasedItemCount = msg.ReadUInt16(); List purchasedItems = new List(); @@ -166,65 +578,129 @@ namespace Barotrauma purchasedItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity)); } + 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(); + soldItems.Add(new SoldItem(ItemPrefab.Prefabs[itemPrefabIdentifier], id, removed, sellerId)); + } + + ushort pendingUpgradeCount = msg.ReadUInt16(); + List pendingUpgrades = new List(); + for (int i = 0; i < pendingUpgradeCount; i++) + { + string upgradeIdentifier = msg.ReadString(); + UpgradePrefab prefab = UpgradePrefab.Find(upgradeIdentifier); + string categoryIdentifier = msg.ReadString(); + UpgradeCategory category = UpgradeCategory.Find(categoryIdentifier); + int upgradeLevel = msg.ReadByte(); + if (prefab == null || category == null) { continue; } + pendingUpgrades.Add(new PurchasedUpgrade(prefab, category, upgradeLevel)); + } + bool hasCharacterData = msg.ReadBoolean(); CharacterInfo myCharacterInfo = null; if (hasCharacterData) { myCharacterInfo = CharacterInfo.ClientRead(CharacterPrefab.HumanSpeciesName, msg); } - - MultiPlayerCampaign campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign; - if (campaign == null || campaignID != campaign.CampaignID) + + if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaignID != campaign.CampaignID) { string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer); - GameMain.GameSession = new GameSession(null, savePath, - GameModePreset.List.Find(g => g.Identifier == "multiplayercampaign")); - - campaign = ((MultiPlayerCampaign)GameMain.GameSession.GameMode); + GameMain.GameSession = new GameSession(null, savePath, GameModePreset.MultiPlayerCampaign, mapSeed); + campaign = (MultiPlayerCampaign)GameMain.GameSession.GameMode; campaign.CampaignID = campaignID; - campaign.GenerateMap(mapSeed); GameMain.NetLobbyScreen.ToggleCampaignMode(true); } - //server has a newer save file if (NetIdUtils.IdMoreRecent(saveID, campaign.PendingSaveID)) { - /*//stop any active campaign save transfers, they're outdated now - List saveTransfers = - GameMain.Client.FileReceiver.ActiveTransfers.FindAll(t => t.FileType == FileTransferType.CampaignSave); - - foreach (var transfer in saveTransfers) - { - GameMain.Client.FileReceiver.StopTransfer(transfer); - } - - GameMain.Client.RequestFile(FileTransferType.CampaignSave, null, null);*/ campaign.PendingSaveID = saveID; } if (NetIdUtils.IdMoreRecent(updateID, campaign.lastUpdateID)) { campaign.SuppressStateSending = true; + campaign.IsFirstRound = isFirstRound; //we need to have the latest save file to display location/mission/store if (campaign.LastSaveID == saveID) { + campaign.ForceMapUI = forceMapUI; + + UpgradeStore.WaitForServerUpdate = false; + campaign.Map.SetLocation(currentLocIndex == UInt16.MaxValue ? -1 : currentLocIndex); campaign.Map.SelectLocation(selectedLocIndex == UInt16.MaxValue ? -1 : selectedLocIndex); campaign.Map.SelectMission(selectedMissionIndex); + campaign.CargoManager.SetItemsInBuyCrate(buyCrateItems); campaign.CargoManager.SetPurchasedItems(purchasedItems); + campaign.CargoManager.SetSoldItems(soldItems); + if (storeBalance.HasValue) { campaign.Map.CurrentLocation.StoreCurrentBalance = storeBalance.Value; } + campaign.UpgradeManager.SetPendingUpgrades(pendingUpgrades); + campaign.UpgradeManager.PurchasedUpgrades.Clear(); + + foreach (var (identifier, rep) in factionReps) + { + Faction faction = campaign.Factions.FirstOrDefault(f => f.Prefab.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase)); + if (faction?.Reputation != null) + { + faction.Reputation.Value = rep; + } + else + { + DebugConsole.ThrowError($"Received an update for a faction that doesn't exist \"{identifier}\"."); + } + } + + if (reputation.HasValue) + { + campaign.Map.CurrentLocation.Reputation.Value = reputation.Value; + campaign?.CampaignUI?.UpgradeStore?.RefreshAll(); + } + + foreach (var availableMission in availableMissions) + { + MissionPrefab missionPrefab = MissionPrefab.List.Find(mp => mp.Identifier == availableMission.First); + if (missionPrefab == null) + { + DebugConsole.ThrowError($"Error when receiving campaign data from the server: mission prefab \"{availableMission.First}\" not found."); + continue; + } + if (availableMission.Second < 0 || availableMission.Second >= 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})."); + continue; + } + LocationConnection connection = campaign.Map.CurrentLocation.Connections[availableMission.Second]; + campaign.Map.CurrentLocation.UnlockMission(missionPrefab, connection); + } + + GameMain.NetLobbyScreen.ToggleCampaignMode(true); } - campaign.startWatchmanID = startWatchmanID; - campaign.endWatchmanID = endWatchmanID; + bool shouldRefresh = campaign.Money != money || + campaign.PurchasedHullRepairs != purchasedHullRepairs || + campaign.PurchasedItemRepairs != purchasedItemRepairs || + campaign.PurchasedLostShuttles != purchasedLostShuttles; campaign.Money = money; campaign.PurchasedHullRepairs = purchasedHullRepairs; campaign.PurchasedItemRepairs = purchasedItemRepairs; campaign.PurchasedLostShuttles = purchasedLostShuttles; + if (shouldRefresh) + { + campaign?.CampaignUI?.UpgradeStore?.RefreshAll(); + } + if (myCharacterInfo != null) { GameMain.Client.CharacterInfo = myCharacterInfo; @@ -240,9 +716,68 @@ namespace Barotrauma } } + public void ClientReadCrew(IReadMessage msg) + { + ushort availableHireLength = msg.ReadUInt16(); + List availableHires = new List(); + for (int i = 0; i < availableHireLength; i++) + { + CharacterInfo hire = CharacterInfo.ClientRead("human", msg); + hire.Salary = msg.ReadInt32(); + availableHires.Add(hire); + } + + ushort pendingHireLength = msg.ReadUInt16(); + List pendingHires = new List(); + for (int i = 0; i < pendingHireLength; i++) + { + pendingHires.Add(msg.ReadInt32()); + } + + bool validateHires = msg.ReadBoolean(); + + bool fireCharacter = msg.ReadBoolean(); + + int firedIdentifier = -1; + if (fireCharacter) { firedIdentifier = msg.ReadInt32(); } + + if (fireCharacter) + { + CharacterInfo firedCharacter = CrewManager.CharacterInfos.FirstOrDefault(info => info.GetIdentifier() == firedIdentifier); + // this one might and is allowed to be null since the character is already fired on the original sender's game + if (firedCharacter != null) { CrewManager.FireCharacter(firedCharacter); } + } + + if (map?.CurrentLocation?.HireManager != null && CampaignUI?.CrewManagement != null) + { + CampaignUI?.CrewManagement?.SetHireables(map.CurrentLocation, availableHires); + if (validateHires) { CampaignUI?.CrewManagement.ValidatePendingHires(); } + CampaignUI?.CrewManagement?.SetPendingHires(pendingHires, map?.CurrentLocation); + if (fireCharacter) { CampaignUI?.CrewManagement.UpdateCrew(); } + } + } + public override void Save(XElement element) { //do nothing, the clients get the save files from the server } + + public void LoadState(string filePath) + { + DebugConsole.Log($"Loading save file for an existing game session ({filePath})"); + SaveUtil.DecompressToDirectory(filePath, SaveUtil.TempPath, null); + + string gamesessionDocPath = Path.Combine(SaveUtil.TempPath, "gamesession.xml"); + XDocument doc = XMLExtensions.TryLoadXml(gamesessionDocPath); + if (doc == null) + { + DebugConsole.ThrowError($"Failed to load the state of a multiplayer campaign. Could not open the file \"{gamesessionDocPath}\"."); + return; + } + Load(doc.Root.Element("MultiPlayerCampaign")); + SubmarineInfo selectedSub; + GameMain.GameSession.OwnedSubmarines = SaveUtil.LoadOwnedSubmarines(doc, out selectedSub); + GameMain.GameSession.SubmarineInfo = selectedSub; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index f61c1174a..7662c9ebf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -1,37 +1,36 @@ -using Barotrauma.Tutorials; +using Barotrauma.Extensions; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using System.Xml.Linq; namespace Barotrauma { class SinglePlayerCampaign : CampaignMode { - private GUIButton endRoundButton; - - private bool crewDead; private float endTimer; - + private bool savedOnStart; + + private bool gameOver; - private List subsToLeaveBehind; + private Character lastControlledCharacter; - private Submarine leavingSub; - private bool atEndPosition; + private bool showCampaignResetText; - public SinglePlayerCampaign(GameModePreset preset, object param) - : base(preset, param) + #region Constructors/initialization + + /// + /// Instantiates a new single player campaign + /// + private SinglePlayerCampaign(string mapSeed) : base(GameModePreset.SinglePlayerCampaign) { - int buttonHeight = (int)(HUDLayoutSettings.ButtonAreaTop.Height * 0.7f); - endRoundButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle(HUDLayoutSettings.ButtonAreaTop.Right - GUI.IntScale(200), HUDLayoutSettings.ButtonAreaTop.Center.Y - buttonHeight / 2, GUI.IntScale(200), buttonHeight), GUICanvas.Instance), - TextManager.Get("EndRound"), textAlignment: Alignment.Center) - { - Font = GUI.SmallFont, - OnClicked = (btn, userdata) => { TryEndRound(GetLeavingSub()); return true; } - }; - + CampaignMetadata = new CampaignMetadata(this); + UpgradeManager = new UpgradeManager(this); + map = new Map(this, mapSeed); foreach (JobPrefab jobPrefab in JobPrefab.Prefabs) { for (int i = 0; i < jobPrefab.InitialCount; i++) @@ -40,356 +39,16 @@ namespace Barotrauma CrewManager.AddCharacterInfo(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: jobPrefab, variant: variant)); } } + InitCampaignData(); + InitUI(); } - public override void Start() + /// + /// Loads a previously saved single player campaign from XML + /// + private SinglePlayerCampaign(XElement element) : base(GameModePreset.SinglePlayerCampaign) { - base.Start(); - CargoManager.CreateItems(); - - if (!savedOnStart) - { - SaveUtil.SaveGame(GameMain.GameSession.SavePath); - savedOnStart = true; - } - - crewDead = false; - endTimer = 5.0f; - isRunning = true; - CrewManager.InitSinglePlayerRound(); - } - - public bool TryHireCharacter(Location location, CharacterInfo characterInfo) - { - if (Money < characterInfo.Salary) { return false; } - - location.RemoveHireableCharacter(characterInfo); - CrewManager.AddCharacterInfo(characterInfo); - Money -= characterInfo.Salary; - - return true; - } - - public void FireCharacter(CharacterInfo characterInfo) - { - CrewManager.RemoveCharacterInfo(characterInfo); - } - - private Submarine GetLeavingSub() - { - if (Character.Controlled?.Submarine == null) - { - return null; - } - - //allow leaving if inside an outpost, and the submarine is either docked to it or close enough - return GetLeavingSubAtOutpost(Level.Loaded.StartOutpost) ?? GetLeavingSubAtOutpost(Level.Loaded.EndOutpost); - - Submarine GetLeavingSubAtOutpost(Submarine outpost) - { - //controlled character has to be inside the outpost - if (Character.Controlled.Submarine != outpost) { return null; } - - //if there's a sub docked to the outpost, we can leave the level - if (outpost.DockedTo.Any()) - { - var dockedSub = outpost.DockedTo.FirstOrDefault(); - return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub; - } - - //nothing docked, check if there's a sub close enough to the outpost - Submarine closestSub = Submarine.FindClosest(outpost.WorldPosition, ignoreOutposts: true); - if (closestSub == null) { return null; } - - if (outpost == Level.Loaded.StartOutpost) - { - if (!closestSub.AtStartPosition) { return null; } - } - else if (outpost == Level.Loaded.EndOutpost) - { - if (!closestSub.AtEndPosition) { return null; } - } - return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub; - } - } - - public override void Draw(SpriteBatch spriteBatch) - { - if (!isRunning|| GUI.DisableHUD || GUI.DisableUpperHUD) return; - - if (Submarine.MainSub == null) return; - - Submarine leavingSub = GetLeavingSub(); - if (leavingSub == null) - { - endRoundButton.Visible = false; - } - else if (leavingSub.AtEndPosition) - { - endRoundButton.Text = ToolBox.LimitString(TextManager.GetWithVariable("EnterLocation", "[locationname]", Map.SelectedLocation.Name), endRoundButton.Font, endRoundButton.Rect.Width - 5); - endRoundButton.Visible = true; - } - else if (leavingSub.AtStartPosition) - { - endRoundButton.Text = ToolBox.LimitString(TextManager.GetWithVariable("EnterLocation", "[locationname]", Map.CurrentLocation.Name), endRoundButton.Font, endRoundButton.Rect.Width - 5); - endRoundButton.Visible = true; - } - else - { - endRoundButton.Visible = false; - } - - endRoundButton.DrawManually(spriteBatch); - } - - public override void AddToGUIUpdateList() - { - if (!isRunning) return; - - base.AddToGUIUpdateList(); - CrewManager.AddToGUIUpdateList(); - endRoundButton.AddToGUIUpdateList(); - } - - public override void Update(float deltaTime) - { - if (!isRunning) { return; } - - base.Update(deltaTime); - - if (!GUI.DisableHUD && !GUI.DisableUpperHUD) - { - endRoundButton.UpdateManually(deltaTime); - } - - if (!crewDead) - { - if (!CrewManager.GetCharacters().Any(c => !c.IsDead)) crewDead = true; - } - else - { - endTimer -= deltaTime; - if (endTimer <= 0.0f) { EndRound(leavingSub: null); } - } - } - - - protected override void WatchmanInteract(Character watchman, Character interactor) - { - if (interactor != null) - { - interactor.FocusedCharacter = null; - } - - Submarine leavingSub = GetLeavingSub(); - if (leavingSub == null) - { - CreateDialog(new List { watchman }, "WatchmanInteractNoLeavingSub", 5.0f); - return; - } - - CreateDialog(new List { watchman }, "WatchmanInteract", 1.0f); - - if (GUIMessageBox.MessageBoxes.Any(mbox => mbox.UserData as string == "watchmanprompt")) - { - return; - } - var msgBox = new GUIMessageBox("", TextManager.GetWithVariable("CampaignEnterOutpostPrompt", "[locationname]", - leavingSub.AtStartPosition ? Map.CurrentLocation.Name : Map.SelectedLocation.Name), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }) - { - UserData = "watchmanprompt" - }; - msgBox.Buttons[0].OnClicked = (btn, userdata) => - { - if (!isRunning) { return true; } - TryEndRound(GetLeavingSub()); - return true; - }; - msgBox.Buttons[0].OnClicked += msgBox.Close; - msgBox.Buttons[1].OnClicked += msgBox.Close; - } - - public override void End(string endMessage = "") - { - isRunning = false; - - bool success = CrewManager.GetCharacters().Any(c => !c.IsDead); - crewDead = false; - - if (success) - { - if (subsToLeaveBehind == null || leavingSub == null) - { - DebugConsole.ThrowError("Leaving submarine not selected -> selecting the closest one"); - - leavingSub = GetLeavingSub(); - - subsToLeaveBehind = GetSubsToLeaveBehind(leavingSub); - } - } - - GameMain.GameSession.EndRound(""); - - if (success) - { - if (leavingSub != Submarine.MainSub && !leavingSub.DockedTo.Contains(Submarine.MainSub)) - { - Submarine.MainSub = leavingSub; - - GameMain.GameSession.Submarine = leavingSub; - - foreach (Submarine sub in subsToLeaveBehind) - { - MapEntity.mapEntityList.RemoveAll(e => e.Submarine == sub && e is LinkedSubmarine); - LinkedSubmarine.CreateDummy(leavingSub, sub); - } - } - - if (atEndPosition) - { - Map.MoveToNextLocation(); - } - else - { - Map.SelectLocation(-1); - } - Map.ProgressWorld(); - - //save and remove all items that are in someone's inventory - foreach (Character c in Character.CharacterList) - { - if (c.Info == null || c.Inventory == null) { continue; } - var inventoryElement = new XElement("inventory"); - - // Recharge headset batteries - var headset = c.Inventory.FindItemByIdentifier("headset"); - if (headset != null) - { - var battery = headset.OwnInventory.FindItemByTag("loadable"); - if (battery != null) - { - battery.Condition = battery.MaxCondition; - } - } - - c.SaveInventory(c.Inventory, inventoryElement); - c.Info.InventoryData = inventoryElement; - c.Inventory?.DeleteAllItems(); - c.ResetCurrentOrder(); - } - - GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); - - SaveUtil.SaveGame(GameMain.GameSession.SavePath); - } - - if (!success) - { - var summaryScreen = GUIMessageBox.VisibleBox; - - if (summaryScreen != null) - { - summaryScreen = summaryScreen.Children.First(); - var buttonArea = summaryScreen.Children.First().FindChild("buttonarea"); - buttonArea.ClearChildren(); - - - summaryScreen.RemoveChild(summaryScreen.Children.FirstOrDefault(c => c is GUIButton)); - - var okButton = new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), buttonArea.RectTransform), - TextManager.Get("LoadGameButton")) - { - OnClicked = (GUIButton button, object obj) => - { - GameMain.GameSession.LoadPrevious(); - GameMain.LobbyScreen.Select(); - GUIMessageBox.MessageBoxes.RemoveAll(c => c?.UserData as string == "roundsummary"); - return true; - } - }; - - var quitButton = new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), buttonArea.RectTransform), - TextManager.Get("QuitButton")); - quitButton.OnClicked += GameMain.LobbyScreen.QuitToMainMenu; - quitButton.OnClicked += (GUIButton button, object obj) => - { - GUIMessageBox.MessageBoxes.RemoveAll(c => c?.UserData as string == "roundsummary"); - return true; - }; - } - } - - CrewManager.EndRound(); - for (int i = Character.CharacterList.Count - 1; i >= 0; i--) - { - Character.CharacterList[i].Remove(); - } - - Submarine.Unload(); - - GameMain.LobbyScreen.Select(); - } - - private bool TryEndRound(Submarine leavingSub) - { - if (leavingSub == null) { return false; } - - this.leavingSub = leavingSub; - subsToLeaveBehind = GetSubsToLeaveBehind(leavingSub); - atEndPosition = leavingSub.AtEndPosition; - - if (subsToLeaveBehind.Any()) - { - string msg = TextManager.Get(subsToLeaveBehind.Count == 1 ? "LeaveSubBehind" : "LeaveSubsBehind"); - - var msgBox = new GUIMessageBox(TextManager.Get("Warning"), msg, new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); - msgBox.Buttons[0].OnClicked += (btn, userdata) => { EndRound(leavingSub); return true; } ; - msgBox.Buttons[0].OnClicked += msgBox.Close; - msgBox.Buttons[0].UserData = Submarine.Loaded.FindAll(s => !subsToLeaveBehind.Contains(s)); - - msgBox.Buttons[1].OnClicked += msgBox.Close; - } - else - { - EndRound(leavingSub); - } - - return true; - } - - private bool EndRound(Submarine leavingSub) - { - isRunning = false; - - //var cinematic = new RoundEndCinematic(leavingSub, GameMain.GameScreen.Cam, 5.0f); - - SoundPlayer.OverrideMusicType = CrewManager.GetCharacters().Any(c => !c.IsDead) ? "endround" : "crewdead"; - SoundPlayer.OverrideMusicDuration = 18.0f; - - //CoroutineManager.StartCoroutine(EndCinematic(cinematic), "EndCinematic"); - End(""); - - return true; - } - - /*private IEnumerable EndCinematic(RoundEndCinematic cinematic) - { - while (cinematic.Running) - { - if (Submarine.MainSub == null) yield return CoroutineStatus.Success; - - yield return CoroutineStatus.Running; - } - - if (Submarine.MainSub != null) End(""); - - yield return CoroutineStatus.Success; - }*/ - - public static SinglePlayerCampaign Load(XElement element) - { - SinglePlayerCampaign campaign = new SinglePlayerCampaign(GameModePreset.List.Find(gm => gm.Identifier == "singleplayercampaign"), null); + IsFirstRound = false; foreach (XElement subElement in element.Elements()) { @@ -399,15 +58,31 @@ namespace Barotrauma GameMain.GameSession.CrewManager = new CrewManager(subElement, true); break; case "map": - campaign.map = Map.LoadNew(subElement); + map = Map.Load(this, subElement); + break; + case "metadata": + CampaignMetadata = new CampaignMetadata(this, subElement); + break; + case "cargo": + CargoManager.LoadPurchasedItems(subElement); + break; + case "pendingupgrades": + UpgradeManager = new UpgradeManager(this, subElement, isSingleplayer: true); break; } } - campaign.Money = element.GetAttributeInt("money", 0); - campaign.CheatsEnabled = element.GetAttributeBool("cheatsenabled", false); - campaign.InitialSuppliesSpawned = element.GetAttributeBool("initialsuppliesspawned", false); - if (campaign.CheatsEnabled) + CampaignMetadata ??= new CampaignMetadata(this); + + UpgradeManager ??= new UpgradeManager(this); + + InitCampaignData(); + + InitUI(); + + Money = element.GetAttributeInt("money", 0); + CheatsEnabled = element.GetAttributeBool("cheatsenabled", false); + if (CheatsEnabled) { DebugConsole.CheatsEnabled = true; #if USE_STEAM @@ -419,28 +94,580 @@ namespace Barotrauma #endif } - //backwards compatibility with older save files - if (campaign.map == null) + if (map == null) { - string mapSeed = element.GetAttributeString("mapseed", "a"); - campaign.GenerateMap(mapSeed); - campaign.map.SetLocation(element.GetAttributeInt("currentlocation", 0)); + throw new System.Exception("Failed to load the campaign save file (saved with an older, incompatible version of Barotrauma)."); } - campaign.savedOnStart = true; + savedOnStart = true; + } + /// + /// Start a completely new single player campaign + /// + public static SinglePlayerCampaign StartNew(string mapSeed) + { + var campaign = new SinglePlayerCampaign(mapSeed); return campaign; } + /// + /// Load a previously saved single player campaign from xml + /// + /// + /// + public static SinglePlayerCampaign Load(XElement element) + { + return new SinglePlayerCampaign(element); + } + + private void InitUI() + { + int buttonHeight = (int)(GUI.Scale * 40); + int buttonWidth = GUI.IntScale(200); + + endRoundButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle((GameMain.GraphicsWidth / 2) - (buttonWidth / 2), HUDLayoutSettings.ButtonAreaTop.Center.Y - (buttonHeight / 2), buttonWidth, buttonHeight), GUICanvas.Instance), + TextManager.Get("EndRound"), textAlignment: Alignment.Center, style: "EndRoundButton") + { + Pulse = true, + TextBlock = + { + Shadow = true, + AutoScaleHorizontal = true + }, + OnClicked = (btn, userdata) => + { + var availableTransition = GetAvailableTransition(out _, out _); + if (Character.Controlled != null && + availableTransition == TransitionType.ReturnToPreviousLocation && + Character.Controlled?.Submarine == Level.Loaded?.StartOutpost) + { + TryEndRound(); + } + else if (Character.Controlled != null && + availableTransition == TransitionType.ProgressToNextLocation && + Character.Controlled?.Submarine == Level.Loaded?.EndOutpost) + { + TryEndRound(); + } + else + { + ShowCampaignUI = true; + CampaignUI.SelectTab(InteractionType.Map); + } + return true; + } + }; + + campaignUIContainer = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: "InnerGlow", color: Color.Black); + CampaignUI = new CampaignUI(this, campaignUIContainer) + { + StartRound = () => { TryEndRound(); } + }; + } + + #endregion + + public override void Start() + { + base.Start(); + CargoManager.CreatePurchasedItems(); + UpgradeManager.ApplyUpgrades(); + UpgradeManager.SanityCheckUpgrades(Submarine.MainSub); + + if (!savedOnStart) + { + SaveUtil.SaveGame(GameMain.GameSession.SavePath); + savedOnStart = true; + } + + crewDead = false; + endTimer = 5.0f; + CrewManager.InitSinglePlayerRound(); + } + + protected override void LoadInitialLevel() + { + //no level loaded yet -> show a loading screen and load the current location (outpost) + GameMain.Instance.ShowLoading( + DoLoadInitialLevel(map.SelectedConnection?.LevelData ?? map.CurrentLocation.LevelData, + mirror: map.CurrentLocation != map.SelectedConnection?.Locations[0])); + } + + private IEnumerable DoLoadInitialLevel(LevelData level, bool mirror) + { + GameMain.GameSession.StartRound(level, + mirrorLevel: mirror); + GameMain.GameScreen.Select(); + + CoroutineManager.StartCoroutine(DoInitialCameraTransition(), "SinglePlayerCampaign.DoInitialCameraTransition"); + + yield return CoroutineStatus.Success; + } + + private IEnumerable DoInitialCameraTransition() + { + while (GameMain.Instance.LoadingScreenOpen) + { + yield return CoroutineStatus.Running; + } + Character prevControlled = Character.Controlled; + if (prevControlled?.AIController != null) + { + prevControlled.AIController.Enabled = false; + } + Character.Controlled = null; + if (prevControlled != null) + { + prevControlled.ClearInputs(); + } + + GUI.DisableHUD = true; + while (GameMain.Instance.LoadingScreenOpen) + { + yield return CoroutineStatus.Running; + } + + if (IsFirstRound || showCampaignResetText) + { + overlayColor = Color.LightGray; + 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"); + float fadeInDuration = 2.0f; + float textDuration = 10.0f; + float timer = 0.0f; + while (true) + { + if (timer > fadeInDuration) + { + overlayTextBottom = pressAnyKeyText; + if (PlayerInput.GetKeyboardState.GetPressedKeys().Length > 0 || PlayerInput.PrimaryMouseButtonClicked()) + { + break; + } + } + overlayTextColor = Color.Lerp(Color.Transparent, Color.White, (timer - 1.0f) / fadeInDuration); + timer = Math.Min(timer + CoroutineManager.DeltaTime, textDuration); + yield return CoroutineStatus.Running; + } + var transition = new CameraTransition(prevControlled, GameMain.GameScreen.Cam, + null, null, + fadeOut: false, + duration: 5, + startZoom: 1.5f, endZoom: 1.0f) + { + AllowInterrupt = true, + RemoveControlFromCharacter = false + }; + fadeInDuration = 1.0f; + timer = 0.0f; + overlayTextColor = Color.Transparent; + overlayText = ""; + while (timer < fadeInDuration) + { + overlayColor = Color.Lerp(Color.LightGray, Color.Transparent, timer / fadeInDuration); + timer += CoroutineManager.DeltaTime; + yield return CoroutineStatus.Running; + } + overlayColor = Color.Transparent; + while (transition.Running) + { + yield return CoroutineStatus.Running; + } + showCampaignResetText = false; + } + else + { + ISpatialEntity transitionTarget; + if (prevControlled != null) + { + transitionTarget = prevControlled; + } + else + { + transitionTarget = Submarine.MainSub; + } + + var transition = new CameraTransition(transitionTarget, GameMain.GameScreen.Cam, + null, null, + fadeOut: false, + duration: 5, + startZoom: 0.5f, endZoom: 1.0f) + { + AllowInterrupt = true, + RemoveControlFromCharacter = false + }; + while (transition.Running) + { + yield return CoroutineStatus.Running; + } + } + + if (prevControlled != null) + { + prevControlled.SelectedConstruction = null; + if (prevControlled.AIController != null) + { + prevControlled.AIController.Enabled = true; + } + } + + if (prevControlled != null) + { + Character.Controlled = prevControlled; + } + GUI.DisableHUD = false; + yield return CoroutineStatus.Success; + } + + protected override IEnumerable DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror, List traitorResults = null) + { + NextLevel = newLevel; + bool success = CrewManager.GetCharacters().Any(c => !c.IsDead); + SoundPlayer.OverrideMusicType = success ? "endround" : "crewdead"; + SoundPlayer.OverrideMusicDuration = 18.0f; + crewDead = false; + + GameMain.GameSession.EndRound("", traitorResults, transitionType); + var continueButton = GameMain.GameSession.RoundSummary?.ContinueButton; + RoundSummary roundSummary = null; + if (GUIMessageBox.VisibleBox?.UserData is RoundSummary) + { + roundSummary = GUIMessageBox.VisibleBox?.UserData as RoundSummary; + } + if (continueButton != null) + { + continueButton.Visible = false; + } + + lastControlledCharacter = Character.Controlled; + Character.Controlled = null; + + switch (transitionType) + { + case TransitionType.None: + throw new InvalidOperationException("Level transition failed (no transitions available)."); + case TransitionType.ReturnToPreviousLocation: + //deselect destination on map + map.SelectLocation(-1); + break; + case TransitionType.ProgressToNextLocation: + Map.MoveToNextLocation(); + Map.ProgressWorld(); + break; + } + + var endTransition = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, null, + transitionType == TransitionType.LeaveLocation ? Alignment.BottomCenter : Alignment.Center, + fadeOut: false, + duration: EndTransitionDuration); + + GUI.ClearMessages(); + + Location portraitLocation = Map.SelectedLocation ?? Map.CurrentLocation; + overlaySprite = portraitLocation.Type.GetPortrait(portraitLocation.PortraitId); + float fadeOutDuration = endTransition.Duration; + float t = 0.0f; + while (t < fadeOutDuration || endTransition.Running) + { + t += CoroutineManager.UnscaledDeltaTime; + overlayColor = Color.Lerp(Color.Transparent, Color.White, t / fadeOutDuration); + yield return CoroutineStatus.Running; + } + overlayColor = Color.White; + yield return CoroutineStatus.Running; + + //-------------------------------------- + + if (success) + { + if (leavingSub != Submarine.MainSub && !leavingSub.DockedTo.Contains(Submarine.MainSub)) + { + Submarine.MainSub = leavingSub; + GameMain.GameSession.Submarine = leavingSub; + var subsToLeaveBehind = GetSubsToLeaveBehind(leavingSub); + foreach (Submarine sub in subsToLeaveBehind) + { + MapEntity.mapEntityList.RemoveAll(e => e.Submarine == sub && e is LinkedSubmarine); + LinkedSubmarine.CreateDummy(leavingSub, sub); + } + } + + GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); + SaveUtil.SaveGame(GameMain.GameSession.SavePath); + } + else + { + EnableRoundSummaryGameOverState(); + } + + //-------------------------------------- + + if (PendingSubmarineSwitch != null) + { + GameMain.GameSession.SubmarineInfo = PendingSubmarineSwitch; + PendingSubmarineSwitch = null; + } + + SelectSummaryScreen(roundSummary, newLevel, mirror, () => + { + GameMain.GameScreen.Select(); + if (continueButton != null) + { + continueButton.Visible = true; + } + + GUI.DisableHUD = false; + GUI.ClearCursorWait(); + overlayColor = Color.Transparent; + }); + + yield return CoroutineStatus.Success; + } + + protected override void EndCampaignProjSpecific() + { + CoroutineManager.StartCoroutine(DoEndCampaignCameraTransition(), "DoEndCampaignCameraTransition"); + GameMain.CampaignEndScreen.OnFinished = () => + { + showCampaignResetText = true; + LoadInitialLevel(); + IsFirstRound = true; + }; + } + + private IEnumerable DoEndCampaignCameraTransition() + { + if (Character.Controlled != null) + { + Character.Controlled.AIController.Enabled = false; + Character.Controlled = null; + } + GUI.DisableHUD = true; + ISpatialEntity endObject = Level.Loaded.LevelObjectManager.GetAllObjects().FirstOrDefault(obj => obj.Prefab.SpawnPos == LevelObjectPrefab.SpawnPosType.LevelEnd); + var transition = new CameraTransition(endObject ?? Submarine.MainSub, GameMain.GameScreen.Cam, + null, Alignment.Center, + fadeOut: true, + duration: 10, + startZoom: null, endZoom: 0.2f); + + while (transition.Running) + { + yield return CoroutineStatus.Running; + } + GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); + SaveUtil.SaveGame(GameMain.GameSession.SavePath); + GameMain.CampaignEndScreen.Select(); + GUI.DisableHUD = false; + + yield return CoroutineStatus.Success; + } + + public override void Update(float deltaTime) + { + if (CoroutineManager.IsCoroutineRunning("LevelTransition") || CoroutineManager.IsCoroutineRunning("SubmarineTransition") || gameOver) { return; } + + base.Update(deltaTime); + + if (PlayerInput.RightButtonClicked() || + PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Escape)) + { + ShowCampaignUI = false; + if (GUIMessageBox.VisibleBox?.UserData is RoundSummary roundSummary && + roundSummary.ContinueButton != null && + roundSummary.ContinueButton.Visible) + { + GUIMessageBox.MessageBoxes.Remove(GUIMessageBox.VisibleBox); + } + } + +#if DEBUG + if (PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.R)) + { + if (GUIMessageBox.MessageBoxes.Any()) { GUIMessageBox.MessageBoxes.Remove(GUIMessageBox.MessageBoxes.Last()); } + + GUIFrame summaryFrame = GameMain.GameSession.RoundSummary.CreateSummaryFrame(GameMain.GameSession, "", null); + GUIMessageBox.MessageBoxes.Add(summaryFrame); + GameMain.GameSession.RoundSummary.ContinueButton.OnClicked = (_, __) => { GUIMessageBox.MessageBoxes.Remove(summaryFrame); return true; }; + } +#endif + + if (ShowCampaignUI || ForceMapUI) + { + Character.DisableControls = true; + } + + if (!GUI.DisableHUD && !GUI.DisableUpperHUD) + { + endRoundButton.UpdateManually(deltaTime); + if (CoroutineManager.IsCoroutineRunning("LevelTransition") || ForceMapUI) { return; } + } + + if (Level.Loaded.Type == LevelData.LevelType.Outpost) + { + KeepCharactersCloseToOutpost(deltaTime); + if (wasDocked) + { + var connectedSubs = Submarine.MainSub.GetConnectedSubs(); + bool isDocked = Level.Loaded.StartOutpost != null && connectedSubs.Contains(Level.Loaded.StartOutpost); + if (!isDocked) + { + //undocked from outpost, need to choose a destination + ForceMapUI = true; + CampaignUI.SelectTab(InteractionType.Map); + } + } + else + { + //wasn't initially docked (sub doesn't have a docking port?) + // -> choose a destination when the sub is far enough from the start outpost + if (!Submarine.MainSub.AtStartPosition) + { + ForceMapUI = true; + CampaignUI.SelectTab(InteractionType.Map); + } + } + } + else + { + var transitionType = GetAvailableTransition(out _, out Submarine leavingSub); + if (transitionType == TransitionType.End) + { + EndCampaign(); + } + if (transitionType == TransitionType.ProgressToNextLocation && + Level.Loaded.EndOutpost != null && Level.Loaded.EndOutpost.DockedTo.Contains(leavingSub)) + { + LoadNewLevel(); + } + else if (transitionType == TransitionType.ReturnToPreviousLocation && + Level.Loaded.StartOutpost != null && Level.Loaded.StartOutpost.DockedTo.Contains(leavingSub)) + { + LoadNewLevel(); + } + else if (transitionType == TransitionType.None && CampaignUI.SelectedTab == InteractionType.Map) + { + ShowCampaignUI = false; + } + } + + if (!crewDead) + { + if (!CrewManager.GetCharacters().Any(c => !c.IsDead)) { crewDead = true; } + } + else + { + endTimer -= deltaTime; + if (endTimer <= 0.0f) { GameOver(); } + } + } + + private bool TryEndRound() + { + var transitionType = GetAvailableTransition(out LevelData nextLevel, out Submarine leavingSub); + if (leavingSub == null || transitionType == TransitionType.None) { return false; } + + if (nextLevel == null) + { + //no level selected -> force the player to select one + CampaignUI.SelectTab(InteractionType.Map); + map.SelectLocation(-1); + ForceMapUI = true; + return false; + } + else if (transitionType == TransitionType.ProgressToNextEmptyLocation) + { + Map.SetLocation(Map.Locations.IndexOf(Level.Loaded.EndLocation)); + } + + var subsToLeaveBehind = GetSubsToLeaveBehind(leavingSub); + if (subsToLeaveBehind.Any()) + { + string msg = TextManager.Get(subsToLeaveBehind.Count == 1 ? "LeaveSubBehind" : "LeaveSubsBehind"); + + var msgBox = new GUIMessageBox(TextManager.Get("Warning"), msg, new string[] { 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)); + msgBox.Buttons[1].OnClicked += msgBox.Close; + } + else + { + LoadNewLevel(); + } + + return true; + } + + private void GameOver() + { + gameOver = true; + GameMain.GameSession.EndRound("", transitionType: TransitionType.None); + EnableRoundSummaryGameOverState(); + } + + private void EnableRoundSummaryGameOverState() + { + var roundSummary = GameMain.GameSession.RoundSummary; + if (roundSummary != null) + { + roundSummary.ContinueButton.Visible = false; + roundSummary.ContinueButton.IgnoreLayoutGroups = true; + + new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), roundSummary.ButtonArea.RectTransform), + TextManager.Get("QuitButton")) + { + OnClicked = (GUIButton button, object obj) => + { + GameMain.MainMenuScreen.Select(); + GUIMessageBox.MessageBoxes.Remove(roundSummary.Frame); + return true; + } + }; + new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), roundSummary.ButtonArea.RectTransform), + TextManager.Get("LoadGameButton")) + { + OnClicked = (GUIButton button, object obj) => + { + GameMain.GameSession.LoadPreviousSave(); + GUIMessageBox.MessageBoxes.Remove(roundSummary.Frame); + return true; + } + }; + } + } + public override void Save(XElement element) { XElement modeElement = new XElement("SinglePlayerCampaign", - // Refunds the money when save & quitting from the map if there are items selected in the store - new XAttribute("money", Money + (CargoManager != null ? CargoManager.GetTotalItemCost() : 0)), - new XAttribute("cheatsenabled", CheatsEnabled), - new XAttribute("initialsuppliesspawned", InitialSuppliesSpawned)); + new XAttribute("money", Money), + new XAttribute("cheatsenabled", CheatsEnabled)); + + //save and remove all items that are in someone's inventory so they don't get included in the sub file as well + foreach (Character c in Character.CharacterList) + { + if (c.Info == null) { continue; } + if (c.IsDead) { CrewManager.RemoveCharacterInfo(c.Info); } + c.Info.LastControlled = c == lastControlledCharacter; + c.Info.HealthData = new XElement("health"); + c.CharacterHealth.Save(c.Info.HealthData); + if (c.Inventory != null) + { + c.Info.InventoryData = new XElement("inventory"); + c.SaveInventory(c.Inventory, c.Info.InventoryData); + c.Inventory?.DeleteAllItems(); + } + } + CrewManager.Save(modeElement); + CampaignMetadata.Save(modeElement); Map.Save(modeElement); + CargoManager?.SavePurchasedItems(modeElement); + UpgradeManager?.SavePendingUpgrades(modeElement, UpgradeManager?.PendingUpgrades); element.Add(modeElement); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SubTestMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SubTestMode.cs deleted file mode 100644 index cfddadd7e..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SubTestMode.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Barotrauma.Tutorials; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; - -namespace Barotrauma -{ - class SubTestMode : GameMode - { - public SubTestMode(GameModePreset preset, object param) - : base(preset, param) - { - foreach (JobPrefab jobPrefab in JobPrefab.Prefabs) - { - 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)); - } - } - } - - public override void Start() - { - base.Start(); - - isRunning = true; - CrewManager.InitSinglePlayerRound(); - - Submarine.MainSub.SetPosition(Vector2.Zero); - } - - public override void Draw(SpriteBatch spriteBatch) - { - if (!isRunning|| GUI.DisableHUD || GUI.DisableUpperHUD) return; - - if (Submarine.MainSub == null) return; - } - - public override void AddToGUIUpdateList() - { - if (!isRunning) return; - - base.AddToGUIUpdateList(); - CrewManager.AddToGUIUpdateList(); - } - - public override void Update(float deltaTime) - { - if (!isRunning) { return; } - - base.Update(deltaTime); - } - - public override void End(string endMessage = "") - { - isRunning = false; - - GameMain.GameSession.EndRound(""); - - CrewManager.EndRound(); - - Submarine.Unload(); - - GameMain.SubEditorScreen.Select(); - } - - private bool EndRound(Submarine leavingSub) - { - isRunning = false; - - End(""); - - return true; - } - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs new file mode 100644 index 000000000..27aeb3551 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs @@ -0,0 +1,34 @@ +using Microsoft.Xna.Framework; +using System; + +namespace Barotrauma +{ + class TestGameMode : GameMode + { + public Action OnRoundEnd; + + public TestGameMode(GameModePreset preset) : base(preset) + { + foreach (JobPrefab jobPrefab in JobPrefab.Prefabs) + { + 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)); + } + } + } + + public override void Start() + { + base.Start(); + + CrewManager.InitSinglePlayerRound(); + } + + public override void End(CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None) + { + OnRoundEnd?.Invoke(); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/BasicTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/BasicTutorial.cs index a7e03887e..4451526c6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/BasicTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/BasicTutorial.cs @@ -614,7 +614,7 @@ namespace Barotrauma.Tutorials GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; GameMain.LightManager.LosEnabled = false; - var cinematic = new RoundEndCinematic(Submarine.MainSub, GameMain.GameScreen.Cam, 5.0f); + var cinematic = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, Alignment.CenterLeft, Alignment.CenterRight, duration: 5.0f); while (cinematic.Running) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs index 8825ec280..81cdd715a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs @@ -101,6 +101,7 @@ namespace Barotrauma.Tutorials tutorial_submarineDoorLight = Item.ItemList.Find(i => i.HasTag("tutorial_submarinedoorlight")).GetComponent(); var medicInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, "", JobPrefab.Get("medicaldoctor")); captain_medic = Character.Create(medicInfo, captain_medicSpawnPos, "medicaldoctor"); + captain_medic.TeamID = Character.TeamType.Team1; captain_medic.GiveJobItems(null); captain_medic.CanSpeak = captain_medic.AIController.Enabled = false; SetDoorAccess(tutorial_submarineDoor, tutorial_submarineDoorLight, false); @@ -123,14 +124,17 @@ namespace Barotrauma.Tutorials var mechanicInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, "", JobPrefab.Get("mechanic")); captain_mechanic = Character.Create(mechanicInfo, WayPoint.GetRandom(SpawnType.Human, mechanicInfo.Job, Submarine.MainSub).WorldPosition, "mechanic"); + captain_mechanic.TeamID = Character.TeamType.Team1; captain_mechanic.GiveJobItems(); var securityInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, "", JobPrefab.Get("securityofficer")); captain_security = Character.Create(securityInfo, WayPoint.GetRandom(SpawnType.Human, securityInfo.Job, Submarine.MainSub).WorldPosition, "securityofficer"); + captain_security.TeamID = Character.TeamType.Team1; captain_security.GiveJobItems(); var engineerInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, "", JobPrefab.Get("engineer")); captain_engineer = Character.Create(engineerInfo, WayPoint.GetRandom(SpawnType.Human, engineerInfo.Job, Submarine.MainSub).WorldPosition, "engineer"); + captain_engineer.TeamID = Character.TeamType.Team1; captain_engineer.GiveJobItems(); captain_mechanic.CanSpeak = captain_security.CanSpeak = captain_engineer.CanSpeak = false; @@ -195,7 +199,7 @@ namespace Barotrauma.Tutorials // GameMain.GameSession.CrewManager.HighlightOrderButton(captain_security, "operateweapons", highlightColor, new Vector2(5, 5)); HighlightOrderOption("fireatwill"); } - while (!HasOrder(captain_security, "operateweapons", "fireatwill")); + while (!HasOrder(captain_security, "operateweapons")); RemoveCompletedObjective(segments[2]); yield return new WaitForSeconds(4f, false); TriggerTutorialSegment(3, GameMain.Config.KeyBindText(InputType.Command)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs index d4717ab96..8932c49f5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs @@ -80,6 +80,7 @@ namespace Barotrauma.Tutorials var assistantInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, "", JobPrefab.Get("assistant")); patient1 = Character.Create(assistantInfo, patientHull1.WorldPosition, "1"); + patient1.TeamID = Character.TeamType.Team1; patient1.GiveJobItems(null); patient1.CanSpeak = false; patient1.AddDamage(patient1.WorldPosition, new List() { new Affliction(AfflictionPrefab.Burn, 45.0f) }, stun: 0, playSound: false); @@ -87,22 +88,26 @@ namespace Barotrauma.Tutorials assistantInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, "", JobPrefab.Get("assistant")); patient2 = Character.Create(assistantInfo, patientHull2.WorldPosition, "2"); + patient2.TeamID = Character.TeamType.Team1; patient2.GiveJobItems(null); patient2.CanSpeak = false; patient2.AIController.Enabled = false; var mechanicInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, "", JobPrefab.Get("engineer")); var subPatient1 = Character.Create(mechanicInfo, WayPoint.GetRandom(SpawnType.Human, mechanicInfo.Job, Submarine.MainSub).WorldPosition, "3"); + subPatient1.TeamID = Character.TeamType.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.Get("securityofficer")); var subPatient2 = Character.Create(securityInfo, WayPoint.GetRandom(SpawnType.Human, securityInfo.Job, Submarine.MainSub).WorldPosition, "3"); + subPatient2.TeamID = Character.TeamType.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.Get("engineer")); var subPatient3 = Character.Create(securityInfo, WayPoint.GetRandom(SpawnType.Human, engineerInfo.Job, Submarine.MainSub).WorldPosition, "3"); + subPatient3.TeamID = Character.TeamType.Team1; subPatient3.AddDamage(patient1.WorldPosition, new List() { new Affliction(AfflictionPrefab.Burn, 20.0f) }, stun: 0, playSound: false); subPatients.Add(subPatient3); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs index fe78d0166..94aea1fb7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs @@ -376,7 +376,7 @@ namespace Barotrauma.Tutorials } } yield return null; - } while (!engineer_brokenJunctionBox.IsFullCondition); // Wait until repaired + } while (engineer_brokenJunctionBox.Condition < repairableJunctionBoxComponent.RepairThreshold); // Wait until repaired SetHighlight(engineer_brokenJunctionBox, false); RemoveCompletedObjective(segments[2]); SetDoorAccess(engineer_thirdDoor, engineer_thirdDoorLight, true); @@ -408,15 +408,20 @@ namespace Barotrauma.Tutorials yield return new WaitForSeconds(2f, false); TriggerTutorialSegment(4); // Repair junction box while (ContentRunning) yield return null; - SetHighlight(engineer_submarineJunctionBox_1, true); - SetHighlight(engineer_submarineJunctionBox_2, true); - SetHighlight(engineer_submarineJunctionBox_3, true); engineer.AddActiveObjectiveEntity(engineer_submarineJunctionBox_1, engineer_repairIcon, engineer_repairIconColor); engineer.AddActiveObjectiveEntity(engineer_submarineJunctionBox_2, engineer_repairIcon, engineer_repairIconColor); engineer.AddActiveObjectiveEntity(engineer_submarineJunctionBox_3, engineer_repairIcon, engineer_repairIconColor); + SetHighlight(engineer_submarineJunctionBox_1, true); + SetHighlight(engineer_submarineJunctionBox_2, true); + SetHighlight(engineer_submarineJunctionBox_3, true); + + Repairable repairableJunctionBoxComponent1 = engineer_submarineJunctionBox_1.GetComponent(); + Repairable repairableJunctionBoxComponent2 = engineer_submarineJunctionBox_2.GetComponent(); + Repairable repairableJunctionBoxComponent3 = engineer_submarineJunctionBox_3.GetComponent(); + // Remove highlights when each individual machine is repaired - do { CheckJunctionBoxHighlights(); yield return null; } while (!engineer_submarineJunctionBox_1.IsFullCondition || !engineer_submarineJunctionBox_2.IsFullCondition || !engineer_submarineJunctionBox_3.IsFullCondition); - CheckJunctionBoxHighlights(); + do { CheckJunctionBoxHighlights(repairableJunctionBoxComponent1, repairableJunctionBoxComponent2, repairableJunctionBoxComponent3); yield return null; } while (engineer_submarineJunctionBox_1.Condition < repairableJunctionBoxComponent1.RepairThreshold || engineer_submarineJunctionBox_2.Condition < repairableJunctionBoxComponent2.RepairThreshold || engineer_submarineJunctionBox_3.Condition < repairableJunctionBoxComponent3.RepairThreshold); + CheckJunctionBoxHighlights(repairableJunctionBoxComponent1, repairableJunctionBoxComponent2, repairableJunctionBoxComponent3); RemoveCompletedObjective(segments[4]); yield return new WaitForSeconds(2f, false); @@ -557,19 +562,19 @@ namespace Barotrauma.Tutorials } } - private void CheckJunctionBoxHighlights() + private void CheckJunctionBoxHighlights(Repairable comp1, Repairable comp2, Repairable comp3) { - if (engineer_submarineJunctionBox_1.IsFullCondition && engineer_submarineJunctionBox_1.ExternalHighlight) + if (engineer_submarineJunctionBox_1.Condition > comp1.RepairThreshold && engineer_submarineJunctionBox_1.ExternalHighlight) { SetHighlight(engineer_submarineJunctionBox_1, false); engineer.RemoveActiveObjectiveEntity(engineer_submarineJunctionBox_1); } - if (engineer_submarineJunctionBox_2.IsFullCondition && engineer_submarineJunctionBox_2.ExternalHighlight) + if (engineer_submarineJunctionBox_2.Condition > comp2.RepairThreshold && engineer_submarineJunctionBox_2.ExternalHighlight) { SetHighlight(engineer_submarineJunctionBox_2, false); engineer.RemoveActiveObjectiveEntity(engineer_submarineJunctionBox_2); } - if (engineer_submarineJunctionBox_3.IsFullCondition && engineer_submarineJunctionBox_3.ExternalHighlight) + if (engineer_submarineJunctionBox_3.Condition > comp3.RepairThreshold && engineer_submarineJunctionBox_3.ExternalHighlight) { SetHighlight(engineer_submarineJunctionBox_3, false); engineer.RemoveActiveObjectiveEntity(engineer_submarineJunctionBox_3); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs index 4b898fd5c..2720382d2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs @@ -385,7 +385,8 @@ namespace Barotrauma.Tutorials } } - if (!gotOxygenTank && mechanic.Inventory.FindItemByIdentifier("oxygentank") != null) + if (!gotOxygenTank && (mechanic.Inventory.FindItemByIdentifier("oxygentank") != null || + mechanic_deconstructor.InputContainer.Inventory.FindItemByIdentifier("oxygentank") != null)) { gotOxygenTank = true; } @@ -551,7 +552,7 @@ namespace Barotrauma.Tutorials do { yield return null; - if (!mechanic_brokenPump.Item.IsFullCondition) + if (mechanic_brokenPump.Item.Condition < repairablePumpComponent.RepairThreshold) { if (!mechanic.HasEquippedItem("wrench")) { @@ -575,7 +576,7 @@ namespace Barotrauma.Tutorials } } } - } while (!mechanic_brokenPump.Item.IsFullCondition || mechanic_brokenPump.FlowPercentage >= 0 || !mechanic_brokenPump.IsActive); + } while (mechanic_brokenPump.Item.Condition < repairablePumpComponent.RepairThreshold || mechanic_brokenPump.FlowPercentage >= 0 || !mechanic_brokenPump.IsActive); RemoveCompletedObjective(segments[9]); SetHighlight(mechanic_brokenPump.Item, false); do { yield return null; } while (mechanic_brokenhull_2.WaterPercentage > waterVolumeBeforeOpening); @@ -592,9 +593,14 @@ namespace Barotrauma.Tutorials SetHighlight(mechanic_ballastPump_1.Item, true); SetHighlight(mechanic_ballastPump_2.Item, true); SetHighlight(mechanic_submarineEngine.Item, true); + + Repairable repairablePumpComponent1 = mechanic_ballastPump_1.Item.GetComponent(); + Repairable repairablePumpComponent2 = mechanic_ballastPump_2.Item.GetComponent(); + Repairable repairableEngineComponent = mechanic_submarineEngine.Item.GetComponent(); + // Remove highlights when each individual machine is repaired - do { CheckHighlights(); yield return null; } while (!mechanic_ballastPump_1.Item.IsFullCondition || !mechanic_ballastPump_2.Item.IsFullCondition || !mechanic_submarineEngine.Item.IsFullCondition); - CheckHighlights(); + do { CheckHighlights(repairablePumpComponent1, repairablePumpComponent2, repairableEngineComponent); yield return null; } while (mechanic_ballastPump_1.Item.Condition < repairablePumpComponent1.RepairThreshold || mechanic_ballastPump_2.Item.Condition < repairablePumpComponent2.RepairThreshold || mechanic_submarineEngine.Item.Condition < repairableEngineComponent.RepairThreshold); + CheckHighlights(repairablePumpComponent1, repairablePumpComponent2, repairableEngineComponent); RemoveCompletedObjective(segments[10]); GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Mechanic.Radio.Complete"), ChatMessageType.Radio, null); @@ -617,19 +623,19 @@ namespace Barotrauma.Tutorials return false; } - private void CheckHighlights() + private void CheckHighlights(Repairable comp1, Repairable comp2, Repairable comp3) { - if (mechanic_ballastPump_1.Item.IsFullCondition && mechanic_ballastPump_1.Item.ExternalHighlight) + if (mechanic_ballastPump_1.Item.Condition > comp1.RepairThreshold && mechanic_ballastPump_1.Item.ExternalHighlight) { SetHighlight(mechanic_ballastPump_1.Item, false); mechanic.RemoveActiveObjectiveEntity(mechanic_ballastPump_1.Item); } - if (mechanic_ballastPump_2.Item.IsFullCondition && mechanic_ballastPump_2.Item.ExternalHighlight) + if (mechanic_ballastPump_2.Item.Condition > comp2.RepairThreshold && mechanic_ballastPump_2.Item.ExternalHighlight) { SetHighlight(mechanic_ballastPump_2.Item, false); mechanic.RemoveActiveObjectiveEntity(mechanic_ballastPump_2.Item); } - if (mechanic_submarineEngine.Item.IsFullCondition && mechanic_submarineEngine.Item.ExternalHighlight) + if (mechanic_submarineEngine.Item.Condition > comp3.RepairThreshold && mechanic_submarineEngine.Item.ExternalHighlight) { SetHighlight(mechanic_submarineEngine.Item, false); mechanic.RemoveActiveObjectiveEntity(mechanic_submarineEngine.Item); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs index 0585ae807..ebfda0359 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs @@ -315,13 +315,14 @@ namespace Barotrauma.Tutorials 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 officer_hammerhead = SpawnMonster("hammerhead", officer_hammerheadSpawnPos); + ((EnemyAIController)officer_hammerhead.AIController).StayInsideLevel = false; officer_hammerhead.AIController.SelectTarget(officer.AiTarget); SetHighlight(officer_coilgunPeriscope, true); float originalDistance = Vector2.Distance(officer_coilgunPeriscope.WorldPosition, officer_hammerheadSpawnPos); do { float distance = Vector2.Distance(officer_coilgunPeriscope.WorldPosition, officer_hammerhead.WorldPosition); - if (distance > originalDistance * 1.5f) + if (distance > originalDistance * 1.5f || officer_hammerhead.WorldPosition.Y > officer_coilgunPeriscope.WorldPosition.Y) { // Don't let the Hammerhead go too far. officer_hammerhead.TeleportTo(officer_hammerheadSpawnPos + new Vector2(0, -1000)); @@ -329,7 +330,13 @@ namespace Barotrauma.Tutorials if (distance > originalDistance) { // Ensure that the Hammerhead targets the player + officer.AiTarget.SoundRange = float.MaxValue; + officer.AiTarget.SightRange = float.MaxValue; officer_hammerhead.AIController.SelectTarget(officer.AiTarget); + if ((officer_hammerhead.AIController as EnemyAIController)?.SelectedTargetingParams != null) + { + ((EnemyAIController)officer_hammerhead.AIController).SelectedTargetingParams.ReactDistance = 5000.0f; + } /*var ai = officer_hammerhead.AIController as EnemyAIController; ai.sight = 2.0f;*/ } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs index e82aef15e..913fa9137 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs @@ -58,30 +58,31 @@ namespace Barotrauma.Tutorials { SubmarineInfo subInfo = new SubmarineInfo(submarinePath); - LevelGenerationParams generationParams = LevelGenerationParams.LevelParams.Find(p => p.Name == levelParams); + LevelGenerationParams generationParams = LevelGenerationParams.LevelParams.Find(p => p.Identifier.Equals(levelParams, StringComparison.OrdinalIgnoreCase)); yield return CoroutineStatus.Running; - GameMain.GameSession = new GameSession(subInfo, "", - GameModePreset.List.Find(g => g.Identifier == "tutorial")); + GameMain.GameSession = new GameSession(subInfo, GameModePreset.Tutorial, missionPrefab: null); (GameMain.GameSession.GameMode as TutorialMode).Tutorial = this; if (generationParams != null) { - Biome biome = LevelGenerationParams.GetBiomes().Find(b => generationParams.AllowedBiomes.Contains(b)); + Biome biome = + LevelGenerationParams.GetBiomes().FirstOrDefault(b => generationParams.AllowedBiomes.Contains(b)) ?? + LevelGenerationParams.GetBiomes().First(); - if (startOutpostPath != string.Empty) + if (!string.IsNullOrEmpty(startOutpostPath)) { startOutpost = new SubmarineInfo(startOutpostPath); } - if (endOutpostPath != string.Empty) + if (!string.IsNullOrEmpty(endOutpostPath)) { endOutpost = new SubmarineInfo(endOutpostPath); } - Level tutorialLevel = new Level(levelSeed, 0, 0, generationParams, biome, startOutpost, endOutpost); - GameMain.GameSession.StartRound(tutorialLevel); + LevelData tutorialLevel = new LevelData(levelSeed, 0, 0, generationParams, biome); + GameMain.GameSession.StartRound(tutorialLevel, startOutpost: startOutpost, endOutpost: endOutpost); } else { @@ -100,6 +101,13 @@ namespace Barotrauma.Tutorials base.Start(); Submarine.MainSub.GodMode = true; + foreach (Structure wall in Structure.WallList) + { + if (wall.Submarine != null && wall.Submarine.Info.IsOutpost) + { + wall.Indestructible = true; + } + } CharacterInfo charInfo = configElement.Element("Character") == null ? new CharacterInfo(CharacterPrefab.HumanSpeciesName, "", JobPrefab.Get("engineer")) : @@ -114,6 +122,7 @@ namespace Barotrauma.Tutorials } character = Character.Create(charInfo, wayPoint.WorldPosition, "", false, false); + character.TeamID = Character.TeamType.Team1; Character.Controlled = character; character.GiveJobItems(null); @@ -126,19 +135,14 @@ namespace Barotrauma.Tutorials idCard.AddTag("com"); idCard.AddTag("eng"); - List entities = Entity.GetEntityList(); - - for (int i = 0; i < entities.Count; i++) + foreach (Item item in Item.ItemList) { - if (entities[i] is Item) + Door door = item.GetComponent(); + if (door != null) { - Door door = (entities[i] as Item).GetComponent(); - if (door != null) - { - door.CanBeWelded = false; - } + door.CanBeWelded = false; } - } + } tutorialCoroutine = CoroutineManager.StartCoroutine(UpdateState()); } @@ -284,7 +288,7 @@ namespace Barotrauma.Tutorials yield return new WaitForSeconds(waitBeforeFade); - var endCinematic = new RoundEndCinematic(Submarine.MainSub, GameMain.GameScreen.Cam, fadeOutTime); + var endCinematic = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, null, Alignment.Center, duration: fadeOutTime); currentTutorialCompleted = Completed = true; while (endCinematic.Running) yield return null; Stop(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs index 8127788ae..a598561a3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs @@ -26,6 +26,7 @@ namespace Barotrauma.Tutorials 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; @@ -207,7 +208,7 @@ namespace Barotrauma.Tutorials public virtual void AddToGUIUpdateList() { - if (GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y || prevUIScale != GUI.Scale) + if (GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y || prevUIScale != GUI.Scale || GameMain.Config.WindowMode != windowMode) { CreateObjectiveFrame(); } @@ -340,6 +341,7 @@ namespace Barotrauma.Tutorials } screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); + windowMode = GameMain.Config.WindowMode; prevUIScale = GUI.Scale; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/TutorialMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/TutorialMode.cs index 407f742e9..ac23b8a31 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/TutorialMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/TutorialMode.cs @@ -11,8 +11,8 @@ namespace Barotrauma tutorial.Initialize(); } - public TutorialMode(GameModePreset preset, object param) - : base(preset, param) + public TutorialMode(GameModePreset preset) + : base(preset) { } @@ -21,6 +21,11 @@ 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 + item.SpawnedInOutpost = false; + } } public override void AddToGUIUpdateList() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs index 38e4dff6d..54537e63a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs @@ -1,14 +1,19 @@ using Microsoft.Xna.Framework.Graphics; -using Microsoft.Xna.Framework.Input; namespace Barotrauma { partial class GameSession { - public RoundSummary RoundSummary { get; private set; } + public RoundSummary RoundSummary + { + get; + private set; + } + public static bool IsTabMenuOpen => GameMain.GameSession?.tabMenu != null; public static TabMenu TabMenuInstance => GameMain.GameSession?.tabMenu; + private TabMenu tabMenu; public bool ToggleTabMenu() @@ -46,30 +51,20 @@ namespace Barotrauma partial void UpdateProjSpecific(float deltaTime) { - if (GUI.DisableHUD) return; + if (GUI.DisableHUD) { return; } - if (GameMode.IsRunning) + if (tabMenu == null) { - if (tabMenu == null) + if (PlayerInput.KeyHit(InputType.InfoTab) && GUI.KeyboardDispatcher.Subscriber is GUITextBox == false) { - if (PlayerInput.KeyHit(InputType.InfoTab) && GUI.KeyboardDispatcher.Subscriber is GUITextBox == false) - { - ToggleTabMenu(); - } - } - else - { - tabMenu.Update(); - - if (PlayerInput.KeyHit(InputType.InfoTab) && GUI.KeyboardDispatcher.Subscriber is GUITextBox == false) - { - ToggleTabMenu(); - } + ToggleTabMenu(); } } else { - if (tabMenu != null) + tabMenu.Update(); + + if (PlayerInput.KeyHit(InputType.InfoTab) && GUI.KeyboardDispatcher.Subscriber is GUITextBox == false) { ToggleTabMenu(); } @@ -97,7 +92,6 @@ namespace Barotrauma public void Draw(SpriteBatch spriteBatch) { - if (GUI.DisableHUD) return; GameMode?.Draw(spriteBatch); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index 12cd4a55e..ad7843fc9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -1,185 +1,654 @@ using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Globalization; using System.Linq; namespace Barotrauma { class RoundSummary { - private Location startLocation, endLocation; + private const float jobColumnWidthPercentage = 0.11f; + private const float characterColumnWidthPercentage = 0.44f; + private const float statusColumnWidthPercentage = 0.45f; - private GameSession gameSession; + private int jobColumnWidth, characterColumnWidth, statusColumnWidth; - private Mission selectedMission; - - public RoundSummary(GameSession gameSession) + private readonly SubmarineInfo sub; + private readonly Mission selectedMission; + private readonly Location startLocation, endLocation; + + private readonly GameMode gameMode; + + private readonly float initialLocationReputation; + private readonly Dictionary initialFactionReputations = new Dictionary(); + + public GUILayoutGroup ButtonArea { get; private set; } + + public GUIButton ContinueButton { get; private set; } + + public GUIComponent Frame { get; private set; } + + + + public RoundSummary(SubmarineInfo sub, GameMode gameMode, Mission selectedMission, Location startLocation, Location endLocation) { - this.gameSession = gameSession; - - startLocation = gameSession.StartLocation; - endLocation = gameSession.EndLocation; - - selectedMission = gameSession.Mission; + this.sub = sub; + this.gameMode = gameMode; + this.selectedMission = selectedMission; + this.startLocation = startLocation; + this.endLocation = endLocation; + initialLocationReputation = startLocation?.Reputation?.Value ?? 0.0f; + if (gameMode is CampaignMode campaignMode) + { + foreach (Faction faction in campaignMode.Factions) + { + initialFactionReputations.Add(faction, faction.Reputation.Value); + } + } } - public GUIFrame CreateSummaryFrame(string endMessage) + public GUIFrame CreateSummaryFrame(GameSession gameSession, string endMessage, List traitorResults, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None) { bool singleplayer = GameMain.NetworkMember == null; - bool gameOver = gameSession.CrewManager.GetCharacters().All(c => c.IsDead || c.IsIncapacitated); - bool progress = Submarine.MainSub.AtEndPosition; + bool gameOver = + gameSession.GameMode.IsSinglePlayer ? + gameSession.CrewManager.GetCharacters().All(c => c.IsDead || c.IsIncapacitated) : + gameSession.CrewManager.GetCharacters().All(c => c.IsDead || c.IsIncapacitated || c.IsBot); + if (!singleplayer) { SoundPlayer.OverrideMusicType = gameOver ? "crewdead" : "endround"; SoundPlayer.OverrideMusicDuration = 18.0f; } - GUIFrame background = new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas, Anchor.Center), style: "GUIBackgroundBlocker"); - - GUIFrame frame = new GUIFrame(new RectTransform(Vector2.One, background.RectTransform, Anchor.Center), style: null) + GUIFrame background = new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas, Anchor.Center), style: "GUIBackgroundBlocker") { - UserData = "roundsummary" + UserData = this }; - int width = 760, height = 500; - GUIFrame innerFrame = new GUIFrame(new RectTransform(new Vector2(0.4f, 0.5f), frame.RectTransform, Anchor.Center, minSize: new Point(width, height))); - var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), innerFrame.RectTransform, Anchor.Center)) + List rightPanels = new List(); + + int minWidth = 400, minHeight = 350; + int padding = GUI.IntScale(25.0f); + + //crew panel ------------------------------------------------------------------------------- + + GUIFrame crewFrame = new GUIFrame(new RectTransform(new Vector2(0.35f, 0.55f), background.RectTransform, Anchor.TopCenter, minSize: new Point(minWidth, minHeight))); + GUIFrame crewFrameInner = new GUIFrame(new RectTransform(new Point(crewFrame.Rect.Width - padding * 2, crewFrame.Rect.Height - padding * 2), crewFrame.RectTransform, Anchor.Center), style: "InnerFrame"); + + var crewContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), crewFrameInner.RectTransform, Anchor.Center)) { - Stretch = true, - RelativeSpacing = 0.03f + Stretch = true }; - GUIListBox infoTextBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.7f), paddedFrame.RectTransform)) + var crewHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), crewContent.RectTransform), + TextManager.Get("crew"), textAlignment: Alignment.TopLeft, font: GUI.SubHeadingFont); + crewHeader.RectTransform.MinSize = new Point(0, GUI.IntScale(crewHeader.Rect.Height * 2.0f)); + + CreateCrewList(crewContent, gameSession.CrewManager.GetCharacterInfos().Where(c => c.TeamID != Character.TeamType.Team2)); + + //another crew frame for the 2nd team in combat missions + if (gameSession.Mission is CombatMission) { - Spacing = (int)(5 * GUI.Scale) - }; - - //spacing - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), infoTextBox.Content.RectTransform), style: null); - - string summaryText = TextManager.GetWithVariables(gameOver ? "RoundSummaryGameOver" : - (progress ? "RoundSummaryProgress" : "RoundSummaryReturn"), new string[2] { "[sub]", "[location]" }, - new string[2] { Submarine.MainSub.Info.Name, progress ? GameMain.GameSession.EndLocation.Name : GameMain.GameSession.StartLocation.Name }); - - var infoText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoTextBox.Content.RectTransform), - summaryText, wrap: true); - - GUIComponent endText = null; - if (!string.IsNullOrWhiteSpace(endMessage)) - { - endText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoTextBox.Content.RectTransform), - TextManager.GetServerMessage(endMessage), wrap: true); + crewHeader.Text = CombatMission.GetTeamName(Character.TeamType.Team1); + GUIFrame crewFrame2 = new GUIFrame(new RectTransform(new Vector2(0.35f, 0.55f), background.RectTransform, Anchor.TopCenter, minSize: new Point(minWidth, minHeight))); + rightPanels.Add(crewFrame2); + GUIFrame crewFrameInner2 = new GUIFrame(new RectTransform(new Point(crewFrame2.Rect.Width - padding * 2, crewFrame2.Rect.Height - padding * 2), crewFrame2.RectTransform, Anchor.Center), style: "InnerFrame"); + var crewContent2 = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), crewFrameInner2.RectTransform, Anchor.Center)) + { + Stretch = true + }; + var crewHeader2 = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), crewContent2.RectTransform), + CombatMission.GetTeamName(Character.TeamType.Team2), textAlignment: Alignment.TopLeft, font: GUI.SubHeadingFont); + crewHeader2.RectTransform.MinSize = new Point(0, GUI.IntScale(crewHeader2.Rect.Height * 2.0f)); + CreateCrewList(crewContent2, gameSession.CrewManager.GetCharacterInfos().Where(c => c.TeamID == Character.TeamType.Team2)); } - //don't show the mission info if the mission was not completed and there's no localized "mission failed" text available - if (GameMain.GameSession.Mission != null) + //header ------------------------------------------------------------------------------- + + string headerText = GetHeaderText(gameOver, transitionType); + GUITextBlock headerTextBlock = null; + if (!string.IsNullOrEmpty(headerText)) { - string message = GameMain.GameSession.Mission.Completed ? GameMain.GameSession.Mission.SuccessMessage : GameMain.GameSession.Mission.FailureMessage; - if (!string.IsNullOrEmpty(message)) - { - //spacing - var spacingTransform = new RectTransform(new Vector2(1.0f, 0.1f), infoTextBox.Content.RectTransform); - - new GUIFrame(spacingTransform, style: null); - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoTextBox.Content.RectTransform), - TextManager.AddPunctuation(':', TextManager.Get("Mission"), GameMain.GameSession.Mission.Name), - font: GUI.LargeFont); - - var missionInfo = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoTextBox.Content.RectTransform), - message, wrap: true); - - if (GameMain.GameSession.Mission.Completed && singleplayer) - { - var missionReward = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoTextBox.Content.RectTransform), - TextManager.GetWithVariable("MissionReward", "[reward]", GameMain.GameSession.Mission.Reward.ToString())); - } - } + 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); } - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), - TextManager.Get("RoundSummaryCrewStatus"), font: GUI.LargeFont); - GUIListBox characterListBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.4f), paddedFrame.RectTransform, minSize: new Point(0, 75)), isHorizontal: true); - - foreach (CharacterInfo characterInfo in gameSession.CrewManager.GetCharacterInfos()) + //traitor panel ------------------------------------------------------------------------------- + + if (traitorResults != null && traitorResults.Any()) { - if (GameMain.GameSession.Mission is CombatMission && - characterInfo.TeamID != GameMain.GameSession.WinningTeam) - { - continue; - } + GUIFrame traitorframe = new GUIFrame(new RectTransform(crewFrame.RectTransform.RelativeSize, background.RectTransform, Anchor.TopCenter, minSize: crewFrame.RectTransform.MinSize)); + rightPanels.Add(traitorframe); + GUIFrame traitorframeInner = new GUIFrame(new RectTransform(new Point(traitorframe.Rect.Width - padding * 2, traitorframe.Rect.Height - padding * 2), traitorframe.RectTransform, Anchor.Center), style: "InnerFrame"); - var characterFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.2f, 1.0f), characterListBox.Content.RectTransform, minSize: new Point(170, 0))) + var traitorContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), traitorframeInner.RectTransform, Anchor.Center)) { - CanBeFocused = false, Stretch = true }; - characterInfo.CreateCharacterFrame(characterFrame, - characterInfo.Job != null ? (characterInfo.Name + '\n' + "(" + characterInfo.Job.Name + ")") : characterInfo.Name, null); + var traitorHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), traitorContent.RectTransform), + TextManager.Get("traitors"), font: GUI.SubHeadingFont); + traitorHeader.RectTransform.MinSize = new Point(0, GUI.IntScale(traitorHeader.Rect.Height * 2.0f)); - string statusText = TextManager.Get("StatusOK"); - Color statusColor = Color.DarkGreen; + GUIListBox listBox = CreateCrewList(traitorContent, traitorResults.SelectMany(tr => tr.Characters.Select(c => c.Info))); - Character character = characterInfo.Character; - if (character == null || character.IsDead) + foreach (var traitorResult in traitorResults) { - if (characterInfo.CauseOfDeath == null) + var traitorMission = TraitorMissionPrefab.List.Find(t => t.Identifier == traitorResult.MissionIdentifier); + if (traitorMission == null) { continue; } + + //spacing + new GUIFrame(new RectTransform(new Point(listBox.Content.Rect.Width, GUI.IntScale(25)), listBox.Content.RectTransform), style: null); + + var traitorResultHorizontal = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), listBox.Content.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true) { - statusText = TextManager.Get("CauseOfDeathDescription.Unknown"); + RelativeSpacing = 0.05f, + Stretch = true + }; + + new GUIImage(new RectTransform(new Point(traitorResultHorizontal.Rect.Height), traitorResultHorizontal.RectTransform), traitorMission.Icon, scaleToFit: true) + { + Color = traitorMission.IconColor + }; + + string traitorMessage = TextManager.GetServerMessage(traitorResult.EndMessage); + if (!string.IsNullOrEmpty(traitorMessage)) + { + var textContent = new GUILayoutGroup(new RectTransform(Vector2.One, traitorResultHorizontal.RectTransform)) + { + RelativeSpacing = 0.025f + }; + + 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); + + var traitorMissionInfo = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), textContent.RectTransform), + traitorMessage, font: GUI.SmallFont, wrap: true); + + traitorResultHorizontal.Recalculate(); + + traitorStatusText.CalculateHeightFromText(); + traitorMissionInfo.CalculateHeightFromText(); + traitorStatusText.RectTransform.MinSize = new Point(0, traitorStatusText.Rect.Height); + traitorMissionInfo.RectTransform.MinSize = new Point(0, traitorMissionInfo.Rect.Height); + textContent.RectTransform.MaxSize = new Point(int.MaxValue, (int)((traitorStatusText.Rect.Height + traitorMissionInfo.Rect.Height) * 1.2f)); + traitorResultHorizontal.RectTransform.MinSize = new Point(0, traitorStatusText.RectTransform.MinSize.Y + traitorMissionInfo.RectTransform.MinSize.Y); } - else if (characterInfo.CauseOfDeath.Type == CauseOfDeathType.Affliction && characterInfo.CauseOfDeath.Affliction == null) + } + } + + //reputation panel ------------------------------------------------------------------------------- + + if (gameMode is CampaignMode campaignMode) + { + GUIFrame reputationframe = new GUIFrame(new RectTransform(crewFrame.RectTransform.RelativeSize, background.RectTransform, Anchor.TopCenter, minSize: crewFrame.RectTransform.MinSize)); + rightPanels.Add(reputationframe); + GUIFrame reputationframeInner = new GUIFrame(new RectTransform(new Point(reputationframe.Rect.Width - padding * 2, reputationframe.Rect.Height - padding * 2), reputationframe.RectTransform, Anchor.Center), style: "InnerFrame"); + + var reputationContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), reputationframeInner.RectTransform, Anchor.Center)) + { + Stretch = true + }; + + var reputationHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), reputationContent.RectTransform), + TextManager.Get("reputation"), textAlignment: Alignment.TopLeft, font: GUI.SubHeadingFont); + reputationHeader.RectTransform.MinSize = new Point(0, GUI.IntScale(reputationHeader.Rect.Height * 2.0f)); + + GUIListBox reputationList = new GUIListBox(new RectTransform(Vector2.One, reputationContent.RectTransform)) + { + Padding = new Vector4(2, 5, 0, 0) + }; + reputationList.ContentBackground.Color = Color.Transparent; + + if (startLocation.Type.HasOutpost && startLocation.Reputation != null) + { + var iconStyle = GUI.Style.GetComponentStyle("LocationReputationIcon"); + CreateReputationElement( + reputationList.Content, + startLocation.Name, + startLocation.Reputation.Value, startLocation.Reputation.NormalizedValue, initialLocationReputation, + startLocation.Type.Name, "", + iconStyle?.GetDefaultSprite(), startLocation.Type.GetPortrait(0), iconStyle?.Color ?? Color.White); + } + + foreach (Faction faction in campaignMode.Factions) + { + float initialReputation = faction.Reputation.Value; + if (initialFactionReputations.ContainsKey(faction)) { - string errorMsg = "Character \"" + 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); - GameAnalyticsManager.AddErrorEventOnce("RoundSummary:InvalidCauseOfDeath", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); - statusText = TextManager.Get("CauseOfDeathDescription.Unknown"); + initialReputation = initialFactionReputations[faction]; } else { - statusText = characterInfo.CauseOfDeath.Type == CauseOfDeathType.Affliction ? - characterInfo.CauseOfDeath.Affliction.CauseOfDeathDescription : - TextManager.Get("CauseOfDeathDescription." + characterInfo.CauseOfDeath.Type.ToString()); + DebugConsole.AddWarning($"Could not determine reputation change for faction \"{faction.Prefab.Name}\" (faction was not present at the start of the round)."); } + CreateReputationElement( + reputationList.Content, + faction.Prefab.Name, + faction.Reputation.Value, faction.Reputation.NormalizedValue, initialReputation, + faction.Prefab.ShortDescription, faction.Prefab.Description, + faction.Prefab.Icon, faction.Prefab.BackgroundPortrait, faction.Prefab.IconColor); + } + + float otherElementHeight = 0.0f; + float maxDescriptionHeight = 0.0f; + foreach (GUIComponent child in reputationList.Content.Children) + { + var descriptionElement = child.FindChild("description", recursive: true) as GUITextBlock; + maxDescriptionHeight = Math.Max(maxDescriptionHeight, descriptionElement.TextSize.Y * 1.1f); + otherElementHeight = Math.Max(otherElementHeight, descriptionElement.Parent.Rect.Height - descriptionElement.TextSize.Y); + } + foreach (GUIComponent child in reputationList.Content.Children) + { + var descriptionElement = child.FindChild("description", recursive: true) as GUITextBlock; + descriptionElement.RectTransform.MaxSize = new Point(int.MaxValue, (int)(maxDescriptionHeight)); + child.RectTransform.MaxSize = new Point(int.MaxValue, (int)((maxDescriptionHeight + otherElementHeight) * 1.2f)); + (descriptionElement?.Parent as GUILayoutGroup).Recalculate(); + } + } + + //mission panel ------------------------------------------------------------------------------- + + GUIFrame missionframe = new GUIFrame(new RectTransform(new Vector2(0.39f, 0.22f), background.RectTransform, Anchor.TopCenter, minSize: new Point(minWidth, minHeight / 4))); + GUIFrame missionframeInner = new GUIFrame(new RectTransform(new Point(missionframe.Rect.Width - padding * 2, missionframe.Rect.Height - padding * 2), missionframe.RectTransform, Anchor.Center), style: "InnerFrame"); + + var missionContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), missionframeInner.RectTransform, Anchor.Center)) + { + RelativeSpacing = 0.05f, + Stretch = true + }; + + if (!string.IsNullOrWhiteSpace(endMessage)) + { + var endText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionContent.RectTransform), + TextManager.GetServerMessage(endMessage), wrap: true); + endText.RectTransform.MinSize = new Point(0, endText.Rect.Height); + var line = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.1f), missionContent.RectTransform), style: "HorizontalLine"); + line.RectTransform.NonScaledSize = new Point(line.Rect.Width, GUI.IntScale(5.0f)); + } + + var missionContentHorizontal = new GUILayoutGroup(new RectTransform(Vector2.One, missionContent.RectTransform), childAnchor: Anchor.TopLeft, isHorizontal: true) + { + RelativeSpacing = 0.025f, + Stretch = true + }; + + Mission displayedMission = selectedMission ?? startLocation.SelectedMission; + string missionMessage = ""; + GUIImage missionIcon; + if (displayedMission != null) + { + missionMessage = + displayedMission == selectedMission ? + displayedMission.Completed ? displayedMission.SuccessMessage : displayedMission.FailureMessage : + displayedMission.Description; + missionIcon = new GUIImage(new RectTransform(new Point(missionContentHorizontal.Rect.Height), missionContentHorizontal.RectTransform), displayedMission.Prefab.Icon, scaleToFit: true) + { + Color = displayedMission.Prefab.IconColor + }; + if (displayedMission == selectedMission) + { + new GUIImage(new RectTransform(Vector2.One, missionIcon.RectTransform), displayedMission.Completed ? "MissionCompletedIcon" : "MissionFailedIcon", scaleToFit: true); + } + } + else + { + missionIcon = new GUIImage(new RectTransform(new Point(missionContentHorizontal.Rect.Height), missionContentHorizontal.RectTransform), style: "NoMissionIcon", scaleToFit: true); + } + var missionTextContent = new GUILayoutGroup(new RectTransform(Vector2.One, missionContentHorizontal.RectTransform)) + { + RelativeSpacing = 0.05f + }; + missionContentHorizontal.Recalculate(); + missionContent.Recalculate(); + missionIcon.RectTransform.MinSize = new Point(0, missionContentHorizontal.Rect.Height); + missionTextContent.RectTransform.MaxSize = new Point(int.MaxValue, missionIcon.Rect.Width); + + GUITextBlock missionDescription = null; + if (displayedMission == null) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), + TextManager.Get("nomission"), font: GUI.LargeFont); + } + else + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), + TextManager.AddPunctuation(':', TextManager.Get("Mission"), displayedMission.Name), font: GUI.SubHeadingFont); + missionDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), + missionMessage, wrap: true); + if (displayedMission == selectedMission && displayedMission.Completed) + { + string rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", displayedMission.Reward)); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), + TextManager.GetWithVariable("MissionReward", "[reward]", rewardText)); + } + } + + ButtonArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), missionContent.RectTransform, Anchor.BottomCenter), isHorizontal: true, childAnchor: Anchor.BottomRight) + { + IgnoreLayoutGroups = true, + RelativeSpacing = 0.025f + }; + + ContinueButton = new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), ButtonArea.RectTransform), TextManager.Get("Close")); + ButtonArea.RectTransform.NonScaledSize = new Point(ButtonArea.Rect.Width, ContinueButton.Rect.Height); + ButtonArea.RectTransform.IsFixedSize = true; + + missionContent.Recalculate(); + //description overlapping with the buttons -> switch to small font + if (missionDescription != null && missionDescription.Rect.Y + missionDescription.TextSize.Y > ButtonArea.Rect.Y) + { + missionDescription.Font = GUI.Style.SmallFont; + //still overlapping -> shorten the text + if (missionDescription.Rect.Y + missionDescription.TextSize.Y > ButtonArea.Rect.Y && missionDescription.WrappedText.Contains('\n')) + { + missionDescription.ToolTip = missionDescription.Text; + missionDescription.Text = missionDescription.WrappedText.Split('\n').First() + "..."; + } + } + + // set layout ------------------------------------------------------------------- + + int panelSpacing = GUI.IntScale(20); + int totalHeight = crewFrame.Rect.Height + panelSpacing + missionframe.Rect.Height; + int totalWidth = crewFrame.Rect.Width; + + crewFrame.RectTransform.AbsoluteOffset = new Point(0, (GameMain.GraphicsHeight - totalHeight) / 2); + missionframe.RectTransform.AbsoluteOffset = new Point(0, crewFrame.Rect.Bottom + panelSpacing); + + if (rightPanels.Any()) + { + totalWidth = crewFrame.Rect.Width * 2 + panelSpacing; + if (headerTextBlock != null) + { + headerTextBlock.RectTransform.MinSize = new Point(totalWidth, 0); + } + crewFrame.RectTransform.AbsoluteOffset = new Point(-(crewFrame.Rect.Width + panelSpacing) / 2, crewFrame.RectTransform.AbsoluteOffset.Y); + foreach (var rightPanel in rightPanels) + { + rightPanel.RectTransform.AbsoluteOffset = new Point((rightPanel.Rect.Width + panelSpacing) / 2, crewFrame.RectTransform.AbsoluteOffset.Y); + } + } + + if (!(gameSession.GameMode is CampaignMode)) + { + var shadow = new GUIFrame(new RectTransform(new Point((int)(totalWidth * 1.2f), GameMain.GraphicsHeight * 2), background.RectTransform, Anchor.Center), style: "OuterGlow") + { + Color = Color.Black + }; + shadow.RectTransform.SetAsFirstChild(); + } + + Frame = background; + return background; + } + + private string GetHeaderText(bool gameOver, CampaignMode.TransitionType transitionType) + { + string locationName = Submarine.MainSub.AtEndPosition ? endLocation?.Name : startLocation?.Name; + + string textTag; + if (gameOver) + { + textTag = "RoundSummaryGameOver"; + } + else + { + switch (transitionType) + { + case CampaignMode.TransitionType.LeaveLocation: + locationName = startLocation?.Name; + textTag = "RoundSummaryLeaving"; + break; + case CampaignMode.TransitionType.ProgressToNextLocation: + case CampaignMode.TransitionType.ProgressToNextEmptyLocation: + locationName = endLocation?.Name; + textTag = "RoundSummaryProgress"; + break; + case CampaignMode.TransitionType.ReturnToPreviousLocation: + case CampaignMode.TransitionType.ReturnToPreviousEmptyLocation: + locationName = startLocation?.Name; + textTag = "RoundSummaryReturn"; + break; + default: + textTag = Submarine.MainSub.AtEndPosition ? "RoundSummaryProgress" : "RoundSummaryReturn"; + break; + } + } + + if (textTag == null) { return ""; } + + if (locationName == null) + { + DebugConsole.ThrowError($"Error while creating round summary: could not determine destination location. Start location: {startLocation?.Name ?? "null"}, end location: {endLocation?.Name ?? "null"}"); + locationName = "[UNKNOWN]"; + } + + string 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 }); + } + + private GUIListBox CreateCrewList(GUIComponent parent, IEnumerable characterInfos) + { + var headerFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform, Anchor.TopCenter, minSize: new Point(0, (int)(30 * GUI.Scale))) { }, isHorizontal: true) + { + AbsoluteSpacing = 2 + }; + 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 statusButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("label.statuslabel"), style: "GUIButtonSmallFreeScale"); + + float sizeMultiplier = 1.0f; + //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); + statusButton.RectTransform.RelativeSize = new Vector2(statusColumnWidthPercentage * sizeMultiplier, 1f); + + jobButton.TextBlock.Font = characterButton.TextBlock.Font = statusButton.TextBlock.Font = GUI.HotkeyFont; + jobButton.CanBeFocused = characterButton.CanBeFocused = statusButton.CanBeFocused = false; + jobButton.TextBlock.ForceUpperCase = characterButton.TextBlock.ForceUpperCase = statusButton.ForceUpperCase = true; + + jobColumnWidth = jobButton.Rect.Width; + characterColumnWidth = characterButton.Rect.Width; + statusColumnWidth = statusButton.Rect.Width; + + GUIListBox crewList = new GUIListBox(new RectTransform(Vector2.One, parent.RectTransform)) + { + Padding = new Vector4(2, 5, 0, 0), + AutoHideScrollBar = false + }; + crewList.ContentBackground.Color = Color.Transparent; + + headerFrame.RectTransform.RelativeSize -= new Vector2(crewList.ScrollBar.RectTransform.RelativeSize.X, 0.0f); + + foreach (CharacterInfo characterInfo in characterInfos) + { + if (characterInfo == null) { continue; } + CreateCharacterElement(characterInfo, crewList); + } + + return crewList; + } + + private void CreateCharacterElement(CharacterInfo characterInfo, GUIListBox listBox) + { + GUIFrame frame = new GUIFrame(new RectTransform(new Point(listBox.Content.Rect.Width, GUI.IntScale(45)), listBox.Content.RectTransform), style: "ListBoxElement") + { + CanBeFocused = false, + UserData = characterInfo, + Color = (Character.Controlled?.Info == characterInfo) ? TabMenu.OwnCharacterBGColor : Color.Transparent + }; + + var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: true) + { + AbsoluteSpacing = 2, + Stretch = true + }; + + new GUICustomComponent(new RectTransform(new Point(jobColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), onDraw: (sb, component) => characterInfo.DrawJobIcon(sb, component.Rect)) + { + ToolTip = characterInfo.Job.Name ?? "", + HoverColor = Color.White, + SelectedColor = Color.White + }; + + 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); + + string statusText = TextManager.Get("StatusOK"); + Color statusColor = GUI.Style.Green; + + Character character = characterInfo.Character; + if (character == null || character.IsDead) + { + if (characterInfo.IsNewHire) + { + statusText = TextManager.Get("CampaignCrew.NewHire"); + statusColor = GUI.Style.Blue; + } + else if (characterInfo.CauseOfDeath == null) + { + statusText = TextManager.Get("CauseOfDeathDescription.Unknown"); statusColor = Color.DarkRed; } + else if (characterInfo.CauseOfDeath.Type == CauseOfDeathType.Affliction && characterInfo.CauseOfDeath.Affliction == null) + { + string errorMsg = "Character \"" + characterInfo.Name + "\" had an invalid cause of death (the type of the cause of death was Affliction, but affliction was not specified)."; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("RoundSummary:InvalidCauseOfDeath", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + statusText = TextManager.Get("CauseOfDeathDescription.Unknown"); + statusColor = GUI.Style.Red; + } else { - if (character.IsUnconscious) - { - statusText = TextManager.Get("Unconscious"); - statusColor = Color.DarkOrange; - } - else if (character.Vitality / character.MaxVitality < 0.8f) - { - statusText = TextManager.Get("Injured"); - statusColor = Color.DarkOrange; - } + statusText = characterInfo.CauseOfDeath.Type == CauseOfDeathType.Affliction ? + characterInfo.CauseOfDeath.Affliction.CauseOfDeathDescription : + TextManager.Get("CauseOfDeathDescription." + characterInfo.CauseOfDeath.Type.ToString()); + statusColor = Color.DarkRed; + } + } + else + { + if (character.IsUnconscious) + { + statusText = TextManager.Get("Unconscious"); + statusColor = Color.DarkOrange; + } + else if (character.Vitality / character.MaxVitality < 0.8f) + { + statusText = TextManager.Get("Injured"); + statusColor = Color.DarkOrange; } - - var textHolder = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), characterFrame.RectTransform, Anchor.BottomCenter), style: "InnerGlow", color: statusColor); - new GUITextBlock(new RectTransform(Vector2.One, textHolder.RectTransform, Anchor.Center), - statusText, Color.White, - textAlignment: Alignment.Center, - wrap: true, font: GUI.SmallFont, style: null); } - new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), paddedFrame.RectTransform), isHorizontal: true, childAnchor: Anchor.BottomRight) + 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); + } + + private void CreateReputationElement(GUIComponent parent, + string name, float reputation, float normalizedReputation, float initialReputation, + string shortDescription, string fullDescription, Sprite icon, Sprite backgroundPortrait, Color iconColor) + { + var factionFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.3f), parent.RectTransform), style: null); + + if (backgroundPortrait != null) { - RelativeSpacing = 0.05f, - UserData = "buttonarea" + new GUICustomComponent(new RectTransform(Vector2.One, factionFrame.RectTransform), onDraw: (sb, customComponent) => + { + backgroundPortrait.Draw(sb, customComponent.Rect.Center.ToVector2(), customComponent.Color, backgroundPortrait.size / 2, scale: customComponent.Rect.Width / backgroundPortrait.size.X); + }) + { + HideElementsOutsideFrame = true, + IgnoreLayoutGroups = true, + Color = iconColor * 0.2f + }; + } + + var factionInfoHorizontal = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), factionFrame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterLeft, isHorizontal: true) + { + RelativeSpacing = 0.02f, + Stretch = true }; - paddedFrame.Recalculate(); - foreach (GUIComponent child in infoTextBox.Content.Children) + var factionTextContent = new GUILayoutGroup(new RectTransform(Vector2.One, factionInfoHorizontal.RectTransform)) { - child.CanBeFocused = false; - if (child is GUITextBlock textBlock) - { - textBlock.CalculateHeightFromText(); - } + RelativeSpacing = 0.05f, + Stretch = true + }; + var factionIcon = new GUIImage(new RectTransform(new Point((int)(factionInfoHorizontal.Rect.Height * 0.7f)), factionInfoHorizontal.RectTransform, scaleBasis: ScaleBasis.Smallest), icon, scaleToFit: true) + { + Color = iconColor + }; + factionInfoHorizontal.Recalculate(); + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), factionTextContent.RectTransform), + name, font: GUI.SubHeadingFont) + { + Padding = Vector4.Zero + }; + var factionDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.6f), factionTextContent.RectTransform), + shortDescription, font: GUI.SmallFont, wrap: true) + { + UserData = "description", + Padding = Vector4.Zero + }; + if (shortDescription != fullDescription && !string.IsNullOrEmpty(fullDescription)) + { + factionDescription.ToolTip = fullDescription; } - return frame; + var sliderHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), factionTextContent.RectTransform), + childAnchor: Anchor.CenterLeft, isHorizontal: true) + { + RelativeSpacing = 0.05f, + Stretch = true + }; + sliderHolder.RectTransform.MaxSize = new Point(int.MaxValue, GUI.IntScale(25.0f)); + factionTextContent.Recalculate(); + + new GUICustomComponent(new RectTransform(new Vector2(0.8f, 1.0f), sliderHolder.RectTransform), onDraw: (sb, customComponent) => + { + GUI.DrawRectangle(sb, customComponent.Rect, GUI.Style.ColorInventoryBackground, isFilled: true); + if (normalizedReputation < 0.5f) + { + int barWidth = (int)((0.5f - normalizedReputation) * customComponent.Rect.Width); + GUI.DrawRectangle(sb, new Rectangle(customComponent.Rect.Center.X - barWidth, customComponent.Rect.Y, barWidth, customComponent.Rect.Height), GUI.Style.Red, isFilled: true); + } + else if (normalizedReputation > 0.5f) + { + int barWidth = (int)((normalizedReputation - 0.5f) * customComponent.Rect.Width); + GUI.DrawRectangle(sb, new Rectangle(customComponent.Rect.Center.X, customComponent.Rect.Y, barWidth, customComponent.Rect.Height), GUI.Style.Green, isFilled: true); + } + GUI.DrawLine(sb, new Vector2(customComponent.Rect.Center.X, customComponent.Rect.Y - 2), new Vector2(customComponent.Rect.Center.X, customComponent.Rect.Bottom + 2), factionDescription.TextColor, width: 1); + }); + + string reputationText = ((int)Math.Round(reputation)).ToString(); + 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); + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), sliderHolder.RectTransform), + rtData, sanitizedText, + textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont); + } + else + { + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), sliderHolder.RectTransform), + reputationText, + textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont); + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/UpgradeManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/UpgradeManager.cs new file mode 100644 index 000000000..e66fb012d --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/UpgradeManager.cs @@ -0,0 +1,81 @@ +#nullable enable +using System; +using System.Linq; +using Barotrauma.Networking; + +namespace Barotrauma +{ + partial class UpgradeManager + { + partial void UpgradeNPCSpeak(string text, bool isSinglePlayer, Character? character) + { + if (Level.Loaded?.StartOutpost?.Info?.OutpostNPCs == null) { return; } + + if (character != null) + { + Speak(character); + return; + } + + foreach (Character npc in Level.Loaded.StartOutpost.Info.OutpostNPCs.SelectMany(kpv => kpv.Value)) + { + if (npc.CampaignInteractionType == CampaignMode.InteractionType.Upgrade) + { + Speak(npc); + break; + } + } + + void Speak(Character npc) + { + ChatMessage message = ChatMessage.Create(npc.Name, text, ChatMessageType.Default, npc); + if (!isSinglePlayer) + { + GameMain.Client?.AddChatMessage(message); + } + else + { + GameMain.GameSession?.CrewManager?.AddSinglePlayerChatMessage(message); + } + } + } + + /// + /// Server has notified us that upgrades were reset. + /// + /// + /// + public void ClientRead(IReadMessage inc) + { + bool shouldReset = inc.ReadBoolean(); + int money = inc.ReadInt32(); + // uint length = inc.ReadUInt32(); + // + // for (int i = 0; i < length; i++) + // { + // string key = inc.ReadString(); + // byte value = inc.ReadByte(); + // Metadata.SetValue(key, value); + // } + + Campaign.Money = money; + + if (shouldReset) + { + ResetUpgrades(); + } + + // spentMoney is local, so this message box should only appear for those who have spent money on upgrades + if (spentMoney > 0) + { + GUIMessageBox msgBox = new GUIMessageBox(TextManager.Get("UpgradeRefundTitle"), TextManager.Get("UpgradeRefundBody"), new [] { TextManager.Get("Ok") }); + msgBox.Buttons[0].OnClicked += msgBox.Close; + } + + spentMoney = 0; + PendingUpgrades.Clear(); + PurchasedUpgrades.Clear(); + CanUpgrade = false; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs index c3f9fb6a3..bb1f6e4af 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs @@ -761,6 +761,44 @@ namespace Barotrauma 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) @@ -893,7 +931,7 @@ namespace Barotrauma deviceList.OnSelected = (GUIComponent selected, object obj) => { string name = obj as string; - if (VoiceCaptureDevice == name) { return true; } + if (!(VoipCapture.Instance?.Disconnected ?? true) && VoiceCaptureDevice == name) { return true; } VoipCapture.ChangeCaptureDevice(name); return true; @@ -1179,12 +1217,16 @@ namespace Barotrauma 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: KeyBindText((InputType)i), font: GUI.SmallFont, style: "GUITextBoxNoIcon") + text: keyText, 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)); + keyBox.RectTransform.SizeChanged += () => + { + keyBox.Text = ToolBox.LimitString(keyText, keyBox.Font, (int)(keyBox.Rect.Width - keyBox.Padding.X - keyBox.Padding.Z)); + }; keyBox.OnSelected += KeyBoxSelected; keyBox.SelectedColor = Color.Gold * 0.3f; } @@ -1337,6 +1379,16 @@ namespace Barotrauma return true; }; + var automaticCampaignLoadTickBox = new GUITickBox(new RectTransform(tickBoxScale / 0.18f, debugTickBoxes.RectTransform, scaleBasis: ScaleBasis.BothHeight), "Automatic campaign load enabled", style: "GUITickBox"); + automaticCampaignLoadTickBox.Selected = AutomaticCampaignLoadEnabled; + automaticCampaignLoadTickBox.ToolTip = "Will the game automatically load the latest campaign save when the game is launched"; + automaticCampaignLoadTickBox.OnSelected = (tickBox) => + { + AutomaticCampaignLoadEnabled = tickBox.Selected; + UnsavedSettings = true; + return true; + }; + var showSplashScreenTickBox = new GUITickBox(new RectTransform(tickBoxScale / 0.18f, debugTickBoxes.RectTransform, scaleBasis: ScaleBasis.BothHeight), "Splash screen enabled", style: "GUITickBox"); showSplashScreenTickBox.Selected = EnableSplashScreen; showSplashScreenTickBox.ToolTip = "Are the splash screens shown when the game is launched"; @@ -1520,8 +1572,7 @@ namespace Barotrauma { return GameMain.Client == null && (ContentPackage.IngameModSwap || - (Screen.Selected != GameMain.GameScreen && - Screen.Selected != GameMain.LobbyScreen) && + Screen.Selected != GameMain.GameScreen && Screen.Selected != GameMain.SubEditorScreen) && (!core || (Screen.Selected != GameMain.CharacterEditorScreen && diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 50a8cb166..1859241ea 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -35,6 +35,26 @@ namespace Barotrauma } private static Dictionary limbSlotIcons; + public static Dictionary LimbSlotIcons + { + get + { + if (limbSlotIcons == null) + { + limbSlotIcons = new Dictionary(); + int margin = 2; + limbSlotIcons.Add(InvSlotType.Headset, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(384 + margin, 128 + margin, 128 - margin * 2, 128 - margin * 2))); + limbSlotIcons.Add(InvSlotType.InnerClothes, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(512 + margin, 128 + margin, 128 - margin * 2, 128 - margin * 2))); + limbSlotIcons.Add(InvSlotType.Card, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(640 + margin, 128 + margin, 128 - margin * 2, 128 - margin * 2))); + + limbSlotIcons.Add(InvSlotType.Head, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(896 + margin, 128 + margin, 128 - margin * 2, 128 - margin * 2))); + limbSlotIcons.Add(InvSlotType.LeftHand, new Sprite("Content/UI/InventoryUIAtlas.png", new Rectangle(634, 0, 128, 128))); + limbSlotIcons.Add(InvSlotType.RightHand, new Sprite("Content/UI/InventoryUIAtlas.png", new Rectangle(762, 0, 128, 128))); + limbSlotIcons.Add(InvSlotType.OuterClothes, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(256 + margin, 128 + margin, 128 - margin * 2, 128 - margin * 2))); + } + return limbSlotIcons; + } + } public const InvSlotType PersonalSlots = InvSlotType.Card | InvSlotType.Headset | InvSlotType.InnerClothes | InvSlotType.OuterClothes | InvSlotType.Head; @@ -88,7 +108,7 @@ namespace Barotrauma indicatorGroup = new GUILayoutGroup(new RectTransform(Point.Zero, hideButton.RectTransform)) { IsHorizontal = false }; indicatorGroup.ChildAnchor = Anchor.TopCenter; - indicatorSpriteSize = GUI.Style.GetComponentStyle("EquipmentIndicatorDivingSuit").Sprites[GUIComponent.ComponentState.None][0].Sprite.size; + indicatorSpriteSize = GUI.Style.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"); @@ -115,20 +135,6 @@ namespace Barotrauma hidePersonalSlots = false; - if (limbSlotIcons == null) - { - limbSlotIcons = new Dictionary(); - - int margin = 2; - limbSlotIcons.Add(InvSlotType.Headset, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(384 + margin, 128 + margin, 128 - margin * 2, 128 - margin * 2))); - limbSlotIcons.Add(InvSlotType.InnerClothes, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(512 + margin, 128 + margin, 128 - margin * 2, 128 - margin * 2))); - limbSlotIcons.Add(InvSlotType.Card, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(640 + margin, 128 + margin, 128 - margin * 2, 128 - margin * 2))); - - limbSlotIcons.Add(InvSlotType.Head, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(896 + margin, 128 + margin, 128 - margin * 2, 128 - margin * 2))); - limbSlotIcons.Add(InvSlotType.LeftHand, new Sprite("Content/UI/InventoryUIAtlas.png", new Rectangle(634, 0, 128, 128))); - limbSlotIcons.Add(InvSlotType.RightHand, new Sprite("Content/UI/InventoryUIAtlas.png", new Rectangle(762, 0, 128, 128))); - limbSlotIcons.Add(InvSlotType.OuterClothes, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(256 + margin, 128 + margin, 128 - margin * 2, 128 - margin * 2))); - } SlotPositions = new Vector2[SlotTypes.Length]; CurrentLayout = Layout.Default; SetSlotPositions(layout); @@ -522,14 +528,7 @@ namespace Barotrauma if (hoverOnInventory) { HideTimer = 0.5f; } if (HideTimer > 0.0f) { HideTimer -= deltaTime; } - for (int i = 0; i < capacity; i++) - { - if (Items[i] != null && Items[i] != draggingItem && Character.Controlled?.Inventory == this && - GUI.KeyboardDispatcher.Subscriber == null && !CrewManager.IsCommandInterfaceOpen && PlayerInput.InventoryKeyHit(slots[i].InventoryKeyIndex)) - { - QuickUseItem(Items[i], true, false, true); - } - } + UpdateSlotInput(); //force personal slots open if an item is running out of battery/fuel/oxygen/etc if (hidePersonalSlots) @@ -685,6 +684,18 @@ namespace Barotrauma doubleClickedItem = null; } + public void UpdateSlotInput() + { + for (int i = 0; i < capacity; i++) + { + if (Items[i] != null && Items[i] != draggingItem && Character.Controlled?.Inventory == this && + GUI.KeyboardDispatcher.Subscriber == null && !CrewManager.IsCommandInterfaceOpen && PlayerInput.InventoryKeyHit(slots[i].InventoryKeyIndex)) + { + QuickUseItem(Items[i], true, false, true); + } + } + } + private void HandleButtonEquipStates(Item item, InventorySlot slot, float deltaTime) { slot.EquipButtonState = slot.EquipButtonRect.Contains(PlayerInput.MousePosition) ? @@ -1084,9 +1095,9 @@ namespace Barotrauma !Items[i].AllowedSlots.Any(a => a != InvSlotType.Any)) { //draw limb icons on empty slots - if (limbSlotIcons.ContainsKey(SlotTypes[i])) + if (LimbSlotIcons.ContainsKey(SlotTypes[i])) { - var icon = limbSlotIcons[SlotTypes[i]]; + var icon = LimbSlotIcons[SlotTypes[i]]; icon.Draw(spriteBatch, slots[i].Rect.Center.ToVector2() + slots[i].DrawOffset, GUI.Style.EquipmentSlotIconColor, origin: icon.size / 2, scale: slots[i].Rect.Width / icon.size.X); } continue; @@ -1096,12 +1107,12 @@ namespace Barotrauma //draw hand icons if the item is equipped in a hand slot if (IsInLimbSlot(Items[i], InvSlotType.LeftHand)) { - var icon = limbSlotIcons[InvSlotType.LeftHand]; + var icon = LimbSlotIcons[InvSlotType.LeftHand]; icon.Draw(spriteBatch, new Vector2(slots[i].Rect.X, slots[i].Rect.Bottom) + slots[i].DrawOffset, Color.White * 0.6f, origin: new Vector2(icon.size.X * 0.35f, icon.size.Y * 0.75f), scale: slots[i].Rect.Width / icon.size.X * 0.7f); } if (IsInLimbSlot(Items[i], InvSlotType.RightHand)) { - var icon = limbSlotIcons[InvSlotType.RightHand]; + var icon = LimbSlotIcons[InvSlotType.RightHand]; icon.Draw(spriteBatch, new Vector2(slots[i].Rect.Right, slots[i].Rect.Bottom) + slots[i].DrawOffset, Color.White * 0.6f, origin: new Vector2(icon.size.X * 0.65f, icon.size.Y * 0.75f), scale: slots[i].Rect.Width / icon.size.X * 0.7f); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs index fb818670e..3b6fcbe42 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs @@ -48,6 +48,8 @@ namespace Barotrauma.Items.Components private void UpdateConvexHulls() { + if (item.Removed) { return; } + doorRect = new Rectangle( item.Rect.Center.X - (int)(doorSprite.size.X / 2 * item.Scale), item.Rect.Y - item.Rect.Height / 2 + (int)(doorSprite.size.Y / 2.0f * item.Scale), @@ -92,8 +94,8 @@ namespace Barotrauma.Items.Components } } } - - if (convexHull == null) return; + + if (convexHull == null) { return; } if (rect.Height == 0 || rect.Width == 0) { @@ -128,7 +130,7 @@ namespace Barotrauma.Items.Components if (brokenSprite == null) { //broken doors turn black if no broken sprite has been configured - color *= (item.Condition / item.Prefab.Health); + color *= (item.Condition / item.MaxCondition); color.A = 255; } @@ -162,10 +164,10 @@ namespace Barotrauma.Items.Components color, 0.0f, doorSprite.Origin, item.Scale, SpriteEffects.None, doorSprite.Depth); } - if (brokenSprite != null && item.Health < item.Prefab.Health) + if (brokenSprite != null && item.Health < item.MaxCondition) { - Vector2 scale = scaleBrokenSprite ? new Vector2(1.0f, 1.0f - item.Health / item.Prefab.Health) : Vector2.One; - float alpha = fadeBrokenSprite ? 1.0f - item.Health / item.Prefab.Health : 1.0f; + Vector2 scale = scaleBrokenSprite ? new Vector2(1.0f, 1.0f - item.Health / item.MaxCondition) : Vector2.One; + float alpha = fadeBrokenSprite ? 1.0f - item.Health / item.MaxCondition : 1.0f; spriteBatch.Draw(brokenSprite.Texture, pos, new Rectangle((int)(brokenSprite.SourceRect.X + brokenSprite.size.X * openState), brokenSprite.SourceRect.Y, (int)(brokenSprite.size.X * (1.0f - openState)), (int)brokenSprite.size.Y), @@ -188,10 +190,10 @@ namespace Barotrauma.Items.Components color, 0.0f, doorSprite.Origin, item.Scale, SpriteEffects.None, doorSprite.Depth); } - if (brokenSprite != null && item.Health < item.Prefab.Health) + if (brokenSprite != null && item.Health < item.MaxCondition) { - Vector2 scale = scaleBrokenSprite ? new Vector2(1.0f - item.Health / item.Prefab.Health, 1.0f) : Vector2.One; - float alpha = fadeBrokenSprite ? 1.0f - item.Health / item.Prefab.Health : 1.0f; + Vector2 scale = scaleBrokenSprite ? new Vector2(1.0f - item.Health / item.MaxCondition, 1.0f) : Vector2.One; + float alpha = fadeBrokenSprite ? 1.0f - item.Health / item.MaxCondition : 1.0f; spriteBatch.Draw(brokenSprite.Texture, pos, new Rectangle(brokenSprite.SourceRect.X, (int)(brokenSprite.SourceRect.Y + brokenSprite.size.Y * openState), (int)brokenSprite.size.X, (int)(brokenSprite.size.Y * (1.0f - openState))), diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs index 3491978b7..b5b9fd2f7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs @@ -114,6 +114,7 @@ namespace Barotrauma.Items.Components protected override void RemoveComponentSpecific() { + base.RemoveComponentSpecific(); crosshairSprite?.Remove(); crosshairSprite = null; crosshairPointerSprite?.Remove(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index d1025621a..7b8235322 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -186,7 +186,6 @@ namespace Barotrauma.Items.Components } } - private bool shouldMuffleLooping; private float lastMuffleCheckTime; private ItemSound loopingSound; @@ -295,8 +294,6 @@ namespace Barotrauma.Items.Components PlaySound(matchingSounds[index], item.WorldPosition); } } - - private void PlaySound(ItemSound itemSound, Vector2 position) { if (Vector2.DistanceSquared(new Vector2(GameMain.SoundManager.ListenerPosition.X, GameMain.SoundManager.ListenerPosition.Y), position) > itemSound.Range * itemSound.Range) @@ -387,7 +384,6 @@ namespace Barotrauma.Items.Components return true; } - public ItemComponent GetLinkUIToComponent() { if (string.IsNullOrEmpty(LinkUIToComponent)) @@ -431,13 +427,8 @@ namespace Barotrauma.Items.Components DebugConsole.ThrowError("Error in item config \"" + item.ConfigFile + "\" - GUIFrame defined as rect, use RectTransform instead."); break; } - - Color? color = null; - if (subElement.Attribute("color") != null) color = subElement.GetAttributeColor("color", Color.White); - string style = subElement.Attribute("style") == null ? - null : subElement.GetAttributeString("style", ""); - GuiFrame = new GUIFrame(RectTransform.Load(subElement, GUI.Canvas.ItemComponentHolder, Anchor.Center), style, color); - DefaultLayout = GUILayoutSettings.Load(subElement); + GuiFrameSource = subElement; + ReloadGuiFrame(); break; case "alternativelayout": AlternativeLayout = GUILayoutSettings.Load(subElement); @@ -501,6 +492,42 @@ namespace Barotrauma.Items.Components return true; //element processed } + private XElement GuiFrameSource; + + protected void ReleaseGuiFrame() + { + if (GuiFrame != null) + { + GuiFrame.RectTransform.Parent = null; + } + } + + protected void ReloadGuiFrame() + { + if (GuiFrame != null) + { + ReleaseGuiFrame(); + } + Color? color = null; + if (GuiFrameSource.Attribute("color") != null) + { + color = GuiFrameSource.GetAttributeColor("color", Color.White); + } + string style = GuiFrameSource.Attribute("style") == null ? null : GuiFrameSource.GetAttributeString("style", ""); + GuiFrame = new GUIFrame(RectTransform.Load(GuiFrameSource, GUI.Canvas.ItemComponentHolder, Anchor.Center), style, color); + DefaultLayout = GUILayoutSettings.Load(GuiFrameSource); + if (GuiFrame != null) + { + GuiFrame.RectTransform.ParentChanged += OnGUIParentChanged; + } + GameMain.Instance.ResolutionChanged += OnResolutionChanged; + } + + /// + /// Overload this method and implement. The method is automatically called when the resolution changes. + /// + 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) { @@ -530,5 +557,29 @@ namespace Barotrauma.Items.Components yield return CoroutineStatus.Success; } + + /// + /// Launches when the parent of the GuiFrame is changed. + /// + protected void OnGUIParentChanged(RectTransform newParent) + { + if (newParent == null) + { + // Make sure to unregister. It doesn't matter if we haven't ever registered to the event. + GameMain.Instance.ResolutionChanged -= OnResolutionChangedPrivate; + } + } + + protected virtual void OnResolutionChanged() { } + + private void OnResolutionChangedPrivate() + { + if (RecreateGUIOnResolutionChange) + { + ReloadGuiFrame(); + CreateGUI(); + } + OnResolutionChanged(); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index f389f50d1..274220734 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -13,8 +13,6 @@ namespace Barotrauma.Items.Components private GUICustomComponent guiCustomComponent; - private Point prevResolution; - public Sprite InventoryTopSprite { get { return inventoryTopSprite; } @@ -115,16 +113,16 @@ namespace Barotrauma.Items.Components { CanBeFocused = false }; + GuiFrame.RectTransform.ParentChanged += OnGUIParentChanged; } else { //if a GUIFrame has been defined, draw the inventory inside it CreateGUI(); - prevResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); } } - private void CreateGUI() + protected override void CreateGUI() { var content = new GUIFrame(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center) { AbsoluteOffset = GUIStyle.ItemFrameOffset }, style: null) @@ -167,16 +165,6 @@ namespace Barotrauma.Items.Components public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) { if (hideItems || (item.body != null && !item.body.Enabled)) { return; } - - if ((prevResolution.X > 0 && prevResolution.Y > 0) && - (prevResolution.X != GameMain.GraphicsWidth || prevResolution.Y != GameMain.GraphicsHeight)) - { - GuiFrame.ClearChildren(); - CreateGUI(); - - prevResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); - } - DrawContainedItems(spriteBatch, itemDepth); } @@ -238,7 +226,7 @@ namespace Barotrauma.Items.Components if (item.FlippedY) { origin.Y = containedItem.Sprite.SourceRect.Height - origin.Y; } float containedSpriteDepth = ContainedSpriteDepth < 0.0f ? containedItem.Sprite.Depth : ContainedSpriteDepth; - containedSpriteDepth = itemDepth + (containedSpriteDepth - item.SpriteDepth) / 10000.0f; + containedSpriteDepth = itemDepth + (containedSpriteDepth - (item.Sprite?.Depth ?? item.SpriteDepth)) / 10000.0f; containedItem.Sprite.Draw( spriteBatch, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs index 4bc8d1250..75e2f6704 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs @@ -20,7 +20,7 @@ namespace Barotrauma.Items.Components private float[] charWidths; - [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", true, description: "The amount of padding around the text in pixels (left,top,right,bottom).")] public Vector4 Padding { get { return TextBlock.Padding; } @@ -34,7 +34,7 @@ namespace Barotrauma.Items.Components get { return text; } set { - if (value == text || item.Rect.Width < 5) return; + if (value == text || item.Rect.Width < 5) { return; } if (TextBlock.Rect.Width != item.Rect.Width || textBlock.Rect.Height != item.Rect.Height) { @@ -64,7 +64,7 @@ namespace Barotrauma.Items.Components get { return textColor; } set { - if (textBlock != null) textBlock.TextColor = value; + if (textBlock != null) { textBlock.TextColor = value; } textColor = value; } } @@ -75,7 +75,7 @@ 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) { textBlock.TextScale = MathHelper.Clamp(value, 0.1f, 10.0f); } } } @@ -107,7 +107,7 @@ namespace Barotrauma.Items.Components if (textBlock == null) { textBlock = new GUITextBlock(new RectTransform(item.Rect.Size), "", - textColor: textColor, font: GUI.UnscaledSmallFont, textAlignment: Alignment.Center, wrap: true, style: null) + textColor: textColor, font: GUI.UnscaledSmallFont, textAlignment: scrollable ? Alignment.CenterLeft : Alignment.Center, wrap: true, style: null) { TextDepth = item.SpriteDepth - 0.00001f, RoundToNearestPixel = false, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Ladder.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Ladder.cs index 175d48857..08958f682 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Ladder.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Ladder.cs @@ -27,8 +27,9 @@ namespace Barotrauma.Items.Components if (backgroundSprite == null) { return; } backgroundSprite.DrawTiled(spriteBatch, - new Vector2(item.DrawPosition.X - item.Rect.Width / 2, -(item.DrawPosition.Y + item.Rect.Height / 2)) - backgroundSprite.Origin, - new Vector2(backgroundSprite.size.X, item.Rect.Height), color: item.Color, + new Vector2(item.DrawPosition.X - item.Rect.Width / 2 * item.Scale, -(item.DrawPosition.Y + item.Rect.Height / 2)) - backgroundSprite.Origin * item.Scale, + new Vector2(backgroundSprite.size.X * item.Scale, item.Rect.Height), color: item.Color, + textureScale: Vector2.One * item.Scale, depth: BackgroundSpriteDepth); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs index 01439da52..53ab9ddcf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs @@ -1,5 +1,6 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework.Graphics; +using System.ComponentModel; namespace Barotrauma.Items.Components { @@ -74,6 +75,21 @@ namespace Barotrauma.Items.Components } } +#if DEBUG + public override void CreateEditingHUD(SerializableEntityEditor editor) + { + base.CreateEditingHUD(editor); + + foreach (LimbPos limbPos in limbPositions) + { + PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(limbPos); + + PropertyDescriptor limbPosProperty = properties.Find("Position", false); + editor.CreateVector2Field(limbPos, new SerializableProperty(limbPosProperty), limbPos.Position, limbPos.LimbType.ToString(), ""); + } + } +#endif + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { State = msg.ReadBoolean(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs index db1e8fef3..b565fb267 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs @@ -23,17 +23,15 @@ namespace Barotrauma.Items.Components partial void InitProjSpecific(XElement element) { CreateGUI(); - GameMain.Instance.OnResolutionChanged += RecreateGUI; } - private void RecreateGUI() + protected override void OnResolutionChanged() { - GuiFrame.ClearChildren(); - CreateGUI(); + base.OnResolutionChanged(); OnItemLoadedProjSpecific(); } - private void CreateGUI() + protected override void CreateGUI() { var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.90f, 0.80f), GuiFrame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) { @@ -170,10 +168,5 @@ namespace Barotrauma.Items.Components SetActive(msg.ReadBoolean()); progressTimer = msg.ReadSingle(); } - - protected override void RemoveComponentSpecific() - { - GameMain.Instance.OnResolutionChanged -= RecreateGUI; - } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index f08be819d..3fb2886d0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -42,17 +42,15 @@ namespace Barotrauma.Items.Components partial void InitProjSpecific() { CreateGUI(); - GameMain.Instance.OnResolutionChanged += RecreateGUI; } - private void RecreateGUI() + protected override void OnResolutionChanged() { - GuiFrame.ClearChildren(); - CreateGUI(); + base.OnResolutionChanged(); OnItemLoadedProjSpecific(); } - private void CreateGUI() + protected override void CreateGUI() { var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), GuiFrame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter); @@ -242,8 +240,8 @@ namespace Barotrauma.Items.Components var item1 = c1.GUIComponent.UserData as FabricationRecipe; var item2 = c2.GUIComponent.UserData as FabricationRecipe; - bool hasSkills1 = DegreeOfSuccess(character, item1.RequiredSkills) >= 0.5f; - bool hasSkills2 = DegreeOfSuccess(character, item2.RequiredSkills) >= 0.5f; + bool hasSkills1 = FabricationDegreeOfSuccess(character, item1.RequiredSkills) >= 0.5f; + bool hasSkills2 = FabricationDegreeOfSuccess(character, item2.RequiredSkills) >= 0.5f; if (hasSkills1 != hasSkills2) { @@ -267,7 +265,7 @@ namespace Barotrauma.Items.Components AutoScaleHorizontal = true, CanBeFocused = false }; - var firstinSufficient = itemList.Content.Children.FirstOrDefault(c => c.UserData is FabricationRecipe fabricableItem && DegreeOfSuccess(character, fabricableItem.RequiredSkills) < 0.5f); + var firstinSufficient = itemList.Content.Children.FirstOrDefault(c => c.UserData is FabricationRecipe fabricableItem && FabricationDegreeOfSuccess(character, fabricableItem.RequiredSkills) < 0.5f); if (firstinSufficient != null) { insufficientSkillsText.RectTransform.RepositionChildInHierarchy(itemList.Content.RectTransform.GetChildIndex(firstinSufficient.RectTransform)); @@ -476,7 +474,7 @@ namespace Barotrauma.Items.Components List inadequateSkills = new List(); if (user != null) { - inadequateSkills = selectedItem.RequiredSkills.FindAll(skill => user.GetSkillLevel(skill.Identifier) < skill.Level); + inadequateSkills = selectedItem.RequiredSkills.FindAll(skill => user.GetSkillLevel(skill.Identifier) < Math.Round(skill.Level * SkillRequirementMultiplier)); } if (selectedItem.RequiredSkills.Any()) @@ -489,13 +487,13 @@ namespace Barotrauma.Items.Components }; foreach (Skill skill in selectedItem.RequiredSkills) { - text += TextManager.Get("SkillName." + skill.Identifier) + " " + TextManager.Get("Lvl").ToLower() + " " + skill.Level; + 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); } - float degreeOfSuccess = user == null ? 0.0f : DegreeOfSuccess(user, selectedItem.RequiredSkills); + float degreeOfSuccess = user == null ? 0.0f : FabricationDegreeOfSuccess(user, selectedItem.RequiredSkills); if (degreeOfSuccess > 0.5f) { degreeOfSuccess = 1.0f; } float requiredTime = user == null ? selectedItem.RequiredTime : GetRequiredTime(selectedItem, user); @@ -621,10 +619,5 @@ namespace Barotrauma.Items.Components StartFabricating(fabricationRecipes[itemIndex], user); } } - - protected override void RemoveComponentSpecific() - { - GameMain.Instance.OnResolutionChanged -= RecreateGUI; - } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index b80dd19d0..047b5375a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -24,7 +24,17 @@ namespace Barotrauma.Items.Components partial void InitProjSpecific(XElement element) { noPowerTip = TextManager.Get("SteeringNoPowerTip"); + CreateGUI(); + } + protected override void OnResolutionChanged() + { + base.OnResolutionChanged(); + CreateHUD(); + } + + protected override void CreateGUI() + { GuiFrame.RectTransform.RelativeOffset = new Vector2(0.05f, 0.0f); GuiFrame.CanBeFocused = true; new GUICustomComponent(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center) { AbsoluteOffset = GUIStyle.ItemFrameOffset }, @@ -53,7 +63,11 @@ namespace Barotrauma.Items.Components hullAirQualityText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), hullInfoContainer.RectTransform), "") { Wrap = true }; hullWaterText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), hullInfoContainer.RectTransform), "") { Wrap = true }; - hullInfoFrame.Children.ForEach(c => { c.CanBeFocused = false; c.Children.ForEach(c2 => c2.CanBeFocused = false); }); + hullInfoFrame.Children.ForEach(c => + { + c.CanBeFocused = false; + c.Children.ForEach(c2 => c2.CanBeFocused = false); + }); } public override void AddToGUIUpdateList() @@ -72,7 +86,7 @@ namespace Barotrauma.Items.Components { submarineContainer.ClearChildren(); - if (item.Submarine == null) return; + if (item.Submarine == null) { return; } item.Submarine.CreateMiniMap(submarineContainer); displayedSubs.Clear(); @@ -125,10 +139,8 @@ namespace Barotrauma.Items.Components GUI.Style.Orange * (float)Math.Abs(Math.Sin(Timing.TotalTime)), Color.Black * 0.8f, font: GUI.SubHeadingFont); return; } - if (!submarineContainer.Children.Any()) { return; } - - foreach (GUIComponent child in submarineContainer.Children.First().Children) + foreach (GUIComponent child in submarineContainer.Children.FirstOrDefault()?.Children) { if (child.UserData is Hull hull) { @@ -177,9 +189,19 @@ namespace Barotrauma.Items.Components HashSet subs = new HashSet(); foreach (Hull hull in Hull.hullList) { - if (hull.Submarine == null) continue; + if (hull.Submarine == null) { continue; } var hullFrame = submarineContainer.Children.FirstOrDefault()?.FindChild(hull); - if (hullFrame == null) continue; + if (hullFrame == null) { continue; } + + hullFrame.Visible = true; + if (!submarineContainer.Rect.Contains(hullFrame.Rect)) + { + if (hull.Submarine.Info.Type != SubmarineType.Player) + { + hullFrame.Visible = false; + continue; + } + } hullDatas.TryGetValue(hull, out HullData hullData); if (hullData == null) @@ -294,7 +316,7 @@ namespace Barotrauma.Items.Components foreach (Submarine sub in subs) { - if (sub.HullVertices == null) { continue; } + if (sub.HullVertices == null || sub.Info.IsOutpost) { continue; } Rectangle worldBorders = sub.GetDockedBorders(); worldBorders.Location += sub.WorldPosition.ToPoint(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs new file mode 100644 index 000000000..7d9ff5175 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs @@ -0,0 +1,42 @@ + +namespace Barotrauma.Items.Components +{ + partial class OutpostTerminal : ItemComponent + { + private SubmarineSelection selectionUI; + + public override bool Select(Character character) + { + if (GameMain.GameSession?.Campaign == null) + { + return false; + } + + if (selectionUI == null) + { + selectionUI = new SubmarineSelection(true, null, GUICanvas.Instance.ItemComponentHolder); + } + + GuiFrame = selectionUI.GuiFrame; + selectionUI.RefreshSubmarineDisplay(true); + IsActive = true; + return base.Select(character); + } + + public override void Update(float deltaTime, Camera cam) + { + if (Character.Controlled?.SelectedConstruction != item) + { + IsActive = false; + return; + } + + base.Update(deltaTime, cam); + + if (selectionUI != null) + { + selectionUI.Update(); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index 5b98c3fea..fa5065625 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -157,16 +157,15 @@ namespace Barotrauma.Items.Components } } CreateGUI(); - GameMain.Instance.OnResolutionChanged += RecreateGUI; } - private void RecreateGUI() + protected override void OnResolutionChanged() { - GuiFrame.ClearChildren(); - CreateGUI(); + base.OnResolutionChanged(); + UpdateGUIElements(); } - private void CreateGUI() + protected override void CreateGUI() { bool isConnectedToSteering = item.GetComponent() != null; Vector2 size = isConnectedToSteering ? controlBoxSize : new Vector2(controlBoxSize.X * 2.0f, controlBoxSize.Y); @@ -667,23 +666,27 @@ namespace Barotrauma.Items.Components signalWarningText.Visible = false; } - if (GameMain.GameSession == null) { return; } + if (GameMain.GameSession == null || Level.Loaded == null) { return; } - if (Level.Loaded == null) { return; } + if (Level.Loaded.StartLocation != null) + { + DrawMarker(spriteBatch, + Level.Loaded.StartLocation.Name, + "outpost", + Level.Loaded.StartLocation.Name, + Level.Loaded.StartPosition, transducerCenter, + displayScale, center, DisplayRadius); + } - DrawMarker(spriteBatch, - GameMain.GameSession.StartLocation.Name, - "outpost", - GameMain.GameSession.StartLocation.Name, - Level.Loaded.StartPosition, transducerCenter, - displayScale, center, DisplayRadius); - - DrawMarker(spriteBatch, - GameMain.GameSession.EndLocation.Name, - "outpost", - GameMain.GameSession.EndLocation.Name, - Level.Loaded.EndPosition, transducerCenter, - displayScale, center, DisplayRadius); + if (Level.Loaded.EndLocation != null && Level.Loaded.Type == LevelData.LevelType.LocationConnection) + { + DrawMarker(spriteBatch, + Level.Loaded.EndLocation.Name, + "outpost", + Level.Loaded.EndLocation.Name, + Level.Loaded.EndPosition, transducerCenter, + displayScale, center, DisplayRadius); + } foreach (AITarget aiTarget in AITarget.List) { @@ -1444,8 +1447,6 @@ namespace Barotrauma.Items.Components sprite.Remove(); } targetIcons.Clear(); - - GameMain.Instance.OnResolutionChanged -= RecreateGUI; } public void ClientWrite(IWriteMessage msg, object[] extraData = null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index f51566b02..f52417a50 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -53,6 +53,8 @@ namespace Barotrauma.Items.Components private bool? swapDestinationOrder; + private GUIMessageBox enterOutpostPrompt; + private bool levelStartSelected; public bool LevelStartSelected { @@ -105,10 +107,9 @@ namespace Barotrauma.Items.Components } } CreateGUI(); - GameMain.Instance.OnResolutionChanged += RecreateGUI; } - private void CreateGUI() + protected override void CreateGUI() { controlContainer = new GUIFrame(new RectTransform(new Vector2(Sonar.controlBoxSize.X, 1 - Sonar.controlBoxSize.Y * 2), GuiFrame.RectTransform, Anchor.CenterLeft), "ItemUI"); var paddedControlContainer = new GUIFrame(new RectTransform(controlContainer.Rect.Size - GUIStyle.ItemFrameMargin, controlContainer.RectTransform, Anchor.Center) @@ -220,11 +221,12 @@ namespace Barotrauma.Items.Components }; levelEndTickBox = new GUITickBox(new RectTransform(new Vector2(1, 0.333f), paddedAutoPilotControls.RectTransform, Anchor.BottomCenter), - GameMain.GameSession?.EndLocation == null ? "" : ToolBox.LimitString(GameMain.GameSession.EndLocation.Name, textLimit), + (GameMain.GameSession?.EndLocation == null || Level.IsLoadedOutpost) ? "" : ToolBox.LimitString(GameMain.GameSession.EndLocation.Name, textLimit), font: GUI.SmallFont, style: "GUIRadioButton") { Enabled = autoPilot, Selected = levelEndSelected, + Visible = GameMain.GameSession?.EndLocation != null, OnSelected = tickBox => { if (levelEndSelected != tickBox.Selected) @@ -321,7 +323,7 @@ namespace Barotrauma.Items.Components }; break; } - new GUITextBlock(new RectTransform(Vector2.One, left.RectTransform), leftText, font: GUI.SubHeadingFont, wrap: true, textAlignment: Alignment.CenterRight); + 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 }; 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) @@ -347,18 +349,47 @@ namespace Barotrauma.Items.Components { OnClicked = (btn, userdata) => { - if (GameMain.Client == null) + + if (GameMain.GameSession?.Campaign != null) { - item.SendSignal(0, "1", "toggle_docking", sender: null); - } - else - { - dockingNetworkMessagePending = true; - item.CreateClientEvent(this); + if (Level.IsLoadedOutpost && + DockingSources.Any(d => d.Docked && (d.DockingTarget?.Item.Submarine?.Info?.IsOutpost ?? false))) + { + GameMain.GameSession.Campaign.CampaignUI.SelectTab(CampaignMode.InteractionType.Map); + GameMain.GameSession.Campaign.ShowCampaignUI = true; + return false; + } + else if (!Level.IsLoadedOutpost && DockingModeEnabled && ActiveDockingSource != null && + !ActiveDockingSource.Docked && (DockingTarget?.Item?.Submarine?.Info.IsOutpost ?? false)) + { + enterOutpostPrompt = new GUIMessageBox("", TextManager.GetWithVariable("campaignenteroutpostprompt", "[locationname]", DockingTarget.Item.Submarine.Info.Name), new string[] { TextManager.Get("yes"), TextManager.Get("no") }); + enterOutpostPrompt.Buttons[0].OnClicked += (btn, userdata) => + { + SendDockingSignal(); + enterOutpostPrompt.Close(); + return true; + }; + enterOutpostPrompt.Buttons[1].OnClicked += enterOutpostPrompt.Close; + return false; + } } + SendDockingSignal(); + return true; } }; + void SendDockingSignal() + { + if (GameMain.Client == null) + { + item.SendSignal(0, "1", "toggle_docking", sender: null); + } + else + { + dockingNetworkMessagePending = true; + item.CreateClientEvent(this); + } + } dockingButton.Font = GUI.SubHeadingFont; dockingButton.TextBlock.RectTransform.MaxSize = new Point((int)(dockingButton.Rect.Width * 0.7f), int.MaxValue); dockingButton.TextBlock.AutoScaleHorizontal = true; @@ -413,10 +444,9 @@ namespace Barotrauma.Items.Components GameMain.GameSession?.EndLocation == null ? "End" : GameMain.GameSession.EndLocation.Name); } - private void RecreateGUI() + protected override void OnResolutionChanged() { - GuiFrame.ClearChildren(); - CreateGUI(); + base.OnResolutionChanged(); UpdateGUIElements(); } @@ -600,6 +630,10 @@ namespace Barotrauma.Items.Components dockingContainer.Visible = DockingModeEnabled; statusContainer.Visible = !DockingModeEnabled; + if (!DockingModeEnabled) + { + enterOutpostPrompt?.Close(); + } if (DockingModeEnabled && ActiveDockingSource != null) { @@ -613,6 +647,10 @@ namespace Barotrauma.Items.Components dockingButton.Pulsate(Vector2.One, Vector2.One * 1.2f, dockingButton.FlashTimer); } } + else + { + enterOutpostPrompt?.Close(); + } } else if (DockingSources.Any(d => d.Docked)) { @@ -663,7 +701,8 @@ namespace Barotrauma.Items.Components if (Vector2.DistanceSquared(PlayerInput.MousePosition, steerArea.Rect.Center.ToVector2()) < steerRadius * steerRadius) { - if (PlayerInput.PrimaryMouseButtonHeld() && !CrewManager.IsCommandInterfaceOpen && !GameSession.IsTabMenuOpen) + if (PlayerInput.PrimaryMouseButtonHeld() && !CrewManager.IsCommandInterfaceOpen && !GameSession.IsTabMenuOpen && + (!GameMain.GameSession?.Campaign?.ShowCampaignUI ?? true) && !GUIMessageBox.MessageBoxes.Any()) { Vector2 inputPos = PlayerInput.MousePosition - steerArea.Rect.Center.ToVector2(); inputPos.Y = -inputPos.Y; @@ -800,8 +839,7 @@ namespace Barotrauma.Items.Components maintainPosIndicator?.Remove(); maintainPosOriginIndicator?.Remove(); steeringIndicator?.Remove(); - - GameMain.Instance.OnResolutionChanged -= RecreateGUI; + enterOutpostPrompt?.Close(); } public void ClientWrite(IWriteMessage msg, object[] extraData = null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs index a72a5ed3e..2fd5955c2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs @@ -111,27 +111,28 @@ namespace Barotrauma.Items.Components if (item.FlippedX && item.Prefab.CanSpriteFlipX) { indicatorPos.X = -indicatorPos.X - indicatorSize.X * item.Scale; } if (item.FlippedY && item.Prefab.CanSpriteFlipY) { indicatorPos.Y = -indicatorPos.Y - indicatorSize.Y * item.Scale; } - if (charge > 0) + if (charge > 0 && capacity > 0) { - Color indicatorColor = ToolBox.GradientLerp(charge / capacity, Color.Red, Color.Orange, Color.Green); + float chargeRatio = MathHelper.Clamp(charge / capacity, 0.0f, 1.0f); + Color indicatorColor = ToolBox.GradientLerp(chargeRatio, Color.Red, Color.Orange, Color.Green); if (!isHorizontal) { GUI.DrawRectangle(spriteBatch, - new Vector2(item.DrawPosition.X, -item.DrawPosition.Y + ((indicatorSize.Y * item.Scale) * (1.0f - charge / capacity))) + indicatorPos, - new Vector2(indicatorSize.X * item.Scale, (indicatorSize.Y * item.Scale) * (charge / capacity)), indicatorColor, true, + new Vector2(item.DrawPosition.X, -item.DrawPosition.Y + ((indicatorSize.Y * item.Scale) * (1.0f - chargeRatio))) + indicatorPos, + new Vector2(indicatorSize.X * item.Scale, (indicatorSize.Y * item.Scale) * chargeRatio), indicatorColor, true, depth: item.SpriteDepth - 0.00001f); } else { GUI.DrawRectangle(spriteBatch, new Vector2(item.DrawPosition.X, -item.DrawPosition.Y) + indicatorPos, - new Vector2((indicatorSize.X * item.Scale) * (charge / capacity), indicatorSize.Y * item.Scale), indicatorColor, true, + new Vector2((indicatorSize.X * item.Scale) * chargeRatio, indicatorSize.Y * item.Scale), indicatorColor, true, depth: item.SpriteDepth - 0.00001f); } } GUI.DrawRectangle(spriteBatch, new Vector2(item.DrawPosition.X, -item.DrawPosition.Y) + indicatorPos, - indicatorSize * item.Scale, Color.Black, depth: item.SpriteDepth - 0.00001f); + indicatorSize * item.Scale, Color.Black, depth: item.SpriteDepth - 0.000015f); } public void ClientWrite(IWriteMessage msg, object[] extraData) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs index d83a4a17a..634e10fcd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs @@ -83,7 +83,7 @@ namespace Barotrauma.Items.Components var progressBar = user.UpdateHUDProgressBar( targetStructure.ID * 1000 + sectionIndex, //unique "identifier" for each wall section progressBarPos, - 1.0f - targetStructure.SectionDamage(sectionIndex) / targetStructure.Health, + MathUtils.InverseLerp(targetStructure.Prefab.MinHealth, targetStructure.Health, targetStructure.Health - targetStructure.SectionDamage(sectionIndex)), GUI.Style.Red, GUI.Style.Green); 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 e7eb0f15d..0099e8bfb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -1,4 +1,5 @@ -using Barotrauma.Networking; +using System; +using Barotrauma.Networking; using Barotrauma.Particles; using Barotrauma.Sounds; using Microsoft.Xna.Framework; @@ -49,6 +50,42 @@ namespace Barotrauma.Items.Components } partial void InitProjSpecific(XElement element) + { + CreateGUI(); + foreach (XElement subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "emitter": + case "particleemitter": + particleEmitters.Add(new ParticleEmitter(subElement)); + float minCondition = subElement.GetAttributeFloat("mincondition", 0.0f); + float maxCondition = subElement.GetAttributeFloat("maxcondition", 100.0f); + + if (maxCondition < minCondition) + { + DebugConsole.ThrowError("Invalid damage particle configuration in the Repairable component of " + item.Name + ". MaxCondition needs to be larger than MinCondition."); + float temp = maxCondition; + maxCondition = minCondition; + minCondition = temp; + } + particleEmitterConditionRanges.Add(new Vector2(minCondition, maxCondition)); + + break; + } + } + } + + private void RecreateGUI() + { + if (GuiFrame != null) + { + GuiFrame.ClearChildren(); + CreateGUI(); + } + } + + private void CreateGUI() { var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.75f), GuiFrame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) { @@ -56,7 +93,7 @@ namespace Barotrauma.Items.Components RelativeSpacing = 0.05f, CanBeFocused = true }; - + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), paddedFrame.RectTransform), header, textAlignment: Alignment.TopCenter, font: GUI.LargeFont); @@ -68,7 +105,7 @@ namespace Barotrauma.Items.Components 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) requiredSkills[i].Level).ToString()), + " - " + TextManager.AddPunctuation(':', TextManager.Get("SkillName." + requiredSkills[i].Identifier), ((int) Math.Round(requiredSkills[i].Level * SkillRequirementMultiplier)).ToString()), font: GUI.SmallFont) { UserData = requiredSkills[i] @@ -111,43 +148,27 @@ namespace Barotrauma.Items.Components return true; } }; - - foreach (XElement subElement in element.Elements()) - { - switch (subElement.Name.ToString().ToLowerInvariant()) - { - case "emitter": - case "particleemitter": - particleEmitters.Add(new ParticleEmitter(subElement)); - float minCondition = subElement.GetAttributeFloat("mincondition", 0.0f); - float maxCondition = subElement.GetAttributeFloat("maxcondition", 100.0f); - - if (maxCondition < minCondition) - { - DebugConsole.ThrowError("Invalid damage particle configuration in the Repairable component of " + item.Name + ". MaxCondition needs to be larger than MinCondition."); - float temp = maxCondition; - maxCondition = minCondition; - minCondition = temp; - } - particleEmitterConditionRanges.Add(new Vector2(minCondition, maxCondition)); - - break; - } - } } partial void UpdateProjSpecific(float deltaTime) { - if (Character.Controlled == null || (Character.Controlled.CharacterHealth.GetAffliction("psychosis")?.Strength ?? 0.0f) <= 0.0f) + if (FakeBrokenTimer > 0.0f) { - FakeBrokenTimer = 0.0f; + item.FakeBroken = true; + if (Character.Controlled == null || (Character.Controlled.CharacterHealth.GetAffliction("psychosis")?.Strength ?? 0.0f) <= 0.0f) + { + FakeBrokenTimer = 0.0f; + } + else + { + FakeBrokenTimer -= deltaTime; + } } else { - FakeBrokenTimer -= deltaTime; + item.FakeBroken = false; } - item.FakeBroken = FakeBrokenTimer > 0.0f; if (!GameMain.IsMultiplayer) { @@ -211,7 +232,7 @@ namespace Barotrauma.Items.Components if (!(c.UserData is Skill skill)) continue; GUITextBlock textBlock = (GUITextBlock)c; - if (character.GetSkillLevel(skill.Identifier) < skill.Level) + if (character.GetSkillLevel(skill.Identifier) < (skill.Level * SkillRequirementMultiplier)) { textBlock.TextColor = GUI.Style.Red; } @@ -247,6 +268,7 @@ namespace Barotrauma.Items.Components protected override void RemoveComponentSpecific() { + base.RemoveComponentSpecific(); repairSoundChannel?.FadeOutAndDispose(); repairSoundChannel = null; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs index 4184e554e..a6378276e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs @@ -289,7 +289,7 @@ namespace Barotrauma.Items.Components flashColor * (float)Math.Sin(FlashTimer % flashCycleDuration / flashCycleDuration * MathHelper.Pi * 0.8f), scale: connectorSpriteScale); } - if (Wires.Any(w => w != null && w != DraggingConnected)) + if (Wires.Any(w => w != null && w != DraggingConnected && !w.Hidden)) { int screwIndex = (int)Math.Floor(position.Y / 30.0f) % screwSprites.Count; screwSprites[screwIndex].Draw(spriteBatch, position, scale: connectorSpriteScale); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs index be7b5a42f..e872fc842 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs @@ -18,17 +18,9 @@ namespace Barotrauma.Items.Components partial void InitProjSpecific(XElement element) { CreateGUI(); - GameMain.Instance.OnResolutionChanged += RecreateGUI; } - private void RecreateGUI() - { - GuiFrame.ClearChildren(); - CreateGUI(); - UpdateLabelsProjSpecific(); - } - - private void CreateGUI() + protected override void CreateGUI() { uiElements.Clear(); var visibleElements = customInterfaceElementList.Where(ciElement => !string.IsNullOrEmpty(ciElement.Label)); @@ -309,10 +301,5 @@ namespace Barotrauma.Items.Components } } } - - protected override void RemoveComponentSpecific() - { - GameMain.Instance.OnResolutionChanged -= RecreateGUI; - } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs index fc67bfeac..b6bb282bb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs @@ -187,7 +187,7 @@ namespace Barotrauma.Items.Components } if (IsActive && item.ParentInventory?.Owner is Character user && user == Character.Controlled)// && Vector2.Distance(newNodePos, nodes[nodes.Count - 1]) > nodeDistance) { - if (user.CanInteract) + if (user.CanInteract && currLength < MaxLength) { Vector2 gridPos = Character.Controlled.Position; Vector2 roundedGridPos = new Vector2( diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs index a49647775..02b174c32 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs @@ -135,7 +135,7 @@ namespace Barotrauma.Items.Components Entity targetEntity = Entity.FindEntityByID(dockingTargetID); if (targetEntity == null || !(targetEntity is Item)) { - DebugConsole.ThrowError("Invalid docking port network event (can't dock to " + targetEntity?.ToString() ?? "null" + ")"); + DebugConsole.ThrowError("Invalid docking port network event (can't dock to " + (targetEntity?.ToString() ?? "null") + ")"); return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 11b3c45b6..2a6e27003 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -260,7 +260,11 @@ namespace Barotrauma item.Name : item.Name + '\n' + description; } - + if (item.SpawnedInOutpost) + { + string colorStr = XMLExtensions.ColorToString(GUI.Style.Red); + toolTip = $"‖color:{colorStr}‖{toolTip}‖color:end‖"; + } return toolTip; } } @@ -1127,6 +1131,8 @@ namespace Barotrauma public static void DrawFront(SpriteBatch spriteBatch) { if (GUI.PauseMenuOpen || GUI.SettingsMenuOpen) { return; } + if (GameMain.GameSession?.Campaign != null && + (GameMain.GameSession.Campaign.ShowCampaignUI || GameMain.GameSession.Campaign.ForceMapUI)) { return; } subInventorySlotsToDraw.Clear(); subInventorySlotsToDraw.AddRange(highlightedSubInventorySlots); @@ -1377,6 +1383,17 @@ namespace Barotrauma sprite.Draw(spriteBatch, itemPos + Vector2.One * 2, Color.Black * 0.6f, rotate: rotation, scale: scale); } sprite.Draw(spriteBatch, itemPos, spriteColor, rotation, scale); + + if (item.SpawnedInOutpost && CharacterInventory.LimbSlotIcons.ContainsKey(InvSlotType.LeftHand)) + { + var stealIcon = CharacterInventory.LimbSlotIcons[InvSlotType.LeftHand]; + Vector2 iconSize = new Vector2(25 * GUI.Scale); + stealIcon.Draw( + spriteBatch, + new Vector2(rect.X + iconSize.X * 0.2f, rect.Bottom - iconSize.Y * 1.2f), + color: GUI.Style.Red, + scale: iconSize.X / stealIcon.size.X); + } } if (inventory != null && diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 4702c1767..83694b914 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -246,7 +246,7 @@ namespace Barotrauma float fadeInBrokenSpriteAlpha = 0.0f; float displayCondition = FakeBroken ? 0.0f : condition; Vector2 drawOffset = Vector2.Zero; - if (displayCondition < Prefab.Health) + if (displayCondition < MaxCondition) { for (int i = 0; i < Prefab.BrokenSprites.Count; i++) { @@ -299,22 +299,23 @@ namespace Barotrauma if (!spriteAnimState[decorativeSprite].IsActive) { continue; } Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState) * Scale; decorativeSprite.Sprite.DrawTiled(spriteBatch, - new Vector2(DrawPosition.X + offset.X - rect.Width / 2, -(DrawPosition.Y + offset.Y + rect.Height / 2)), - new Vector2(rect.Width, rect.Height), color: color, + new Vector2(DrawPosition.X + offset.X - rect.Width / 2, -(DrawPosition.Y + offset.Y + rect.Height / 2)), + size, color: color, + textureScale: Vector2.One * Scale, depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth), 0.999f)); } } else { - activeSprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, color, SpriteRotation, Scale, activeSprite.effects, depth); - fadeInBrokenSprite?.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + fadeInBrokenSprite.Offset.ToVector2() * Scale, color * fadeInBrokenSpriteAlpha, SpriteRotation, Scale, activeSprite.effects, depth - 0.000001f); + activeSprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, color, SpriteRotation + rotation, Scale, activeSprite.effects, depth); + fadeInBrokenSprite?.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + fadeInBrokenSprite.Offset.ToVector2() * Scale, color * fadeInBrokenSpriteAlpha, SpriteRotation + rotation, Scale, activeSprite.effects, depth - 0.000001f); foreach (var decorativeSprite in Prefab.DecorativeSprites) { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } - float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState); + float rot = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState); Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState) * Scale; decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color, - SpriteRotation + rotation, decorativeSprite.Scale * Scale, activeSprite.effects, + SpriteRotation + rotation + rot, decorativeSprite.Scale * Scale, activeSprite.effects, depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth), 0.999f)); } } @@ -328,7 +329,7 @@ namespace Barotrauma if (holdable.Picker.SelectedItems[0] == this) { Limb holdLimb = holdable.Picker.AnimController.GetLimb(LimbType.RightHand); - if (holdLimb != null) + if (holdLimb?.ActiveSprite != null) { depth = holdLimb.ActiveSprite.Depth + holdable.Picker.AnimController.GetDepthOffset() + depthStep * 2; foreach (WearableSprite wearableSprite in holdLimb.WearingItems) @@ -340,7 +341,7 @@ namespace Barotrauma else if (holdable.Picker.SelectedItems[1] == this) { Limb holdLimb = holdable.Picker.AnimController.GetLimb(LimbType.LeftHand); - if (holdLimb != null) + if (holdLimb?.ActiveSprite != null) { depth = holdLimb.ActiveSprite.Depth + holdable.Picker.AnimController.GetDepthOffset() - depthStep * 2; foreach (WearableSprite wearableSprite in holdLimb.WearingItems) @@ -368,6 +369,23 @@ namespace Barotrauma depth: depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth)); } } + + foreach (var upgrade in Upgrades) + { + var upgradeSprites = GetUpgradeSprites(upgrade); + + foreach (var decorativeSprite in upgradeSprites) + { + if (!spriteAnimState[decorativeSprite].IsActive) { continue; } + float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState); + var (xOff, yOff) = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState) * Scale; + + decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + xOff, -(DrawPosition.Y + yOff)), color, + rotation, decorativeSprite.Scale * Scale, activeSprite.effects, + depth: depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth)); + } + + } activeSprite.effects = oldEffects; if (fadeInBrokenSprite != null && fadeInBrokenSprite.Sprite != activeSprite) @@ -437,6 +455,21 @@ namespace Barotrauma } } + private void DrawDecorativeSprite(SpriteBatch spriteBatch, DecorativeSprite decorativeSprite, Color color, float depth) + { + if (!spriteAnimState[decorativeSprite].IsActive) { return; } + float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState); + Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState) * Scale; + + var ca = (float)Math.Cos(-body.Rotation); + var sa = (float)Math.Sin(-body.Rotation); + Vector2 transformedOffset = new Vector2(ca * offset.X + sa * offset.Y, -sa * offset.X + ca * offset.Y); + + decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + transformedOffset.X, -(DrawPosition.Y + transformedOffset.Y)), color, + -body.Rotation + rotation, decorativeSprite.Scale * Scale, activeSprite.effects, + depth: depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth)); + } + partial void OnCollisionProjSpecific(float impact) { if (impact > 1.0f && @@ -451,6 +484,22 @@ namespace Barotrauma public void UpdateSpriteStates(float deltaTime) { DecorativeSprite.UpdateSpriteStates(Prefab.DecorativeSpriteGroups, spriteAnimState, ID, deltaTime, ConditionalMatches); + + foreach (var upgrade in Upgrades) + { + var upgradeSprites = GetUpgradeSprites(upgrade); + + foreach (var decorativeSprite in upgradeSprites) + { + var spriteState = spriteAnimState[decorativeSprite]; + spriteState.IsActive = true; + foreach (var _ in decorativeSprite.IsActiveConditionals.Where(conditional => !ConditionalMatches(conditional))) + { + spriteState.IsActive = false; + break; + } + } + } } public override void UpdateEditing(Camera cam) @@ -541,12 +590,14 @@ namespace Barotrauma linkText.TextColor = GUI.Style.Orange; itemsText.TextColor = GUI.Style.Orange; } + var buttonContainer = new GUILayoutGroup(new RectTransform(new Point(listBox.Content.Rect.Width, heightScaled)), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.02f, CanBeFocused = true }; + new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityX"), style: "GUIButtonSmall") { ToolTip = TextManager.Get("MirrorEntityXToolTip"), @@ -588,6 +639,22 @@ namespace Barotrauma buttonContainer.RectTransform.IsFixedSize = true; itemEditor.AddCustomContent(buttonContainer, itemEditor.ContentCount); GUITextBlock.AutoScaleAndNormalize(buttonContainer.Children.Select(b => ((GUIButton)b).TextBlock)); + + 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, + Selected = RemoveIfLinkedOutpostDoorInUse, + ToolTip = TextManager.Get("sp.structure.removeiflinkedoutpostdoorinuse.description"), + OnSelected = (tickBox) => + { + RemoveIfLinkedOutpostDoorInUse = tickBox.Selected; + return true; + } + }; + itemEditor.AddCustomContent(tickBox, 1); + } } foreach (ItemComponent ic in components) @@ -667,6 +734,39 @@ namespace Barotrauma return editingHUD; } + private List GetUpgradeSprites(Upgrade upgrade) + { + var upgradeSprites = upgrade.Prefab.DecorativeSprites; + + if (Prefab.UpgradeOverrideSprites.ContainsKey(upgrade.Prefab.Identifier)) + { + upgradeSprites = Prefab.UpgradeOverrideSprites[upgrade.Prefab.Identifier]; + } + + return upgradeSprites; + } + + public override bool AddUpgrade(Upgrade upgrade, bool createNetworkEvent = false) + { + if (upgrade.Prefab.IsWallUpgrade) { return false; } + bool result = base.AddUpgrade(upgrade, createNetworkEvent); + if (result && !upgrade.Disposed) + { + List upgradeSprites = GetUpgradeSprites(upgrade); + + if (upgradeSprites.Any()) + { + foreach (DecorativeSprite decorativeSprite in upgradeSprites) + { + decorativeSprite.Sprite.EnsureLazyLoaded(); + spriteAnimState.Add(decorativeSprite, new DecorativeSprite.State()); + } + UpdateSpriteStates(0.0f); + } + } + return result; + } + 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)); @@ -756,6 +856,10 @@ namespace Barotrauma private readonly List debugInitialHudPositions = new List(); + private readonly List prevActiveHUDs = new List(); + private readonly List activeComponents = new List(); + private readonly List maxPriorityHUDs = new List(); + public void UpdateHUD(Camera cam, Character character, float deltaTime) { bool editingHUDCreated = false; @@ -774,8 +878,11 @@ namespace Barotrauma editingHUDRefreshTimer -= deltaTime; } - List prevActiveHUDs = new List(activeHUDs); - List activeComponents = new List(components); + prevActiveHUDs.Clear(); + prevActiveHUDs.AddRange(activeHUDs); + activeComponents.Clear(); + activeComponents.AddRange(components); + foreach (MapEntity entity in linkedTo) { if (prefab.IsLinkAllowed(entity.prefab) && entity is Item i) @@ -788,12 +895,11 @@ namespace Barotrauma activeHUDs.Clear(); //the HUD of the component with the highest priority will be drawn //if all components have a priority of 0, all of them are drawn - List maxPriorityHUDs = new List(); + maxPriorityHUDs.Clear(); + bool DrawHud(ItemComponent ic) => ic.ShouldDrawHUD(character) && (ic.CanBeSelected && ic.HasRequiredItems(character, addMessage: false) || (character.HasEquippedItem(this) && ic.DrawHudWhenEquipped)); foreach (ItemComponent ic in activeComponents) { - if (ic.HudPriority > 0 && ic.ShouldDrawHUD(character) && - (ic.CanBeSelected || (character.HasEquippedItem(this) && ic.DrawHudWhenEquipped)) && - (maxPriorityHUDs.Count == 0 || ic.HudPriority >= maxPriorityHUDs[0].HudPriority)) + if (ic.HudPriority > 0 && DrawHud(ic) && (maxPriorityHUDs.Count == 0 || ic.HudPriority >= maxPriorityHUDs[0].HudPriority)) { if (maxPriorityHUDs.Count > 0 && ic.HudPriority > maxPriorityHUDs[0].HudPriority) { maxPriorityHUDs.Clear(); } maxPriorityHUDs.Add(ic); @@ -808,8 +914,7 @@ namespace Barotrauma { foreach (ItemComponent ic in activeComponents) { - if (ic.ShouldDrawHUD(character) && - (ic.CanBeSelected || (character.HasEquippedItem(this) && ic.DrawHudWhenEquipped))) + if (DrawHud(ic)) { activeHUDs.Add(ic); } @@ -914,11 +1019,11 @@ namespace Barotrauma color = Color.Cyan; } } - texts.Add(new ColoredText(ic.DisplayMsg, color, false)); + texts.Add(new ColoredText(ic.DisplayMsg, color, false, false)); } if ((PlayerInput.KeyDown(Keys.LeftShift) || PlayerInput.KeyDown(Keys.RightShift)) && CrewManager.DoesItemHaveContextualOrders(this)) { - texts.Add(new ColoredText(TextManager.ParseInputTypes(TextManager.Get("itemmsgcontextualorders")), Color.Cyan, false)); + texts.Add(new ColoredText(TextManager.ParseInputTypes(TextManager.Get("itemmsgcontextualorders")), Color.Cyan, false, false)); } return texts; } @@ -1048,6 +1153,29 @@ namespace Barotrauma ReadPropertyChange(msg, false); editingHUDRefreshPending = true; break; + case NetEntityEvent.Type.Upgrade: + { + string identifier = msg.ReadString(); + byte level = msg.ReadByte(); + if (UpgradePrefab.Find(identifier) is { } upgradePrefab) + { + Upgrade upgrade = new Upgrade(this, upgradePrefab, level); + + byte targetCount = msg.ReadByte(); + for (int i = 0; i < targetCount; i++) + { + byte propertyCount = msg.ReadByte(); + for (int j = 0; j < propertyCount; j++) + { + float value = msg.ReadSingle(); + upgrade.TargetComponents.ElementAt(i).Value[j].SetOriginalValue(value); + } + } + + AddUpgrade(upgrade, false); + } + break; + } case NetEntityEvent.Type.Invalid: break; } @@ -1220,7 +1348,7 @@ namespace Barotrauma ushort itemId = msg.ReadUInt16(); ushort inventoryId = msg.ReadUInt16(); - DebugConsole.Log("Received entity spawn message for item " + itemName + "."); + DebugConsole.Log($"Received entity spawn message for item \"{itemName}\" (identifier: {itemIdentifier}, id: {itemId})"); Vector2 pos = Vector2.Zero; Submarine sub = null; @@ -1243,10 +1371,10 @@ namespace Barotrauma } } - byte bodyType = msg.ReadByte(); - - byte teamID = msg.ReadByte(); - bool tagsChanged = msg.ReadBoolean(); + byte bodyType = msg.ReadByte(); + bool spawnedInOutpost = msg.ReadBoolean(); + byte teamID = msg.ReadByte(); + bool tagsChanged = msg.ReadBoolean(); string tags = ""; if (tagsChanged) { @@ -1289,6 +1417,7 @@ namespace Barotrauma GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); DebugConsole.ThrowError(errorMsg); + inventory = parentItem.GetComponent()?.Inventory; } else if (parentItem.components[itemContainerIndex] is ItemContainer container) { @@ -1307,7 +1436,8 @@ namespace Barotrauma var item = new Item(itemPrefab, pos, sub) { - ID = itemId + ID = itemId, + SpawnedInOutpost = spawnedInOutpost }; if (item.body != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs index ec6c72cff..f45406199 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs @@ -105,7 +105,7 @@ namespace Barotrauma } else { - Vector2 placeSize = size; + Vector2 placeSize = size * Scale; if (placePosition == Vector2.Zero) { @@ -161,7 +161,14 @@ namespace Barotrauma } else { - sprite?.DrawTiled(spriteBatch, new Vector2(position.X, -position.Y), size, color: SpriteColor); + 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); } } @@ -173,7 +180,15 @@ namespace Barotrauma } else { - if (sprite != null) sprite.DrawTiled(spriteBatch, new Vector2(placeRect.X, -placeRect.Y), placeRect.Size.ToVector2(), null, SpriteColor * 0.8f); + Vector2 position = Submarine.MouseToWorldGrid(Screen.Selected.Cam, Submarine.MainSub); + 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); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs index c9101a9fc..6eeda5f1f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs @@ -83,7 +83,7 @@ namespace Barotrauma { Vector2 dir = IsHorizontal ? new Vector2(Math.Sign(linkedTo[i].Rect.Center.X - rect.Center.X), 0.0f) - : new Vector2(0.0f, Math.Sign((linkedTo[i].Rect.Y - linkedTo[i].Rect.Height / 2.0f) - (rect.Y - rect.Height / 2.0f))); + : new Vector2(0.0f, Math.Sign((rect.Y - rect.Height / 2.0f) - (linkedTo[i].Rect.Y - linkedTo[i].Rect.Height / 2.0f))); Vector2 arrowPos = new Vector2(WorldRect.Center.X, -(WorldRect.Y - WorldRect.Height / 2)); arrowPos += new Vector2(dir.X * (WorldRect.Width / 2), dir.Y * (WorldRect.Height / 2)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index 69debe79f..bc9545749 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -201,7 +201,7 @@ namespace Barotrauma for (int i = 1; i < waveY.Length - 1; i++) { float maxDelta = Math.Max(Math.Abs(rightDelta[i]), Math.Abs(leftDelta[i])); - if (maxDelta > Rand.Range(1.0f, 10.0f)) + if (maxDelta > 1.0f && maxDelta > Rand.Range(1.0f, 10.0f)) { var particlePos = new Vector2(rect.X + WaveWidth * i, surface + waveY[i]); if (Submarine != null) particlePos += Submarine.Position; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs index a59df6509..506f1387d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs @@ -22,7 +22,7 @@ namespace Barotrauma HashSet uniqueTextures = new HashSet(); HashSet uniqueSprites = new HashSet(); - var allLevelObjects = levelObjectManager.GetAllObjects(); + var allLevelObjects = LevelObjectManager.GetAllObjects(); foreach (var levelObj in allLevelObjects) { foreach (Sprite sprite in levelObj.Prefab.Sprites) @@ -56,7 +56,7 @@ namespace Barotrauma if (GameMain.DebugDraw && Screen.Selected.Cam.Zoom > 0.1f) { - foreach (InterestingPosition pos in positionsOfInterest) + foreach (InterestingPosition pos in PositionsOfInterest) { Color color = Color.Yellow; if (pos.PositionType == PositionType.Cave) @@ -71,7 +71,7 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, new Vector2(pos.Position.X - 15.0f, -pos.Position.Y - 15.0f), new Vector2(30.0f, 30.0f), color, true); } - foreach (RuinGeneration.Ruin ruin in ruins) + foreach (RuinGeneration.Ruin ruin in Ruins) { Rectangle ruinArea = ruin.Area; ruinArea.Y = -ruinArea.Y - ruinArea.Height; @@ -113,7 +113,7 @@ namespace Barotrauma public void DrawBack(GraphicsDevice graphics, SpriteBatch spriteBatch, Camera cam) { float brightness = MathHelper.Clamp(1.1f + (cam.Position.Y - Size.Y) / 100000.0f, 0.1f, 1.0f); - var lightColorHLS = generationParams.AmbientLightColor.RgbToHLS(); + var lightColorHLS = GenerationParams.AmbientLightColor.RgbToHLS(); lightColorHLS.Y *= brightness; GameMain.LightManager.AmbientLight = ToolBox.HLSToRGB(lightColorHLS); @@ -121,12 +121,12 @@ namespace Barotrauma graphics.Clear(BackgroundColor); if (renderer == null) return; - renderer.DrawBackground(spriteBatch, cam, levelObjectManager, backgroundCreatureManager); + renderer.DrawBackground(spriteBatch, cam, LevelObjectManager, backgroundCreatureManager); } public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { - foreach (LevelWall levelWall in extraWalls) + foreach (LevelWall levelWall in ExtraWalls) { if (levelWall.Body.BodyType == BodyType.Static) continue; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs index a792836fb..9b93770ed 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -9,8 +9,12 @@ namespace Barotrauma { partial class LevelObjectManager { - private List visibleObjectsBack = new List(); - private List visibleObjectsFront = new List(); + private readonly List visibleObjectsBack = new List(); + private readonly List visibleObjectsFront = new List(); + + //Maximum number of visible objects drawn at once. Should be large enough to not have an effect during normal gameplay, + //but small enough to prevent wrecking performance when zooming out very far + const int MaxVisibleObjects = 500; private Rectangle currentGridIndices; @@ -43,7 +47,7 @@ namespace Barotrauma { for (int y = currentIndices.Y; y <= currentIndices.Height; y++) { - if (objectGrid[x, y] == null) continue; + if (objectGrid[x, y] == null) { continue; } foreach (LevelObject obj in objectGrid[x, y]) { var objectList = obj.Position.Z >= 0 ? visibleObjectsBack : visibleObjectsFront; @@ -69,6 +73,7 @@ namespace Barotrauma if (drawOrderIndex >= 0) { objectList.Insert(drawOrderIndex, obj); + if (objectList.Count >= MaxVisibleObjects) { break; } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs index 55a24802c..582022214 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs @@ -209,7 +209,7 @@ namespace Barotrauma level.GenerationParams.WaterParticles.DrawTiled( spriteBatch, origin + offsetS, new Vector2(cam.WorldView.Width - offsetS.X, cam.WorldView.Height - offsetS.Y), - rect: srcRect, color: Color.White * alpha, textureScale: new Vector2(texScale)); + color: Color.White * alpha, textureScale: new Vector2(texScale)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs index 11d7d1736..e4e810725 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs @@ -170,10 +170,12 @@ namespace Barotrauma.Lights isHorizontal = BoundingBox.Width > BoundingBox.Height; if (ParentEntity is Structure structure) { + System.Diagnostics.Debug.Assert(!structure.Removed); isHorizontal = structure.IsHorizontal; } else if (ParentEntity is Item item) { + System.Diagnostics.Debug.Assert(!item.Removed); var door = item.GetComponent(); if (door != null) { isHorizontal = door.IsHorizontal; } } @@ -444,7 +446,7 @@ namespace Barotrauma.Lights CalculateDimensions(); - if (ParentEntity == null) return; + if (ParentEntity == null) { return; } var chList = HullLists.Find(h => h.Submarine == ParentEntity.Submarine); if (chList != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index dd25fa123..46c22c1ae 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -55,7 +55,9 @@ namespace Barotrauma.Lights public bool ObstructVision; private readonly Texture2D visionCircle; - + + private Vector2 losOffset; + public IEnumerable Lights { get { return lights; } @@ -70,7 +72,7 @@ namespace Barotrauma.Lights visionCircle = Sprite.LoadTexture("Content/Lights/visioncircle.png"); highlightRaster = Sprite.LoadTexture("Content/UI/HighlightRaster.png"); - GameMain.Instance.OnResolutionChanged += () => + GameMain.Instance.ResolutionChanged += () => { CreateRenderTargets(graphics); }; @@ -279,7 +281,7 @@ namespace Barotrauma.Lights spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, effect: SolidColorEffect, transformMatrix: spriteBatchTransform); foreach (Character character in Character.CharacterList) { - if (character.CurrentHull == null || !character.Enabled) { continue; } + if (character.CurrentHull == null || !character.Enabled || !character.IsVisible) { continue; } if (Character.Controlled?.FocusedCharacter == character) { continue; } Color lightColor = character.CurrentHull.AmbientLight == Color.TransparentBlack ? Color.Black : @@ -297,7 +299,7 @@ namespace Barotrauma.Lights spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, transformMatrix: spriteBatchTransform); foreach (Character character in Character.CharacterList) { - if (character.CurrentHull == null || !character.Enabled) { continue; } + if (character.CurrentHull == null || !character.Enabled || !character.IsVisible) { continue; } if (Character.Controlled?.FocusedCharacter == character) { continue; } Color lightColor = character.CurrentHull.AmbientLight == Color.TransparentBlack ? Color.Black : @@ -335,20 +337,34 @@ namespace Barotrauma.Lights if (Character.Controlled != null) { - Vector2 haloDrawPos = Character.Controlled.DrawPosition; + DrawHalo(Character.Controlled); + } + else + { + foreach (Character character in Character.CharacterList) + { + if (character.Submarine == null || character.IsDead || !character.IsHuman) { continue; } + DrawHalo(character); + } + } + + void DrawHalo(Character character) + { + Vector2 haloDrawPos = character.DrawPosition; haloDrawPos.Y = -haloDrawPos.Y; //ambient light decreases the brightness of the halo (no need for a bright halo if the ambient light is bright enough) float ambientBrightness = (AmbientLight.R + AmbientLight.B + AmbientLight.G) / 255.0f / 3.0f; - Color haloColor = Color.White.Multiply(0.3f - ambientBrightness); + Color haloColor = Color.White.Multiply(0.3f - ambientBrightness); if (haloColor.A > 0) { float scale = 512.0f / LightSource.LightTexture.Width; spriteBatch.Draw( LightSource.LightTexture, haloDrawPos, null, haloColor, 0.0f, new Vector2(LightSource.LightTexture.Width, LightSource.LightTexture.Height) / 2, scale, SpriteEffects.None, 0.0f); - } + } } + spriteBatch.End(); //draw the actual light volumes, additive particles, hull ambient lights and the halo around the player @@ -477,10 +493,11 @@ namespace Barotrauma.Lights graphics.Clear(Color.Black); Vector2 diff = lookAtPosition - ViewTarget.WorldPosition; diff.Y = -diff.Y; - float rotation = MathUtils.VectorToAngle(diff); + if (diff.LengthSquared() > 30.0f) { losOffset = diff; } + float rotation = MathUtils.VectorToAngle(losOffset); Vector2 scale = new Vector2( - MathHelper.Clamp(diff.Length() / 256.0f, 2.0f, 5.0f), 2.0f); + MathHelper.Clamp(losOffset.Length() / 256.0f, 2.0f, 5.0f), 2.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); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Location.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Location.cs deleted file mode 100644 index 65ac4f008..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Location.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma -{ - partial class Location - { - private HireManager hireManager; - - public void RemoveHireableCharacter(CharacterInfo character) - { - if (!Type.HasHireableCharacters) - { - DebugConsole.ThrowError("Cannot hire a character from location \"" + Name + "\" - the location has no hireable characters.\n" + Environment.StackTrace); - return; - } - if (hireManager == null) - { - DebugConsole.ThrowError("Cannot hire a character from location \"" + Name + "\" - hire manager has not been instantiated.\n" + Environment.StackTrace); - return; - } - - hireManager.RemoveCharacter(character); - } - - public IEnumerable GetHireableCharacters() - { - if (!Type.HasHireableCharacters) - { - return Enumerable.Empty(); - } - - if (hireManager == null) - { - hireManager = new HireManager(); - } - if (!hireManager.AvailableCharacters.Any()) - { - hireManager.GenerateCharacters(location: this, amount: HireManager.MaxAvailableCharacters); - } - return hireManager.AvailableCharacters; - } - - partial void RemoveProjSpecific() - { - hireManager?.Remove(); - } - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index afbfdcf3b..d36f8cc27 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -2,15 +2,15 @@ using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; namespace Barotrauma { partial class Map { - //how much larger the ice background is compared to the size of the map - private const float BackgroundScale = 1.5f; - + public bool AllowDebugTeleport; + class MapAnim { public Location StartLocation; @@ -18,7 +18,13 @@ namespace Barotrauma public string StartMessage; public string EndMessage; + /// + /// Initial zoom (0 - 1, from min zoom to max) + /// public float? StartZoom; + /// + /// Initial zoom (0 - 1, from min zoom to max) + /// public float? EndZoom; private float startDelay; @@ -40,28 +46,24 @@ namespace Barotrauma public bool Finished; } - private Queue mapAnimQueue = new Queue(); - - private Location highlightedLocation; + private readonly Queue mapAnimQueue = new Queue(); - public Location HighlightedLocation => highlightedLocation; + public Location HighlightedLocation { get; private set; } - private Vector2 drawOffset; + private static Sprite noiseOverlay; + + public Vector2 DrawOffset; private Vector2 drawOffsetNoise; - - private float subReticleAnimState; - private float targetReticleAnimState; - private Vector2 subReticlePosition; + private Vector2 currLocationIndicatorPos; private float zoom = 3.0f; + private float targetZoom; private Rectangle borders; - private MapTile[,] mapTiles; - private bool messageBoxOpen; - - public Vector2 CenterOffset; + private Sprite[,] mapTiles; + private bool[,] tileDiscovered; #if DEBUG private GUIComponent editor; @@ -81,208 +83,166 @@ namespace Barotrauma new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), paddedFrame.RectTransform), "Generate") { - OnClicked =(btn, userData) => + OnClicked = (btn, userData) => { Rand.SetSyncedSeed(ToolBox.StringToInt(this.Seed)); Generate(); + InitProjectSpecific(); return true; } }; } #endif - - struct MapTile + public Location CurrentDisplayLocation { - public readonly Sprite Sprite; - public SpriteEffects SpriteEffect; - public Vector2 Offset; - - public MapTile(Sprite sprite, SpriteEffects spriteEffect) + get { - Sprite = sprite; - SpriteEffect = spriteEffect; - - Offset = Rand.Vector(Rand.Range(0.0f, 1.0f)); + return GameMain.GameSession.Campaign.CurrentDisplayLocation; } } - + partial void InitProjectSpecific() { - OnLocationChanged += LocationChanged; + noiseOverlay ??= new Sprite("Content/UI/noise.png", Vector2.Zero); + + OnLocationChanged = LocationChanged; borders = new Rectangle( (int)Locations.Min(l => l.MapPosition.X), (int)Locations.Min(l => l.MapPosition.Y), (int)Locations.Max(l => l.MapPosition.X), (int)Locations.Max(l => l.MapPosition.Y)); - borders.Width = borders.Width - borders.X; - borders.Height = borders.Height - borders.Y; + borders.Width -= borders.X; + borders.Height -= borders.Y; - mapTiles = new MapTile[ - (int)Math.Ceiling(size * BackgroundScale / generationParams.TileSpriteSpacing.X), - (int)Math.Ceiling(size * BackgroundScale / generationParams.TileSpriteSpacing.Y)]; - - for (int x = 0; x < mapTiles.GetLength(0); x++) + if (CurrentLocation != null) { - for (int y = 0; y < mapTiles.GetLength(1); y++) + DrawOffset = -CurrentLocation.MapPosition; + } + + + Vector2 tileSize = generationParams.MapTiles.Values.First().First().size * generationParams.MapTileScale; + int tilesX = (int)Math.Ceiling(Width / tileSize.X); + int tilesY = (int)Math.Ceiling(Height / tileSize.Y); + mapTiles = new Sprite[tilesX, tilesY]; + tileDiscovered = new bool[tilesX, tilesY]; + for (int x = 0; x < tilesX; x++) + { + for (int y = 0; y < tilesY; y++) { - mapTiles[x, y] = new MapTile( - generationParams.BackgroundTileSprites[Rand.Int(generationParams.BackgroundTileSprites.Count)], Rand.Range(0.0f, 1.0f) < 0.5f ? - SpriteEffects.FlipHorizontally : SpriteEffects.None); + var biome = GetBiome(x * tileSize.X); + var tileList = generationParams.MapTiles.ContainsKey(biome.Identifier) ? + generationParams.MapTiles[biome.Identifier] : + generationParams.MapTiles.Values.First(); + mapTiles[x, y] = tileList[x % tileList.Count]; } } - drawOffset = -CurrentLocation.MapPosition; + RemoveFogOfWar(StartLocation); + + GenerateLocationConnectionVisuals(); } - - private static Texture2D rawNoiseTexture; - private static Sprite rawNoiseSprite; - private static Texture2D noiseTexture; - partial void GenerateNoiseMapProjSpecific() + partial void GenerateLocationConnectionVisuals() { - if (noiseTexture == null) + foreach (LocationConnection connection in Connections) { - CrossThread.RequestExecutionOnMainThread(() => - { - noiseTexture = new Texture2D(GameMain.Instance.GraphicsDevice, generationParams.NoiseResolution, generationParams.NoiseResolution); - rawNoiseTexture = new Texture2D(GameMain.Instance.GraphicsDevice, generationParams.NoiseResolution, generationParams.NoiseResolution); - }); - rawNoiseSprite = new Sprite(rawNoiseTexture, null, null); - } - - Color[] crackTextureData = new Color[generationParams.NoiseResolution * generationParams.NoiseResolution]; - Color[] noiseTextureData = new Color[generationParams.NoiseResolution * generationParams.NoiseResolution]; - Color[] rawNoiseTextureData = new Color[generationParams.NoiseResolution * generationParams.NoiseResolution]; - for (int x = 0; x < generationParams.NoiseResolution; x++) - { - for (int y = 0; y < generationParams.NoiseResolution; y++) - { - noiseTextureData[x + y * generationParams.NoiseResolution] = Color.Lerp(Color.Black, Color.Transparent, Noise[x, y]); - rawNoiseTextureData[x + y * generationParams.NoiseResolution] = Color.Lerp(Color.Black, Color.White, Rand.Range(0.0f,1.0f)); - } - } - - float mapRadius = size / 2; - Vector2 mapCenter = Vector2.One * mapRadius; - foreach (LocationConnection connection in connections) - { - float centerDist = Vector2.Distance(connection.CenterPos, mapCenter); - Vector2 connectionStart = connection.Locations[0].MapPosition; Vector2 connectionEnd = connection.Locations[1].MapPosition; float connectionLength = Vector2.Distance(connectionStart, connectionEnd); - int iterations = (int)(Math.Sqrt(connectionLength * generationParams.ConnectionIndicatorIterationMultiplier)); - connection.CrackSegments = MathUtils.GenerateJaggedLine( - connectionStart, connectionEnd, - iterations, connectionLength * generationParams.ConnectionIndicatorDisplacementMultiplier); - - iterations = (int)(Math.Sqrt(connectionLength * generationParams.ConnectionIterationMultiplier)); - var visualCrackSegments = MathUtils.GenerateJaggedLine( - connectionStart, connectionEnd, - iterations, connectionLength * generationParams.ConnectionDisplacementMultiplier); - - float totalLength = Vector2.Distance(visualCrackSegments[0][0], visualCrackSegments.Last()[1]); - for (int i = 0; i < visualCrackSegments.Count; i++) - { - Vector2 start = visualCrackSegments[i][0] * (generationParams.NoiseResolution / (float)size); - Vector2 end = visualCrackSegments[i][1] * (generationParams.NoiseResolution / (float)size); - - float length = Vector2.Distance(start, end); - for (float x = 0; x < 1; x += 1.0f / length) - { - Vector2 pos = Vector2.Lerp(start, end, x); - SetNoiseColorOnArea(pos, MathHelper.Clamp((int)(totalLength / 30), 2, 5) + Rand.Range(-1,1), Color.Transparent); - } - } + int iterations = Math.Min((int)Math.Sqrt(connectionLength * generationParams.ConnectionIndicatorIterationMultiplier), 5); + connection.CrackSegments.Clear(); + connection.CrackSegments.AddRange(MathUtils.GenerateJaggedLine( + connectionStart, connectionEnd, + iterations, connectionLength * generationParams.ConnectionIndicatorDisplacementMultiplier)); } - - void SetNoiseColorOnArea(Vector2 pos, int dist, Color color) - { - for (int x = -dist; x < dist; x++) - { - for (int y = -dist; y < dist; y++) - { - float d = 1.0f - new Vector2(x, y).Length() / dist; - if (d <= 0) continue; - - int xIndex = (int)pos.X + x; - if (xIndex < 0 || xIndex >= generationParams.NoiseResolution) continue; - int yIndex = (int)pos.Y + y; - if (yIndex < 0 || yIndex >= generationParams.NoiseResolution) continue; - - float perlin = (float)PerlinNoise.CalculatePerlin( - xIndex / (float)generationParams.NoiseResolution * 100.0f, - yIndex / (float)generationParams.NoiseResolution * 100.0f, 0); - - byte a = Math.Max(crackTextureData[xIndex + yIndex * generationParams.NoiseResolution].A, (byte)((d * perlin) * 255)); - - crackTextureData[xIndex + yIndex * generationParams.NoiseResolution].A = a; - } - } - } - - for (int i = 0; i < noiseTextureData.Length; i++) - { - float darken = noiseTextureData[i].A / 255.0f; - Color pathColor = Color.Lerp(Color.White, Color.Transparent, noiseTextureData[i].A / 255.0f); - noiseTextureData[i] = - Color.Lerp(noiseTextureData[i], pathColor, crackTextureData[i].A / 255.0f * 0.5f); - } - - CrossThread.RequestExecutionOnMainThread(() => - { - noiseTexture.SetData(noiseTextureData); - rawNoiseTexture.SetData(rawNoiseTextureData); - }); } private void LocationChanged(Location prevLocation, Location newLocation) { if (prevLocation == newLocation) return; //focus on starting location - mapAnimQueue.Enqueue(new MapAnim() + if (prevLocation != null) { - EndZoom = 2.0f, - EndLocation = prevLocation, - Duration = MathHelper.Clamp(Vector2.Distance(-drawOffset, prevLocation.MapPosition) / 1000.0f, 0.1f, 0.5f), - }); - mapAnimQueue.Enqueue(new MapAnim() + mapAnimQueue.Enqueue(new MapAnim() + { + EndZoom = 1.0f, + EndLocation = prevLocation, + Duration = MathHelper.Clamp(Vector2.Distance(-DrawOffset, prevLocation.MapPosition) / 1000.0f, 0.1f, 0.5f) + }); + mapAnimQueue.Enqueue(new MapAnim() + { + EndZoom = 0.5f, + StartLocation = prevLocation, + EndLocation = newLocation, + Duration = 2.0f, + StartDelay = 0.5f + }); + } + else { - EndZoom = 3.0f, - StartLocation = prevLocation, - EndLocation = newLocation, - Duration = 2.0f, - StartDelay = 0.5f - }); + currLocationIndicatorPos = CurrentLocation.MapPosition; + } + + RemoveFogOfWar(newLocation); + } + + private void RemoveFogOfWar(Location location, bool removeFromAdjacentLocations = true) + { + if (location == null) { return; } + Vector2 mapTileSize = mapTiles[0, 0].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)); + for (int x = startX; x <= endX; x++) + { + for (int y = startY; y <= endY; y++) + { + tileDiscovered[x, y] = true; + } + } + if (removeFromAdjacentLocations) + { + foreach (LocationConnection c in location.Connections) + { + var otherLocation = c.OtherLocation(location); + RemoveFogOfWar(otherLocation, removeFromAdjacentLocations: false); + } + } + } + + private bool IsInFogOfWar(Location location) + { + if (GameMain.DebugDraw) { return false; } + Vector2 mapTileSize = mapTiles[0, 0].size * generationParams.MapTileScale; + int x = (int)Math.Floor(location.MapPosition.X / mapTileSize.X); + int y = (int)Math.Floor(location.MapPosition.Y / mapTileSize.Y); + + return !tileDiscovered[MathHelper.Clamp(x, 0, tileDiscovered.Length), MathHelper.Clamp(y, 0, tileDiscovered.Length)]; } partial void ChangeLocationType(Location location, string prevName, LocationTypeChange change) - { - //focus on the location - var mapAnim = new MapAnim() + { + if (change.Messages.Any()) { - EndZoom = zoom * 1.5f, - EndLocation = location, - Duration = CurrentLocation == location ? 1.0f : 2.0f, - StartDelay = 1.0f - }; - if (change.Messages != null && change.Messages.Count > 0) - { - mapAnim.EndMessage = change.Messages[Rand.Range(0, change.Messages.Count)] + string msg = change.Messages[Rand.Range(0, change.Messages.Count)] .Replace("[previousname]", prevName) .Replace("[name]", location.Name); - } - mapAnimQueue.Enqueue(mapAnim); - - mapAnimQueue.Enqueue(new MapAnim() - { - EndZoom = zoom, - StartLocation = location, - EndLocation = CurrentLocation, - Duration = 1.0f, - StartDelay = 0.5f - }); + location.LastTypeChangeMessage = msg; + if (GameMain.Client != null) + { + GameMain.Client.AddChatMessage(msg, Networking.ChatMessageType.Default, TextManager.Get("RadioAnnouncerName")); + } + else + { + GameMain.GameSession?.GameMode.CrewManager.AddSinglePlayerChatMessage( + TextManager.Get("RadioAnnouncerName"), + msg, + Networking.ChatMessageType.Default, + sender: null); + } + } } partial void ClearAnimQueue() @@ -294,13 +254,20 @@ namespace Barotrauma { Rectangle rect = mapContainer.Rect; - subReticlePosition = Vector2.Lerp(subReticlePosition, CurrentLocation.MapPosition, deltaTime); - subReticleAnimState = 0.8f - Vector2.Distance(subReticlePosition, CurrentLocation.MapPosition) / 50.0f; - subReticleAnimState = MathHelper.Clamp(subReticleAnimState + (float)Math.Sin(Timing.TotalTime * 3.5f) * 0.2f, 0.0f, 1.0f); + if (CurrentDisplayLocation != null) + { + if (!CurrentDisplayLocation.Discovered) + { + RemoveFogOfWar(CurrentDisplayLocation); + CurrentDisplayLocation.Discovered = true; + if (CurrentDisplayLocation.MapPosition.X > furthestDiscoveredLocation.MapPosition.X) + { + furthestDiscoveredLocation = CurrentDisplayLocation; + } + } + } - targetReticleAnimState = SelectedLocation == null ? - Math.Max(targetReticleAnimState - deltaTime, 0.0f) : - Math.Min(targetReticleAnimState + deltaTime, 0.6f + (float)Math.Sin(Timing.TotalTime * 2.5f) * 0.4f); + currLocationIndicatorPos = Vector2.Lerp(currLocationIndicatorPos, CurrentDisplayLocation.MapPosition, deltaTime); #if DEBUG if (GameMain.DebugDraw) { @@ -311,7 +278,7 @@ namespace Barotrauma if (mapAnimQueue.Count > 0) { - hudOpenState = Math.Max(hudOpenState - deltaTime, 0.0f); + hudVisibility = Math.Max(hudVisibility - deltaTime, 0.0f); UpdateMapAnim(mapAnimQueue.Peek(), deltaTime); if (mapAnimQueue.Peek().Finished) { @@ -320,22 +287,25 @@ namespace Barotrauma return; } - hudOpenState = Math.Min(hudOpenState + deltaTime, 0.75f + (float)Math.Sin(Timing.TotalTime * 3.0f) * 0.25f); - - Vector2 rectCenter = new Vector2(rect.Center.X, rect.Center.Y) + CenterOffset; + hudVisibility = Math.Min(hudVisibility + deltaTime, 0.75f + (float)Math.Sin(Timing.TotalTime * 3.0f) * 0.25f); + Vector2 rectCenter = new Vector2(rect.Center.X, rect.Center.Y); + Vector2 viewOffset = DrawOffset + drawOffsetNoise; + float closestDist = 0.0f; - highlightedLocation = null; + HighlightedLocation = null; if (GUI.MouseOn == null || GUI.MouseOn == mapContainer) { for (int i = 0; i < Locations.Count; i++) { Location location = Locations[i]; - Vector2 pos = rectCenter + (location.MapPosition + drawOffset) * zoom; + if (IsInFogOfWar(location) && !(CurrentDisplayLocation?.Connections.Any(c => c.Locations.Contains(location)) ?? false) && !GameMain.DebugDraw) { continue; } + Vector2 pos = rectCenter + (location.MapPosition + viewOffset) * zoom; if (!rect.Contains(pos)) { continue; } - float iconScale = MapGenerationParams.Instance.LocationIconSize / location.Type.Sprite.size.X; + float iconScale = generationParams.LocationIconSize / location.Type.Sprite.size.X; + if (location == CurrentDisplayLocation) { iconScale *= 1.2f; } Rectangle drawRect = location.Type.Sprite.SourceRect; drawRect.Width = (int)(drawRect.Width * iconScale * zoom * 1.4f); @@ -343,13 +313,13 @@ namespace Barotrauma drawRect.X = (int)pos.X - drawRect.Width / 2; drawRect.Y = (int)pos.Y - drawRect.Width / 2; - if (!drawRect.Contains(PlayerInput.MousePosition)) continue; + if (!drawRect.Contains(PlayerInput.MousePosition)) { continue; } float dist = Vector2.Distance(PlayerInput.MousePosition, pos); - if (highlightedLocation == null || dist < closestDist) + if (HighlightedLocation == null || dist < closestDist) { closestDist = dist; - highlightedLocation = location; + HighlightedLocation = location; } } } @@ -362,25 +332,27 @@ namespace Barotrauma if (PlayerInput.KeyDown(InputType.Right)) { moveAmount -= Vector2.UnitX; } if (PlayerInput.KeyDown(InputType.Up)) { moveAmount += Vector2.UnitY; } if (PlayerInput.KeyDown(InputType.Down)) { moveAmount -= Vector2.UnitY; } - drawOffset += moveAmount * moveSpeed / zoom * deltaTime; + DrawOffset += moveAmount * moveSpeed / zoom * deltaTime; } + targetZoom = MathHelper.Clamp(targetZoom, generationParams.MinZoom, generationParams.MaxZoom); + zoom = MathHelper.Lerp(zoom, targetZoom, 0.1f); + if (GUI.MouseOn == mapContainer) { - foreach (LocationConnection connection in connections) + foreach (LocationConnection connection in Connections) { - if (highlightedLocation != CurrentLocation && - connection.Locations.Contains(highlightedLocation) && connection.Locations.Contains(CurrentLocation)) + if (HighlightedLocation != CurrentDisplayLocation && + connection.Locations.Contains(HighlightedLocation) && connection.Locations.Contains(CurrentDisplayLocation)) { if (PlayerInput.PrimaryMouseButtonClicked() && - SelectedLocation != highlightedLocation && highlightedLocation != null) + SelectedLocation != HighlightedLocation && HighlightedLocation != null) { //clients aren't allowed to select the location without a permission - if (GameMain.Client == null || GameMain.Client.HasPermission(Networking.ClientPermissions.ManageCampaign)) + if ((GameMain.GameSession?.GameMode as CampaignMode)?.AllowedToManageCampaign() ?? false) { SelectedConnection = connection; - SelectedLocation = highlightedLocation; - targetReticleAnimState = 0.0f; + SelectedLocation = HighlightedLocation; OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection); GameMain.Client?.SendCampaignState(); @@ -389,30 +361,51 @@ namespace Barotrauma } } - zoom += PlayerInput.ScrollWheelSpeed / 1000.0f; - zoom = MathHelper.Clamp(zoom, 1.0f, 4.0f); + targetZoom += PlayerInput.ScrollWheelSpeed / 500.0f; - if (PlayerInput.MidButtonHeld() || (highlightedLocation == null && PlayerInput.PrimaryMouseButtonHeld())) + if (PlayerInput.MidButtonHeld() || (HighlightedLocation == null && PlayerInput.PrimaryMouseButtonHeld())) { - drawOffset += PlayerInput.MouseSpeed / zoom; + DrawOffset += PlayerInput.MouseSpeed / zoom; } -#if DEBUG - if (PlayerInput.DoubleClicked() && highlightedLocation != null) + if (AllowDebugTeleport) { - var passedConnection = CurrentLocation.Connections.Find(c => c.OtherLocation(CurrentLocation) == highlightedLocation); - if (passedConnection != null) + if (PlayerInput.DoubleClicked() && HighlightedLocation != null) { - passedConnection.Passed = true; + var passedConnection = CurrentDisplayLocation.Connections.Find(c => c.OtherLocation(CurrentDisplayLocation) == HighlightedLocation); + if (passedConnection != null) + { + passedConnection.Passed = true; + } + + Location prevLocation = CurrentDisplayLocation; + CurrentLocation = HighlightedLocation; + Level.Loaded.DebugSetStartLocation(CurrentLocation); + + CurrentLocation.Discovered = true; + CurrentLocation.CreateStore(); + OnLocationChanged?.Invoke(prevLocation, CurrentLocation); + SelectLocation(-1); + if (GameMain.Client == null) + { + ProgressWorld(); + } + else + { + GameMain.Client.SendCampaignState(); + } } - Location prevLocation = CurrentLocation; - CurrentLocation = highlightedLocation; - CurrentLocation.Discovered = true; - OnLocationChanged?.Invoke(prevLocation, CurrentLocation); - SelectLocation(-1); - ProgressWorld(); + if (PlayerInput.KeyDown(Microsoft.Xna.Framework.Input.Keys.LeftShift) && PlayerInput.PrimaryMouseButtonClicked() && HighlightedLocation != null) + { + int distance = DistanceToClosestLocationWithOutpost(HighlightedLocation, out Location foundLocation); + DebugConsole.NewMessage($"Distance to closest outpost from {HighlightedLocation.Name} to {foundLocation?.Name} is {distance}"); + } + + if (PlayerInput.PrimaryMouseButtonClicked() && HighlightedLocation == null) + { + SelectLocation(-1); + } } -#endif } } @@ -421,177 +414,100 @@ namespace Barotrauma Rectangle rect = mapContainer.Rect; Vector2 viewSize = new Vector2(rect.Width / zoom, rect.Height / zoom); - float edgeBuffer = size * (BackgroundScale - 1.0f) / 2; - drawOffset.X = MathHelper.Clamp(drawOffset.X, -size - edgeBuffer + viewSize.X / 2.0f, edgeBuffer -viewSize.X / 2.0f); - drawOffset.Y = MathHelper.Clamp(drawOffset.Y, -size - edgeBuffer + viewSize.Y / 2.0f, edgeBuffer -viewSize.Y / 2.0f); + Vector2 edgeBuffer = rect.Size.ToVector2() / 2; + DrawOffset.X = MathHelper.Clamp(DrawOffset.X, -Width - edgeBuffer.X + viewSize.X / 2.0f, edgeBuffer.X - viewSize.X / 2.0f); + DrawOffset.Y = MathHelper.Clamp(DrawOffset.Y, -Height - edgeBuffer.Y + viewSize.Y / 2.0f, edgeBuffer.Y - viewSize.Y / 2.0f); drawOffsetNoise = new Vector2( (float)PerlinNoise.CalculatePerlin(Timing.TotalTime * 0.1f % 255, Timing.TotalTime * 0.1f % 255, 0) - 0.5f, (float)PerlinNoise.CalculatePerlin(Timing.TotalTime * 0.2f % 255, Timing.TotalTime * 0.2f % 255, 0.5f) - 0.5f) * 10.0f; - Vector2 viewOffset = drawOffset + drawOffsetNoise; + Vector2 viewOffset = DrawOffset + drawOffsetNoise; - Vector2 rectCenter = new Vector2(rect.Center.X, rect.Center.Y) + CenterOffset; + Vector2 rectCenter = new Vector2(rect.Center.X, rect.Center.Y); Rectangle prevScissorRect = GameMain.Instance.GraphicsDevice.ScissorRectangle; spriteBatch.End(); spriteBatch.GraphicsDevice.ScissorRectangle = Rectangle.Intersect(prevScissorRect, rect); spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); - for (int x = 0; x < mapTiles.GetLength(0); x++) + Vector2 topLeft = rectCenter + viewOffset; + Vector2 bottomRight = rectCenter + (viewOffset + new Vector2(Width, Height)); + Vector2 mapTileSize = mapTiles[0, 0].size * generationParams.MapTileScale; + + int startX = (int)Math.Floor(-topLeft.X / mapTileSize.X) - 1; + int startY = (int)Math.Floor(-topLeft.Y / mapTileSize.Y) - 1; + int endX = (int)Math.Ceiling((-topLeft.X + rect.Width) / mapTileSize.X); + int endY = (int)Math.Ceiling((-topLeft.Y + rect.Height) / mapTileSize.Y); + + float noiseT = (float)(Timing.TotalTime * 0.01f); + cameraNoiseStrength = (float)PerlinNoise.CalculatePerlin(noiseT, noiseT * 0.5f, noiseT * 0.2f); + float noiseScale = (float)PerlinNoise.CalculatePerlin(noiseT * 5.0f, noiseT * 2.0f, 0) * 5.0f; + + for (int x = startX; x <= endX; x++) { - for (int y = 0; y < mapTiles.GetLength(1); y++) + for (int y = startY; y <= endY; y++) { - Vector2 mapPos = new Vector2( - x * generationParams.TileSpriteSpacing.X + ((y % 2 == 0) ? 0.0f : generationParams.TileSpriteSpacing.X * 0.5f), - y * generationParams.TileSpriteSpacing.Y); - - mapPos.X -= size / 2 * (BackgroundScale - 1.0f); - mapPos.Y -= size / 2 * (BackgroundScale - 1.0f); - - Vector2 scale = new Vector2( - generationParams.TileSpriteSize.X / mapTiles[x, y].Sprite.size.X, - generationParams.TileSpriteSize.Y / mapTiles[x, y].Sprite.size.Y); - mapTiles[x, y].Sprite.Draw(spriteBatch, rectCenter + (mapPos + viewOffset) * zoom, Color.White, - origin: new Vector2(256.0f, 256.0f), rotate: 0, scale: scale * zoom, spriteEffect: mapTiles[x, y].SpriteEffect); + int tileX = Math.Abs(x) % mapTiles.GetLength(0); + int tileY = Math.Abs(y) % mapTiles.GetLength(1); + Vector2 tilePos = rectCenter + (viewOffset + new Vector2(x, y) * mapTileSize) * zoom; + mapTiles[tileX, tileY].Draw(spriteBatch, tilePos, Color.White, origin: Vector2.Zero, scale: generationParams.MapTileScale * zoom); + + if (GameMain.DebugDraw) { continue; } + if (!tileDiscovered[tileX, tileY] || x < 0 || y < 0 || x >= tileDiscovered.GetLength(0) || y >= tileDiscovered.GetLength(1)) + { + generationParams.FogOfWarSprite?.Draw(spriteBatch, tilePos, Color.White * cameraNoiseStrength, origin: Vector2.Zero, scale: generationParams.MapTileScale * zoom); + noiseOverlay.DrawTiled(spriteBatch, tilePos, mapTileSize * zoom, + startOffset: new Vector2(Rand.Range(0.0f, noiseOverlay.SourceRect.Width), Rand.Range(0.0f, noiseOverlay.SourceRect.Height)), + color: Color.White * cameraNoiseStrength * 0.2f, + textureScale: Vector2.One * noiseScale); + } } } -#if DEBUG - if (generationParams.ShowNoiseMap) + + if (GameMain.DebugDraw) { - GUI.DrawRectangle(spriteBatch, rectCenter + (borders.Location.ToVector2() + viewOffset) * zoom, borders.Size.ToVector2() * zoom, Color.White, true); + if (topLeft.X > rect.X) + GUI.DrawRectangle(spriteBatch, new Rectangle(rect.X, rect.Y, (int)(topLeft.X - rect.X), rect.Height), Color.Black * 0.5f, true); + if (topLeft.Y > rect.Y) + GUI.DrawRectangle(spriteBatch, new Rectangle((int)topLeft.X, rect.Y, (int)(bottomRight.X - topLeft.X), (int)(topLeft.Y - rect.Y)), Color.Black * 0.5f, true); + if (bottomRight.X < rect.Right) + GUI.DrawRectangle(spriteBatch, new Rectangle((int)bottomRight.X, rect.Y, (int)(rect.Right - bottomRight.X), rect.Height), Color.Black * 0.5f, true); + if (bottomRight.Y < rect.Bottom) + GUI.DrawRectangle(spriteBatch, new Rectangle((int)topLeft.X, (int)bottomRight.Y, (int)(bottomRight.X - topLeft.X), (int)(rect.Bottom - bottomRight.Y)), Color.Black * 0.5f, true); } -#endif - Vector2 topLeft = rectCenter + viewOffset * zoom; - topLeft.X = (int)topLeft.X; - topLeft.Y = (int)topLeft.Y; - Vector2 bottomRight = rectCenter + (viewOffset + new Vector2(size,size)) * zoom; - bottomRight.X = (int)bottomRight.X; - bottomRight.Y = (int)bottomRight.Y; + float rawNoiseScale = 1.0f + PerlinNoise.GetPerlin((int)(Timing.TotalTime * 1 - 1), (int)(Timing.TotalTime * 1 - 1)); + cameraNoiseStrength = PerlinNoise.GetPerlin((int)(Timing.TotalTime * 1 - 1), (int)(Timing.TotalTime * 1 - 1)); - spriteBatch.Draw(noiseTexture, - destinationRectangle: new Rectangle((int)topLeft.X, (int)topLeft.Y, (int)(bottomRight.X- topLeft.X), (int)(bottomRight.Y - topLeft.Y)), - sourceRectangle: null, - color: Color.White); - - if (topLeft.X > rect.X) - GUI.DrawRectangle(spriteBatch, new Rectangle(rect.X, rect.Y, (int)(topLeft.X- rect.X), rect.Height), Color.Black* 0.8f, true); - if (topLeft.Y > rect.Y) - GUI.DrawRectangle(spriteBatch, new Rectangle((int)topLeft.X, rect.Y, (int)(bottomRight.X - topLeft.X), (int)(topLeft.Y - rect.Y)), Color.Black * 0.8f, true); - if (bottomRight.X < rect.Right) - GUI.DrawRectangle(spriteBatch, new Rectangle((int)bottomRight.X, rect.Y, (int)(rect.Right - bottomRight.X), rect.Height), Color.Black * 0.8f, true); - if (bottomRight.Y < rect.Bottom) - GUI.DrawRectangle(spriteBatch, new Rectangle((int)topLeft.X, (int)bottomRight.Y, (int)(bottomRight.X - topLeft.X), (int)(rect.Bottom - bottomRight.Y)), Color.Black * 0.8f, true); - - var sourceRect = rect; - float rawNoiseScale = 1.0f + Noise[(int)(Timing.TotalTime * 100 % Noise.GetLength(0) - 1), (int)(Timing.TotalTime * 100 % Noise.GetLength(1) - 1)]; - cameraNoiseStrength = Noise[(int)(Timing.TotalTime * 10 % Noise.GetLength(0) - 1), (int)(Timing.TotalTime * 10 % Noise.GetLength(1) - 1)]; - - rawNoiseSprite.DrawTiled(spriteBatch, rect.Location.ToVector2(), rect.Size.ToVector2(), - startOffset: new Point(Rand.Range(0,rawNoiseSprite.SourceRect.Width), Rand.Range(0, rawNoiseSprite.SourceRect.Height)), - color : Color.White * cameraNoiseStrength * 0.5f, + noiseOverlay.DrawTiled(spriteBatch, rect.Location.ToVector2(), rect.Size.ToVector2(), + startOffset: new Vector2(Rand.Range(0.0f, noiseOverlay.SourceRect.Width), Rand.Range(0.0f, noiseOverlay.SourceRect.Height)), + color : Color.White * cameraNoiseStrength * 0.1f, textureScale: Vector2.One * rawNoiseScale); - rawNoiseSprite.DrawTiled(spriteBatch, rect.Location.ToVector2(), rect.Size.ToVector2(), - startOffset: new Point(Rand.Range(0, rawNoiseSprite.SourceRect.Width), Rand.Range(0, rawNoiseSprite.SourceRect.Height)), - color: new Color(20,20,20,100), + noiseOverlay.DrawTiled(spriteBatch, rect.Location.ToVector2(), rect.Size.ToVector2(), + startOffset: new Vector2(Rand.Range(0.0f, noiseOverlay.SourceRect.Width), Rand.Range(0.0f, noiseOverlay.SourceRect.Height)), + color: new Color(20,20,20,50), textureScale: Vector2.One * rawNoiseScale * 2); + noiseOverlay.DrawTiled(spriteBatch, Vector2.Zero, new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight), + startOffset: new Vector2(Rand.Range(0.0f, noiseOverlay.SourceRect.Width), Rand.Range(0.0f, noiseOverlay.SourceRect.Height)), + color: Color.White * cameraNoiseStrength * 0.1f, + textureScale: Vector2.One * noiseScale); + + Pair tooltip = null; if (generationParams.ShowLocations) { - foreach (LocationConnection connection in connections) + foreach (LocationConnection connection in Connections) { - Color connectionColor; - if (GameMain.DebugDraw) - { - float sizeFactor = MathUtils.InverseLerp( - MapGenerationParams.Instance.SmallLevelConnectionLength, - MapGenerationParams.Instance.LargeLevelConnectionLength, - connection.Length); - - connectionColor = ToolBox.GradientLerp(sizeFactor, Color.LightGreen, GUI.Style.Orange, GUI.Style.Red); - } - else - { - connectionColor = ToolBox.GradientLerp(connection.Difficulty / 100.0f, - MapGenerationParams.Instance.LowDifficultyColor, - MapGenerationParams.Instance.MediumDifficultyColor, - MapGenerationParams.Instance.HighDifficultyColor); - } - - int width = (int)(3 * zoom); - - if (SelectedLocation != CurrentLocation && - (connection.Locations.Contains(SelectedLocation) && connection.Locations.Contains(CurrentLocation))) - { - connectionColor = Color.Gold; - width *= 2; - } - else if (highlightedLocation != CurrentLocation && - (connection.Locations.Contains(highlightedLocation) && connection.Locations.Contains(CurrentLocation))) - { - connectionColor = Color.Lerp(connectionColor, Color.White, 0.5f); - width *= 2; - } - else if (!connection.Passed) - { - //crackColor *= 0.5f; - } - - for (int i = 0; i < connection.CrackSegments.Count; i++) - { - var segment = connection.CrackSegments[i]; - - Vector2 start = rectCenter + (segment[0] + viewOffset) * zoom; - Vector2 end = rectCenter + (segment[1] + viewOffset) * zoom; - - if (!rect.Contains(start) && !rect.Contains(end)) - { - continue; - } - else - { - if (MathUtils.GetLineRectangleIntersection(start, end, new Rectangle(rect.X, rect.Y + rect.Height, rect.Width, rect.Height), out Vector2 intersection)) - { - if (!rect.Contains(start)) - { - start = intersection; - } - else - { - end = intersection; - } - } - } - - float distFromPlayer = Vector2.Distance(CurrentLocation.MapPosition, (segment[0] + segment[1]) / 2.0f); - float dist = Vector2.Distance(start, end); - - float a = GameMain.DebugDraw ? 1.0f : (200.0f - distFromPlayer) / 200.0f; - spriteBatch.Draw(generationParams.ConnectionSprite.Texture, - new Rectangle((int)start.X, (int)start.Y, (int)(dist - 1 * zoom), width), - null, connectionColor * MathHelper.Clamp(a, 0.1f, 0.5f), MathUtils.VectorToAngle(end - start), - new Vector2(0, 16), SpriteEffects.None, 0.01f); - } - - if (GameMain.DebugDraw && zoom > 1.0f && generationParams.ShowLevelTypeNames) - { - Vector2 center = rectCenter + (connection.CenterPos + viewOffset) * zoom; - if (rect.Contains(center)) - { - GUI.DrawString(spriteBatch, center, connection.Biome.Identifier + " (" + connection.Difficulty + ")", Color.White); - } - } + if (IsInFogOfWar(connection.Locations[0]) && IsInFogOfWar(connection.Locations[1])) { continue; } + DrawConnection(spriteBatch, connection, rect, viewOffset); } - rect.Inflate(8, 8); - GUI.DrawRectangle(spriteBatch, rect, Color.Black, false, 0.0f, 8); - GUI.DrawRectangle(spriteBatch, rect, Color.LightGray); - for (int i = 0; i < Locations.Count; i++) { Location location = Locations[i]; + if (IsInFogOfWar(location)) { continue; } Vector2 pos = rectCenter + (location.MapPosition + viewOffset) * zoom; Rectangle drawRect = location.Type.Sprite.SourceRect; @@ -600,183 +516,228 @@ namespace Barotrauma if (!rect.Intersects(drawRect)) { continue; } - Color color = location.Type.SpriteColor; - if (location.Connections.Find(c => c.Locations.Contains(CurrentLocation)) == null) + if (location == CurrentDisplayLocation ) + { + generationParams.CurrentLocationIndicator.Draw(spriteBatch, + rectCenter + (currLocationIndicatorPos + viewOffset) * zoom, + generationParams.IndicatorColor, + generationParams.CurrentLocationIndicator.Origin, 0, Vector2.One * (generationParams.LocationIconSize / generationParams.CurrentLocationIndicator.size.X) * 1.7f * zoom); + } + + if (location == SelectedLocation) + { + generationParams.SelectedLocationIndicator.Draw(spriteBatch, + rectCenter + (location.MapPosition + viewOffset) * zoom, + generationParams.IndicatorColor, + generationParams.SelectedLocationIndicator.Origin, 0, Vector2.One * (generationParams.LocationIconSize / generationParams.SelectedLocationIndicator.size.X) * 1.7f * zoom); + } + + Color color = location.Type.SpriteColor; + if (!location.Discovered) { color = Color.White; } + if (location.Connections.Find(c => c.Locations.Contains(CurrentDisplayLocation)) == null) { color *= 0.5f; } - float iconScale = location == CurrentLocation ? 1.2f : 1.0f; - if (location == highlightedLocation) + float iconScale = location == CurrentDisplayLocation ? 1.2f : 1.0f; + if (location == HighlightedLocation) { - iconScale *= 1.1f; - color = Color.Lerp(color, Color.White, 0.5f); + iconScale *= 1.2f; } - - float distFromPlayer = Vector2.Distance(CurrentLocation.MapPosition, location.MapPosition); - color *= MathHelper.Clamp((1000.0f - distFromPlayer) / 500.0f, 0.1f, 1.0f); location.Type.Sprite.Draw(spriteBatch, pos, color, - scale: MapGenerationParams.Instance.LocationIconSize / location.Type.Sprite.size.X * iconScale * zoom); - MapGenerationParams.Instance.LocationIndicator.Draw(spriteBatch, pos, color, - scale: MapGenerationParams.Instance.LocationIconSize / MapGenerationParams.Instance.LocationIndicator.size.X * iconScale * zoom * 1.4f); - } - - //PLACEHOLDER until the stuff at the center of the map is implemented - float centerIconSize = 50.0f; - Vector2 centerPos = rectCenter + (new Vector2(size / 2) + viewOffset) * zoom; - bool mouseOn = Vector2.Distance(PlayerInput.MousePosition, centerPos) < centerIconSize * zoom; - - var centerLocationType = LocationType.List.Last(); - Color centerColor = centerLocationType.SpriteColor * (mouseOn ? 1.0f : 0.6f); - centerLocationType.Sprite.Draw(spriteBatch, centerPos, centerColor, - scale: centerIconSize / centerLocationType.Sprite.size.X * zoom); - MapGenerationParams.Instance.LocationIndicator.Draw(spriteBatch, centerPos, centerColor, - scale: centerIconSize / MapGenerationParams.Instance.LocationIndicator.size.X * zoom * 1.2f); - - if (mouseOn && PlayerInput.PrimaryMouseButtonClicked() && !messageBoxOpen) - { - if (TextManager.ContainsTag("centerarealockedheader") && TextManager.ContainsTag("centerarealockedtext") ) + scale: generationParams.LocationIconSize / location.Type.Sprite.size.X * iconScale * zoom); + if (location.TypeChangeTimer <= 0 && !string.IsNullOrEmpty(location.LastTypeChangeMessage) && generationParams.TypeChangeIcon != null) { - var messageBox = new GUIMessageBox( - TextManager.Get("centerarealockedheader"), - TextManager.Get("centerarealockedtext")); - messageBoxOpen = true; - CoroutineManager.StartCoroutine(WaitForMessageBoxClosed(messageBox)); + 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); + if (Vector2.Distance(PlayerInput.MousePosition, typeChangeIconPos) < generationParams.TypeChangeIcon.SourceRect.Width * zoom) + { + tooltip = new Pair( + new Rectangle(typeChangeIconPos.ToPoint(), new Point(30)), + location.LastTypeChangeMessage); + } } - else + if (location != CurrentLocation && CurrentLocation.AvailableMissions.Any(m => m.Locations.Contains(location)) && generationParams.MissionIcon != null) { - //if the message cannot be shown in the selected language, - //show the campaign roadmap (which mentions the center location not being reachable) - var messageBox = new GUIMessageBox(TextManager.Get("CampaignRoadMapTitle"), TextManager.Get("CampaignRoadMapText")); - messageBoxOpen = true; - CoroutineManager.StartCoroutine(WaitForMessageBoxClosed(messageBox)); + Vector2 missionIconPos = pos + new Vector2(1.35f, 0.35f) * generationParams.LocationIconSize * 0.5f * zoom; + float missionIconScale = 18.0f / generationParams.MissionIcon.SourceRect.Width; + generationParams.MissionIcon.Draw(spriteBatch, missionIconPos, generationParams.IndicatorColor, scale: missionIconScale * zoom); + if (Vector2.Distance(PlayerInput.MousePosition, missionIconPos) < generationParams.MissionIcon.SourceRect.Width * zoom) + { + var availableMissions = CurrentLocation.AvailableMissions.Where(m => m.Locations.Contains(location)); + tooltip = new Pair( + new Rectangle(missionIconPos.ToPoint(), new Point(30)), + TextManager.Get("mission") + '\n'+ string.Join('\n', availableMissions.Select(m => "- " + m.Name))); + } + } + + if (GameMain.DebugDraw && location == HighlightedLocation) + { + if (location.Reputation != null) + { + 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); + dPos.Y += nameSize.Y + 16; + + Rectangle bgRect = new Rectangle((int)dPos.X, (int)dPos.Y, 256, 32); + bgRect.Inflate(8,8); + Color barColor = ToolBox.GradientLerp(location.Reputation.NormalizedValue, Color.Red, Color.Yellow, Color.LightGreen); + GUI.DrawRectangle(spriteBatch, bgRect, Color.Black * 0.8f, isFilled: true); + GUI.DrawRectangle(spriteBatch, new Rectangle((int)dPos.X, (int)dPos.Y, (int)(location.Reputation.NormalizedValue * 255), 32), barColor, isFilled: true); + string reputationValue = location.Reputation.Value.ToString(CultureInfo.InvariantCulture); + 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); + GUI.DrawRectangle(spriteBatch, new Rectangle((int)dPos.X, (int)dPos.Y, 256, 32), Color.White); + } } } } - + DrawDecorativeHUD(spriteBatch, rect); - for (int i = 0; i < 2; i++) + if (HighlightedLocation != null) { - Location location = (i == 0) ? highlightedLocation : CurrentLocation; - if (location == null) continue; - - Vector2 pos = rectCenter + (location.MapPosition + viewOffset) * zoom; - pos.X += 25 * zoom; - pos.Y -= 5 * zoom; - Vector2 size = GUI.LargeFont.MeasureString(location.Name); + Vector2 pos = rectCenter + (HighlightedLocation.MapPosition + viewOffset) * zoom; + pos.X += 50 * zoom; + Vector2 nameSize = GUI.LargeFont.MeasureString(HighlightedLocation.Name); + Vector2 typeSize = GUI.Font.MeasureString(HighlightedLocation.Type.Name); + Vector2 size = new Vector2(Math.Max(nameSize.X, typeSize.X), nameSize.Y + typeSize.Y); GUI.Style.GetComponentStyle("OuterGlow").Sprites[GUIComponent.ComponentState.None][0].Draw( - spriteBatch, new Rectangle((int)pos.X - 30, (int)(pos.Y - 10), (int)size.X + 60, (int)(size.Y + 50 * GUI.Scale)), Color.Black * hudOpenState * 0.7f); - GUI.DrawString(spriteBatch, pos, - location.Name, Color.White * hudOpenState * 1.5f, font: GUI.LargeFont); - GUI.DrawString(spriteBatch, pos + Vector2.UnitY * size.Y * 0.8f, - location.Type.Name, Color.White * hudOpenState * 1.5f); + 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); + GUI.DrawString(spriteBatch, pos - new Vector2(0.0f, size.Y / 2), + HighlightedLocation.Name, GUI.Style.TextColor * hudVisibility * 1.5f, font: GUI.LargeFont); + GUI.DrawString(spriteBatch, pos + new Vector2(0.0f, size.Y / 2 - GUI.Font.MeasureString(HighlightedLocation.Type.Name).Y), + HighlightedLocation.Type.Name, GUI.Style.TextColor * hudVisibility * 1.5f); + } + if (tooltip != null) + { + GUIComponent.DrawToolTip(spriteBatch, tooltip.Second, tooltip.First); } - spriteBatch.End(); GameMain.Instance.GraphicsDevice.ScissorRectangle = prevScissorRect; spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); } - private IEnumerable WaitForMessageBoxClosed(GUIMessageBox box) + private void DrawConnection(SpriteBatch spriteBatch, LocationConnection connection, Rectangle viewArea, Vector2 viewOffset, Color? overrideColor = null) { - messageBoxOpen = true; - while (GUIMessageBox.MessageBoxes.Contains(box)) yield return null; - yield return new WaitForSeconds(.1f); - messageBoxOpen = false; + Color connectionColor; + if (GameMain.DebugDraw) + { + float sizeFactor = MathUtils.InverseLerp( + generationParams.SmallLevelConnectionLength, + generationParams.LargeLevelConnectionLength, + connection.Length); + connectionColor = ToolBox.GradientLerp(sizeFactor, Color.LightGreen, GUI.Style.Orange, GUI.Style.Red); + } + else if (overrideColor.HasValue) + { + connectionColor = overrideColor.Value; + } + else + { + connectionColor = connection.Passed ? generationParams.ConnectionColor : generationParams.UnvisitedConnectionColor; + } + + int width = (int)(generationParams.LocationConnectionWidth * zoom); + + if (Level.Loaded?.LevelData == connection.LevelData) + { + connectionColor = generationParams.HighlightedConnectionColor; + width = (int)(width * 1.5f); + } + if (SelectedLocation != CurrentDisplayLocation && + (connection.Locations.Contains(SelectedLocation) && connection.Locations.Contains(CurrentDisplayLocation))) + { + connectionColor = generationParams.HighlightedConnectionColor; + width *= 2; + } + else if (HighlightedLocation != CurrentDisplayLocation && + (connection.Locations.Contains(HighlightedLocation) && connection.Locations.Contains(CurrentDisplayLocation))) + { + connectionColor = generationParams.HighlightedConnectionColor; + width *= 2; + } + + Vector2 rectCenter = viewArea.Center.ToVector2(); + + int startIndex = connection.CrackSegments.Count > 2 ? 1 : 0; + int endIndex = connection.CrackSegments.Count > 2 ? connection.CrackSegments.Count - 1 : connection.CrackSegments.Count; + + for (int i = startIndex; i < endIndex; i++) + { + var segment = connection.CrackSegments[i]; + + Vector2 start = rectCenter + (segment[0] + viewOffset) * zoom; + Vector2 end = rectCenter + (segment[1] + viewOffset) * zoom; + + if (!viewArea.Contains(start) && !viewArea.Contains(end)) + { + continue; + } + else + { + if (MathUtils.GetLineRectangleIntersection(start, end, new Rectangle(viewArea.X, viewArea.Y + viewArea.Height, viewArea.Width, viewArea.Height), out Vector2 intersection)) + { + if (!viewArea.Contains(start)) + { + start = intersection; + } + else + { + end = intersection; + } + } + } + + float a = 1.0f; + if (!connection.Locations[0].Discovered && !connection.Locations[1].Discovered) + { + if (IsInFogOfWar(connection.Locations[0])) + { + a = (float)i / connection.CrackSegments.Count; + } + else if (IsInFogOfWar(connection.Locations[1])) + { + a = 1.0f - (float)i / connection.CrackSegments.Count; + } + } + float dist = Vector2.Distance(start, end); + var connectionSprite = connection.Passed ? generationParams.PassedConnectionSprite : generationParams.ConnectionSprite; + spriteBatch.Draw(connectionSprite.Texture, + new Rectangle((int)start.X, (int)start.Y, (int)(dist - 1 * zoom), width), + connectionSprite.SourceRect, connectionColor * a, MathUtils.VectorToAngle(end - start), + new Vector2(0, connectionSprite.size.Y / 2), SpriteEffects.None, 0.01f); + } + + if (GameMain.DebugDraw && zoom > 1.0f && generationParams.ShowLevelTypeNames) + { + Vector2 center = rectCenter + (connection.CenterPos + viewOffset) * zoom; + if (viewArea.Contains(center) && connection.Biome != null) + { + GUI.DrawString(spriteBatch, center, connection.Biome.Identifier + " (" + connection.Difficulty + ")", Color.White); + } + } } - private float hudOpenState; + private float hudVisibility; private float cameraNoiseStrength; private void DrawDecorativeHUD(SpriteBatch spriteBatch, Rectangle rect) { - spriteBatch.End(); - spriteBatch.Begin(SpriteSortMode.Deferred, blendState: BlendState.Additive, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); - - Vector2 rectCenter = rect.Center.ToVector2() + CenterOffset; - - if (generationParams.ShowOverlay) - { - Vector2 mapCenter = rectCenter + (new Vector2(size, size) / 2 + drawOffset + drawOffsetNoise) * zoom; - Vector2 centerDiff = CurrentLocation.MapPosition - new Vector2(size) / 2; - int currentZone = (int)Math.Floor((centerDiff.Length() / (size * 0.5f) * generationParams.DifficultyZones)); - for (int i = 0; i < generationParams.DifficultyZones; i++) - { - float radius = size / 2 * ((i + 1.0f) / generationParams.DifficultyZones); - float textureSize = (radius / (generationParams.MapCircle.size.X / 2) * zoom); - - generationParams.MapCircle.Draw(spriteBatch, - mapCenter, - i == currentZone || i == currentZone - 1 ? Color.White * 0.5f : Color.White * 0.2f, - i * 0.4f + (float)Timing.TotalTime * 0.01f, textureSize); - } - } - - float animPulsate = (float)Math.Sin(Timing.TotalTime * 2.0f) * 0.1f; - - Vector2 frameSize = generationParams.DecorativeGraphSprite.FrameSize.ToVector2(); - generationParams.DecorativeGraphSprite.Draw(spriteBatch, (int)((cameraNoiseStrength + animPulsate) * hudOpenState * generationParams.DecorativeGraphSprite.FrameCount), - new Vector2(rect.Right, rect.Bottom), Color.White, frameSize, 0, - Vector2.Divide(new Vector2(rect.Width / 4, rect.Height / 10), frameSize)); - - /*frameSize = generationParams.DecorativeMapSprite.FrameSize.ToVector2(); - generationParams.DecorativeMapSprite.Draw(spriteBatch, (int)((cameraNoiseStrength + animPulsate) * hudOpenState * generationParams.DecorativeMapSprite.FrameCount), - new Vector2(rect.X, rect.Y + rect.Height * 0.17f), Color.White, new Vector2(0, frameSize.Y * 0.2f), 0, - Vector2.Divide(new Vector2(rect.Width / 3, rect.Height / 5), frameSize), spriteEffect: SpriteEffects.FlipVertically); + generationParams.DecorativeGraphSprite.Draw(spriteBatch, (int)((Timing.TotalTime * 5.0f) % generationParams.DecorativeGraphSprite.FrameCount), + new Vector2(rect.Left, rect.Top), Color.White, Vector2.Zero, 0, Vector2.One * GUI.Scale); GUI.DrawString(spriteBatch, - new Vector2(rect.X + rect.Width / 15, rect.Y + rect.Height / 11), - "JOVIAN FLUX " + ((cameraNoiseStrength + Rand.Range(-0.02f, 0.02f)) * 500), Color.White * hudOpenState, font: GUI.SmallFont);*/ + 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); GUI.DrawString(spriteBatch, - new Vector2(rect.X + rect.Width * 0.27f, rect.Y + rect.Height * 0.93f), - "LAT " + (-drawOffset.Y / 100.0f) + " LON " + (-drawOffset.X / 100.0f), Color.White * hudOpenState, font: GUI.SmallFont); - - System.Text.StringBuilder sb = new System.Text.StringBuilder("GEST F "); - for (int i = 0; i < 20; i++) - { - sb.Append(Rand.Range(0.0f, 1.0f) < cameraNoiseStrength ? ToolBox.RandomSeed(1) : "0"); - } - GUI.DrawString(spriteBatch, - new Vector2(rect.X + rect.Width * 0.8f, rect.Y + rect.Height * 0.96f), - sb.ToString(), Color.White * hudOpenState, font: GUI.SmallFont); - - frameSize = generationParams.DecorativeLineTop.FrameSize.ToVector2(); - generationParams.DecorativeLineTop.Draw(spriteBatch, (int)(hudOpenState * generationParams.DecorativeLineTop.FrameCount), - new Vector2(rect.Right, rect.Y), Color.White, new Vector2(frameSize.X, frameSize.Y * 0.2f), 0, - Vector2.Divide(new Vector2(rect.Width * 0.72f, rect.Height / 9), frameSize)); - frameSize = generationParams.DecorativeLineBottom.FrameSize.ToVector2(); - generationParams.DecorativeLineBottom.Draw(spriteBatch, (int)(hudOpenState * generationParams.DecorativeLineBottom.FrameCount), - new Vector2(rect.X, rect.Bottom), Color.White, new Vector2(0, frameSize.Y * 0.6f), 0, - Vector2.Divide(new Vector2(rect.Width * 0.72f, rect.Height / 9), frameSize)); - - frameSize = generationParams.DecorativeLineCorner.FrameSize.ToVector2(); - generationParams.DecorativeLineCorner.Draw(spriteBatch, (int)((hudOpenState + animPulsate) * generationParams.DecorativeLineCorner.FrameCount), - new Vector2(rect.Right - rect.Width / 8, rect.Bottom), Color.White, frameSize * 0.8f, 0, - Vector2.Divide(new Vector2(rect.Width / 4, rect.Height / 4), frameSize), spriteEffect: SpriteEffects.FlipVertically); - - generationParams.DecorativeLineCorner.Draw(spriteBatch, (int)((hudOpenState + animPulsate) * generationParams.DecorativeLineCorner.FrameCount), - new Vector2(rect.X + rect.Width / 8, rect.Y), Color.White, frameSize * 0.1f, 0, - Vector2.Divide(new Vector2(rect.Width / 4, rect.Height / 4), frameSize), spriteEffect: SpriteEffects.FlipHorizontally); - - //reticles - generationParams.ReticleLarge.Draw(spriteBatch, (int)(subReticleAnimState * generationParams.ReticleLarge.FrameCount), - rectCenter + (subReticlePosition + drawOffset - drawOffsetNoise * 2) * zoom, Color.White, - generationParams.ReticleLarge.Origin, 0, Vector2.One * (float)Math.Sqrt(zoom) * 0.4f); - generationParams.ReticleMedium.Draw(spriteBatch, (int)(subReticleAnimState * generationParams.ReticleMedium.FrameCount), - rectCenter + (subReticlePosition + drawOffset - drawOffsetNoise) * zoom, Color.White, - generationParams.ReticleMedium.Origin, 0, new Vector2(1.0f, 0.7f) * (float)Math.Sqrt(zoom) * 0.4f); - - if (SelectedLocation != null) - { - generationParams.ReticleSmall.Draw(spriteBatch, (int)(targetReticleAnimState * generationParams.ReticleSmall.FrameCount), - rectCenter + (SelectedLocation.MapPosition + drawOffset + drawOffsetNoise * 2) * zoom, Color.White, - generationParams.ReticleSmall.Origin, 0, new Vector2(1.0f, 0.7f) * (float)Math.Sqrt(zoom) * 0.4f); - } - - spriteBatch.End(); - spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); + 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); } private void UpdateMapAnim(MapAnim anim, float deltaTime) @@ -791,19 +752,21 @@ namespace Barotrauma return; } - if (anim.StartZoom == null) anim.StartZoom = zoom; - if (anim.EndZoom == null) anim.EndZoom = zoom; + if (anim.StartZoom == null) { anim.StartZoom = MathUtils.InverseLerp(generationParams.MinZoom, generationParams.MaxZoom, zoom); } + if (anim.EndZoom == null) { anim.EndZoom = MathUtils.InverseLerp(generationParams.MinZoom, generationParams.MaxZoom, zoom); } - anim.StartPos = (anim.StartLocation == null) ? -drawOffset : anim.StartLocation.MapPosition; + anim.StartPos = (anim.StartLocation == null) ? -DrawOffset : anim.StartLocation.MapPosition; anim.Timer = Math.Min(anim.Timer + deltaTime, anim.Duration); float t = anim.Duration <= 0.0f ? 1.0f : Math.Max(anim.Timer / anim.Duration, 0.0f); - drawOffset = -Vector2.SmoothStep(anim.StartPos.Value, anim.EndLocation.MapPosition, t); - drawOffset += new Vector2( + DrawOffset = -Vector2.SmoothStep(anim.StartPos.Value, anim.EndLocation.MapPosition, t); + DrawOffset += new Vector2( (float)PerlinNoise.CalculatePerlin(Timing.TotalTime * 0.3f % 255, Timing.TotalTime * 0.4f % 255, 0) - 0.5f, (float)PerlinNoise.CalculatePerlin(Timing.TotalTime * 0.4f % 255, Timing.TotalTime * 0.3f % 255, 0.5f) - 0.5f) * 50.0f * (float)Math.Sin(t * MathHelper.Pi); - zoom = MathHelper.SmoothStep(anim.StartZoom.Value, anim.EndZoom.Value, t); + zoom = + MathHelper.Lerp(generationParams.MinZoom, generationParams.MaxZoom, + MathHelper.SmoothStep(anim.StartZoom.Value, anim.EndZoom.Value, t)); if (anim.Timer >= anim.Duration) { @@ -819,14 +782,8 @@ namespace Barotrauma partial void RemoveProjSpecific() { - rawNoiseSprite?.Remove(); - rawNoiseSprite = null; - - rawNoiseTexture?.Dispose(); - rawNoiseTexture = null; - - noiseTexture?.Dispose(); - noiseTexture = null; + noiseOverlay?.Remove(); + noiseOverlay = null; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs index 640cd4a95..2347db53c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs @@ -1,11 +1,14 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; +using System.Collections.Generic; namespace Barotrauma { abstract partial class MapEntityPrefab : IPrefab, IDisposable { + public readonly Dictionary> UpgradeOverrideSprites = new Dictionary>(); + public virtual void UpdatePlacing(Camera cam) { if (PlayerInput.SecondaryMouseButtonClicked()) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index a8b74b8b5..fb552b9e6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -94,7 +94,23 @@ namespace Barotrauma editingHUD = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.25f), GUI.Canvas, Anchor.CenterRight) { MinSize = new Point(400, 0) }) { UserData = this }; GUIListBox listBox = new GUIListBox(new RectTransform(new Vector2(0.95f, 0.8f), editingHUD.RectTransform, Anchor.Center), style: null); var editor = new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, titleFont: GUI.LargeFont); - + + 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, + Selected = RemoveIfLinkedOutpostDoorInUse, + ToolTip = TextManager.Get("sp.structure.removeiflinkedoutpostdoorinuse.description"), + OnSelected = (tickBox) => + { + RemoveIfLinkedOutpostDoorInUse = tickBox.Selected; + return true; + } + }; + editor.AddCustomContent(tickBox, 1); + } + var buttonContainer = new GUILayoutGroup(new RectTransform(new Point(listBox.Content.Rect.Width, heightScaled)), isHorizontal: true) { Stretch = true, @@ -261,7 +277,7 @@ namespace Barotrauma SpriteEffects oldEffects = Prefab.BackgroundSprite.effects; Prefab.BackgroundSprite.effects ^= SpriteEffects; - Point backGroundOffset = new Point( + Vector2 backGroundOffset = new Vector2( MathUtils.PositiveModulo((int)-textureOffset.X, Prefab.BackgroundSprite.SourceRect.Width), MathUtils.PositiveModulo((int)-textureOffset.Y, Prefab.BackgroundSprite.SourceRect.Height)); @@ -299,7 +315,7 @@ namespace Barotrauma { if (damageEffect != null) { - float newCutoff = MathHelper.Lerp(0.0f, 0.65f, Sections[i].damage / Prefab.Health); + float newCutoff = MathHelper.Lerp(0.0f, 0.65f, Sections[i].damage / MaxHealth); if (Math.Abs(newCutoff - Submarine.DamageEffectCutoff) > 0.01f || color != Submarine.DamageEffectColor) { @@ -314,7 +330,7 @@ namespace Barotrauma } } - Point sectionOffset = new Point( + Vector2 sectionOffset = new Vector2( Math.Abs(rect.Location.X - Sections[i].rect.Location.X), Math.Abs(rect.Location.Y - Sections[i].rect.Location.Y)); @@ -371,7 +387,7 @@ namespace Barotrauma { var textPos = SectionPosition(i, true); textPos.Y = -textPos.Y; - GUI.DrawString(spriteBatch, textPos, "Damage: " + (int)((GetSection(i).damage / Health) * 100f) + "%", Color.Yellow); + GUI.DrawString(spriteBatch, textPos, "Damage: " + (int)((GetSection(i).damage / MaxHealth) * 100f) + "%", Color.Yellow); } } } @@ -448,7 +464,7 @@ namespace Barotrauma for (int i = 0; i < sectionCount; i++) { - float damage = msg.ReadRangedSingle(0.0f, 1.0f, 8) * Health; + float damage = msg.ReadRangedSingle(0.0f, 1.0f, 8) * MaxHealth; if (i < Sections.Length) { SetDamage(i, damage); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index cd717dad2..5a97c0294 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -5,6 +5,7 @@ using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; +using System.Collections; using System.Collections.Generic; using Barotrauma.IO; using System.Linq; @@ -306,9 +307,9 @@ namespace Barotrauma public static void DrawGrid(SpriteBatch spriteBatch, int gridCells, Vector2 gridCenter, Vector2 roundedGridCenter, float alpha = 1.0f) { - var horizontalLine = GUI.Style.GetComponentStyle("HorizontalLine").Sprites[GUIComponent.ComponentState.None].First(); - var verticalLine = GUI.Style.GetComponentStyle("VerticalLine").Sprites[GUIComponent.ComponentState.None].First(); - + var horizontalLine = GUI.Style.GetComponentStyle("HorizontalLine").GetDefaultSprite(); + var verticalLine = GUI.Style.GetComponentStyle("VerticalLine").GetDefaultSprite(); + Vector2 topLeft = roundedGridCenter - Vector2.One * GridSize * gridCells / 2; Vector2 bottomRight = roundedGridCenter + Vector2.One * GridSize * gridCells / 2; @@ -324,19 +325,19 @@ namespace Barotrauma float expandY = MathHelper.Lerp(30.0f, 0.0f, normalizedDistY); GUI.DrawLine(spriteBatch, - horizontalLine.Sprite, + horizontalLine, new Vector2(topLeft.X - expandX, -bottomRight.Y + i * GridSize.Y), new Vector2(bottomRight.X + expandX, -bottomRight.Y + i * GridSize.Y), Color.White * (1.0f - normalizedDistY) * alpha, depth: 0.6f, width: 3); GUI.DrawLine(spriteBatch, - verticalLine.Sprite, + verticalLine, new Vector2(topLeft.X + i * GridSize.X, -topLeft.Y + expandY), new Vector2(topLeft.X + i * GridSize.X, -bottomRight.Y - expandY), Color.White * (1.0f - normalizedDistX) * alpha, depth: 0.6f, width: 3); } } - public void CreateMiniMap(GUIComponent parent, IEnumerable pointsOfInterest = null) + public void CreateMiniMap(GUIComponent parent, IEnumerable pointsOfInterest = null, bool ignoreOutpost = false) { Rectangle worldBorders = GetDockedBorders(); worldBorders.Location += WorldPosition.ToPoint(); @@ -354,7 +355,8 @@ namespace Barotrauma foreach (Hull hull in Hull.hullList) { - if (hull.Submarine != this && !DockedTo.Contains(hull.Submarine)) continue; + if (hull.Submarine != this && !(DockedTo.Contains(hull.Submarine))) continue; + if (ignoreOutpost && !IsEntityFoundOnThisSub(hull, true)) { continue; } Vector2 relativeHullPos = new Vector2( (hull.WorldRect.X - worldBorders.X) / (float)worldBorders.Width, @@ -393,35 +395,61 @@ namespace Barotrauma errorMsgs.Add(TextManager.Get("NoHullsWarning")); } - foreach (Item item in Item.ItemList) + if (Info.Type != SubmarineType.OutpostModule || + (Info.OutpostModuleInfo?.ModuleFlags.Any(f => !f.Equals("hallwayvertical", StringComparison.OrdinalIgnoreCase) && !f.Equals("hallwayhorizontal", StringComparison.OrdinalIgnoreCase)) ?? true)) { - if (item.GetComponent() == null) continue; - - if (!item.linkedTo.Any()) + if (!WayPoint.WayPointList.Any(wp => wp.ShouldBeSaved && wp.SpawnType == SpawnType.Path)) { - errorMsgs.Add(TextManager.Get("DisconnectedVentsWarning")); - break; + errorMsgs.Add(TextManager.Get("NoWaypointsWarning")); } } - if (!WayPoint.WayPointList.Any(wp => wp.ShouldBeSaved && wp.SpawnType == SpawnType.Human)) + if (Info.Type == SubmarineType.Player) { - errorMsgs.Add(TextManager.Get("NoHumanSpawnpointWarning")); - } + foreach (Item item in Item.ItemList) + { + if (item.GetComponent() == null) { continue; } + if (!item.linkedTo.Any()) + { + errorMsgs.Add(TextManager.Get("DisconnectedVentsWarning")); + break; + } + } - if (!WayPoint.WayPointList.Any(wp => wp.ShouldBeSaved && wp.SpawnType == SpawnType.Path)) - { - errorMsgs.Add(TextManager.Get("NoWaypointsWarning")); + if (!WayPoint.WayPointList.Any(wp => wp.ShouldBeSaved && wp.SpawnType == SpawnType.Human)) + { + errorMsgs.Add(TextManager.Get("NoHumanSpawnpointWarning")); + } + if (WayPoint.WayPointList.Find(wp => wp.SpawnType == SpawnType.Cargo) == null) + { + errorMsgs.Add(TextManager.Get("NoCargoSpawnpointWarning")); + } + if (!Item.ItemList.Any(it => it.GetComponent() != null && it.HasTag("ballast"))) + { + errorMsgs.Add(TextManager.Get("NoBallastTagsWarning")); + } } - - if (WayPoint.WayPointList.Find(wp => wp.SpawnType == SpawnType.Cargo) == null) + else if (Info.Type == SubmarineType.OutpostModule) { - errorMsgs.Add(TextManager.Get("NoCargoSpawnpointWarning")); - } - - if (!Item.ItemList.Any(it => it.GetComponent() != null && it.HasTag("ballast"))) - { - errorMsgs.Add(TextManager.Get("NoBallastTagsWarning")); + foreach (Item item in Item.ItemList) + { + var junctionBox = item.GetComponent(); + if (junctionBox == null) { continue; } + int doorLinks = + item.linkedTo.Count(lt => lt is Gap || (lt is Item it2 && it2.GetComponent() != null)) + + Item.ItemList.Count(it2 => it2.linkedTo.Contains(item) && !item.linkedTo.Contains(it2)); + for (int i = 0; i < item.Connections.Count; i++) + { + int wireCount = item.Connections[i].Wires.Count(w => w != null); + if (doorLinks + wireCount > Connection.MaxLinked) + { + errorMsgs.Add(TextManager.GetWithVariables("InsufficientFreeConnectionsWarning", + new string[] { "[doorcount]", "[freeconnectioncount]" }, + new string[] { doorLinks.ToString(), (Connection.MaxLinked - wireCount).ToString() })); + break; + } + } + } } if (Gap.GapList.Any(g => g.linkedTo.Count == 0)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs index a168a3952..6ef20f966 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs @@ -1,17 +1,13 @@ using Microsoft.Xna.Framework; using System; -using System.Collections.Generic; -using Barotrauma.IO; using System.Linq; -using System.Text; -using System.Xml.Linq; namespace Barotrauma { partial class SubmarineInfo : IDisposable { public Sprite PreviewImage; - + partial void InitProjectSpecific() { string previewImageData = SubmarineElement.GetAttributeString("previewimage", ""); @@ -57,78 +53,10 @@ namespace Barotrauma ScrollBarVisible = true, Spacing = 5 }; + + ScalableFont font = parent.Rect.Width < 350 ? GUI.SmallFont : GUI.Font; - //space - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.03f), descriptionBox.Content.RectTransform), style: null); - - new GUITextBlock(new RectTransform(new Vector2(1, 0), descriptionBox.Content.RectTransform), TextManager.Get("submarine.name." + Name, true) ?? Name, font: GUI.LargeFont, wrap: true) { ForceUpperCase = true, CanBeFocused = false }; - - float leftPanelWidth = 0.6f; - float rightPanelWidth = 0.4f / leftPanelWidth; - - ScalableFont font = descriptionBox.Rect.Width < 350 ? GUI.SmallFont : GUI.Font; - - 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() }); - - var dimensionsText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), descriptionBox.Content.RectTransform), - TextManager.Get("Dimensions"), textAlignment: Alignment.TopLeft, font: font, wrap: true) - { CanBeFocused = false }; - new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), dimensionsText.RectTransform, Anchor.TopRight, Pivot.TopLeft), - dimensionsStr, textAlignment: Alignment.TopLeft, font: font, wrap: true) - { CanBeFocused = false }; - dimensionsText.RectTransform.MinSize = new Point(0, dimensionsText.Children.First().Rect.Height); - } - - if (RecommendedCrewSizeMax > 0) - { - var crewSizeText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), descriptionBox.Content.RectTransform), - TextManager.Get("RecommendedCrewSize"), textAlignment: Alignment.TopLeft, font: font, wrap: true) - { CanBeFocused = false }; - new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), crewSizeText.RectTransform, Anchor.TopRight, Pivot.TopLeft), - RecommendedCrewSizeMin + " - " + RecommendedCrewSizeMax, textAlignment: Alignment.TopLeft, font: font, wrap: true) - { CanBeFocused = false }; - crewSizeText.RectTransform.MinSize = new Point(0, crewSizeText.Children.First().Rect.Height); - } - - if (!string.IsNullOrEmpty(RecommendedCrewExperience)) - { - var crewExperienceText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), descriptionBox.Content.RectTransform), - TextManager.Get("RecommendedCrewExperience"), textAlignment: Alignment.TopLeft, font: font, wrap: true) - { CanBeFocused = false }; - new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), crewExperienceText.RectTransform, Anchor.TopRight, Pivot.TopLeft), - TextManager.Get(RecommendedCrewExperience), textAlignment: Alignment.TopLeft, font: font, wrap: true) - { CanBeFocused = false }; - crewExperienceText.RectTransform.MinSize = new Point(0, crewExperienceText.Children.First().Rect.Height); - } - - if (RequiredContentPackages.Any()) - { - var contentPackagesText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), descriptionBox.Content.RectTransform), - TextManager.Get("RequiredContentPackages"), textAlignment: Alignment.TopLeft, font: font) - { CanBeFocused = false }; - new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), contentPackagesText.RectTransform, Anchor.TopRight, Pivot.TopLeft), - string.Join(", ", RequiredContentPackages), textAlignment: Alignment.TopLeft, font: font, wrap: true) - { CanBeFocused = false }; - contentPackagesText.RectTransform.MinSize = new Point(0, contentPackagesText.Children.First().Rect.Height); - } - - // show what game version the submarine was created on - if (!IsVanillaSubmarine() && GameVersion != null) - { - var versionText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), descriptionBox.Content.RectTransform), - TextManager.Get("serverlistversion"), textAlignment: Alignment.TopLeft, font: font, wrap: true) - { CanBeFocused = false }; - new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), versionText.RectTransform, Anchor.TopRight, Pivot.TopLeft), - GameVersion.ToString(), textAlignment: Alignment.TopLeft, font: font, wrap: true) - { CanBeFocused = false }; - - versionText.RectTransform.MinSize = new Point(0, versionText.Children.First().Rect.Height); - } - - GUITextBlock.AutoScaleAndNormalize(descriptionBox.Content.Children.Where(c => c is GUITextBlock).Cast()); + CreateSpecsWindow(descriptionBox, font); //space new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), descriptionBox.Content.RectTransform), style: null); @@ -145,5 +73,84 @@ namespace Barotrauma CanBeFocused = false }; } + + public void CreateSpecsWindow(GUIListBox parent, ScalableFont font) + { + float leftPanelWidth = 0.6f; + float rightPanelWidth = 0.4f / leftPanelWidth; + string className = !HasTag(SubmarineTag.Shuttle) ? TextManager.Get($"submarineclass.{SubmarineClass}") : TextManager.Get("shuttle"); + + int nameHeight = (int)GUI.LargeFont.MeasureString(DisplayName, true).Y; + int classHeight = (int)GUI.SubHeadingFont.MeasureString(className).Y; + int leftPanelWidthInt = (int)(parent.Rect.Width * leftPanelWidth); + + var submarineNameText = new GUITextBlock(new RectTransform(new Point(leftPanelWidthInt, nameHeight + HUDLayoutSettings.Padding / 2), parent.Content.RectTransform), DisplayName, textAlignment: Alignment.CenterLeft, font: GUI.LargeFont) { CanBeFocused = false }; + submarineNameText.RectTransform.MinSize = new Point(0, (int)submarineNameText.TextSize.Y); + var submarineClassText = new GUITextBlock(new RectTransform(new Point(leftPanelWidthInt, classHeight), parent.Content.RectTransform), className, textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont) { CanBeFocused = false }; + submarineClassText.RectTransform.MinSize = new Point(0, (int)submarineClassText.TextSize.Y); + + 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() }); + + 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 }; + new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), dimensionsText.RectTransform, Anchor.TopRight, Pivot.TopLeft), + dimensionsStr, textAlignment: Alignment.TopLeft, font: font, wrap: true) + { CanBeFocused = false }; + dimensionsText.RectTransform.MinSize = new Point(0, dimensionsText.Children.First().Rect.Height); + } + + if (RecommendedCrewSizeMax > 0) + { + var crewSizeText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), parent.Content.RectTransform), + TextManager.Get("RecommendedCrewSize"), textAlignment: Alignment.TopLeft, font: font, wrap: true) + { CanBeFocused = false }; + new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), crewSizeText.RectTransform, Anchor.TopRight, Pivot.TopLeft), + RecommendedCrewSizeMin + " - " + RecommendedCrewSizeMax, textAlignment: Alignment.TopLeft, font: font, wrap: true) + { CanBeFocused = false }; + crewSizeText.RectTransform.MinSize = new Point(0, crewSizeText.Children.First().Rect.Height); + } + + if (!string.IsNullOrEmpty(RecommendedCrewExperience)) + { + var crewExperienceText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), parent.Content.RectTransform), + TextManager.Get("RecommendedCrewExperience"), textAlignment: Alignment.TopLeft, font: font, wrap: true) + { CanBeFocused = false }; + new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), crewExperienceText.RectTransform, Anchor.TopRight, Pivot.TopLeft), + TextManager.Get(RecommendedCrewExperience), textAlignment: Alignment.TopLeft, font: font, wrap: true) + { CanBeFocused = false }; + crewExperienceText.RectTransform.MinSize = new Point(0, crewExperienceText.Children.First().Rect.Height); + } + + if (RequiredContentPackages.Any()) + { + var contentPackagesText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), parent.Content.RectTransform), + TextManager.Get("RequiredContentPackages"), textAlignment: Alignment.TopLeft, font: font) + { CanBeFocused = false }; + new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), contentPackagesText.RectTransform, Anchor.TopRight, Pivot.TopLeft), + string.Join(", ", RequiredContentPackages), textAlignment: Alignment.TopLeft, font: font, wrap: true) + { CanBeFocused = false }; + contentPackagesText.RectTransform.MinSize = new Point(0, contentPackagesText.Children.First().Rect.Height); + } + + // show what game version the submarine was created on + if (!IsVanillaSubmarine() && GameVersion != null) + { + var versionText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), parent.Content.RectTransform), + TextManager.Get("serverlistversion"), textAlignment: Alignment.TopLeft, font: font, wrap: true) + { CanBeFocused = false }; + new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), versionText.RectTransform, Anchor.TopRight, Pivot.TopLeft), + GameVersion.ToString(), textAlignment: Alignment.TopLeft, font: font, wrap: true) + { CanBeFocused = false }; + + versionText.RectTransform.MinSize = new Point(0, versionText.Children.First().Rect.Height); + } + + submarineNameText.AutoScaleHorizontal = true; + GUITextBlock.AutoScaleAndNormalize(parent.Content.Children.Where(c => c is GUITextBlock && c != submarineNameText).Cast()); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs index 8baa8eb61..483989f52 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; +using System.Linq; namespace Barotrauma { @@ -22,7 +23,6 @@ namespace Barotrauma get { return !IsHidden(); } } - public override void Draw(SpriteBatch spriteBatch, bool editing, bool back = true) { if (!editing && (!GameMain.DebugDraw || Screen.Selected.Cam.Zoom < 0.1f)) { return; } @@ -37,7 +37,7 @@ namespace Barotrauma public void Draw(SpriteBatch spriteBatch, Vector2 drawPos) { - Color clr = currentHull == null ? Color.CadetBlue : GUI.Style.Green; + Color clr = CurrentHull == null ? Color.DodgerBlue : GUI.Style.Green; if (spawnType != SpawnType.Path) { clr = Color.Gray; } if (isObstructed) { @@ -46,7 +46,7 @@ namespace Barotrauma if (IsHighlighted || IsHighlighted) { clr = Color.Lerp(clr, Color.White, 0.8f); } int iconSize = spawnType == SpawnType.Path ? WaypointSize : SpawnPointSize; - if (ConnectedGap != null || Ladders != null || Stairs != null || SpawnType != SpawnType.Path) { iconSize = (int)(iconSize * 1.5f); } + if (ConnectedDoor != null || Ladders != null || Stairs != null || SpawnType != SpawnType.Path) { iconSize = (int)(iconSize * 1.5f); } if (IsSelected || IsHighlighted) { @@ -83,6 +83,20 @@ namespace Barotrauma new Vector2(e.DrawPosition.X, -e.DrawPosition.Y), (isObstructed ? Color.Gray : GUI.Style.Green) * 0.7f, width: 5, depth: 0.002f); } + if (ConnectedGap != null) + { + GUI.DrawLine(spriteBatch, + drawPos, + new Vector2(ConnectedGap.WorldPosition.X, -ConnectedGap.WorldPosition.Y), + GUI.Style.Green * 0.5f, width: 1); + } + if (Ladders != null) + { + GUI.DrawLine(spriteBatch, + drawPos, + new Vector2(Ladders.Item.WorldPosition.X, -Ladders.Item.WorldPosition.Y), + GUI.Style.Green * 0.5f, width: 1); + } GUI.SmallFont.DrawString(spriteBatch, ID.ToString(), @@ -117,7 +131,7 @@ namespace Barotrauma editingHUD = CreateEditingHUD(); } - if (IsSelected && PlayerInput.PrimaryMouseButtonClicked()) + if (IsSelected && PlayerInput.PrimaryMouseButtonClicked() && GUI.MouseOn == null) { Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition); @@ -144,6 +158,7 @@ namespace Barotrauma } else { + FindHull(); // Update gaps, ladders, and stairs UpdateLinkedEntity(position, Gap.GapList, gap => ConnectedGap = gap, gap => { @@ -170,6 +185,7 @@ namespace Barotrauma } } }, inflate: 5); + FindStairs(); // TODO: Cannot check the rectangle, since the rectangle is not rotated -> Need to use the collider. //var stairList = mapEntityList.Where(me => me is Structure s && s.StairDirection != Direction.None).Select(me => me as Structure); //UpdateLinkedEntity(position, stairList, s => @@ -240,7 +256,16 @@ namespace Barotrauma textBox.Deselect(); return true; } - + + private bool EnterTags(GUITextBox textBox, string text) + { + tags = text.Split(',').ToList(); + textBox.Text = string.Join(",", Tags); + textBox.Flash(GUI.Style.Green); + textBox.Deselect(); + return true; + } + private bool TextBoxChanged(GUITextBox textBox, string text) { textBox.Color = GUI.Style.Red; @@ -298,7 +323,7 @@ namespace Barotrauma var descText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), TextManager.Get("IDCardDescription"), font: GUI.SmallFont); - GUITextBox propertyBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), descText.RectTransform, Anchor.CenterRight), idCardDesc) + GUITextBox propertyBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), descText.RectTransform, Anchor.CenterRight), IdCardDesc) { MaxTextLength = 150, OnEnterPressed = EnterIDCardDesc, @@ -306,9 +331,9 @@ namespace Barotrauma }; propertyBox.OnTextChanged += TextBoxChanged; - var tagsText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), + var idCardTagsText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), TextManager.Get("IDCardTags"), font: GUI.SmallFont); - propertyBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), tagsText.RectTransform, Anchor.CenterRight), string.Join(", ", idCardTags)) + propertyBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), idCardTagsText.RectTransform, Anchor.CenterRight), string.Join(", ", idCardTags)) { MaxTextLength = 60, OnEnterPressed = EnterIDCardTags, @@ -327,7 +352,7 @@ namespace Barotrauma ToolTip = TextManager.Get("SpawnpointJobsTooltip"), OnSelected = (selected, userdata) => { - assignedJob = userdata as JobPrefab; + AssignedJob = userdata as JobPrefab; return true; } }; @@ -336,7 +361,17 @@ namespace Barotrauma { jobDropDown.AddItem(jobPrefab.Name, jobPrefab); } - jobDropDown.SelectItem(assignedJob); + jobDropDown.SelectItem(AssignedJob); + + var tagsText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), + TextManager.Get("spawnpointtags"), font: GUI.SmallFont); + propertyBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), tagsText.RectTransform, Anchor.CenterRight), string.Join(", ", tags)) + { + MaxTextLength = 60, + OnEnterPressed = EnterTags, + ToolTip = TextManager.Get("spawnpointtagstooltip") + }; + propertyBox.OnTextChanged += TextBoxChanged; } PositionEditingHUD(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs index 10541b14b..f6db39bdc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs @@ -19,6 +19,7 @@ namespace Barotrauma.Networking ChatMessageType type = (ChatMessageType)msg.ReadByte(); PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None; string txt = ""; + string styleSetting = string.Empty; if (type != ChatMessageType.Order) { @@ -38,59 +39,65 @@ namespace Barotrauma.Networking } } - if (type == ChatMessageType.ServerMessageBox) + switch (type) { - txt = TextManager.GetServerMessage(txt); - } - else if (type == ChatMessageType.Order) - { - int orderIndex = msg.ReadByte(); - UInt16 targetCharacterID = msg.ReadUInt16(); - Character targetCharacter = Entity.FindEntityByID(targetCharacterID) as Character; - Entity targetEntity = Entity.FindEntityByID(msg.ReadUInt16()); - int optionIndex = msg.ReadByte(); + case ChatMessageType.Default: + break; + case ChatMessageType.Order: + int orderIndex = msg.ReadByte(); + UInt16 targetCharacterID = msg.ReadUInt16(); + Character targetCharacter = Entity.FindEntityByID(targetCharacterID) as Character; + Entity targetEntity = Entity.FindEntityByID(msg.ReadUInt16()); + int optionIndex = msg.ReadByte(); - Order order = null; - if (orderIndex < 0 || orderIndex >= Order.PrefabList.Count) - { - DebugConsole.ThrowError("Invalid order message - order index out of bounds."); - if (NetIdUtils.IdMoreRecent(ID, LastID)) LastID = ID; + Order order = null; + if (orderIndex < 0 || orderIndex >= Order.PrefabList.Count) + { + DebugConsole.ThrowError("Invalid order message - order index out of bounds."); + if (NetIdUtils.IdMoreRecent(ID, LastID)) LastID = ID; + return; + } + else + { + order = Order.PrefabList[orderIndex]; + } + string orderOption = ""; + if (optionIndex >= 0 && optionIndex < order.Options.Length) + { + orderOption = order.Options[optionIndex]; + } + txt = order.GetChatMessage(targetCharacter?.Name, senderCharacter?.CurrentHull?.DisplayName, givingOrderToSelf: targetCharacter == senderCharacter, orderOption: orderOption); + + if (GameMain.Client.GameStarted && Screen.Selected == GameMain.GameScreen) + { + if (order.TargetAllCharacters) + { + GameMain.GameSession?.CrewManager?.AddOrder( + new Order(order.Prefab, targetEntity, (targetEntity as Item)?.Components.FirstOrDefault(ic => ic.GetType() == order.ItemComponentType), orderGiver: senderCharacter), + order.Prefab.FadeOutTime); + } + else if (targetCharacter != null) + { + targetCharacter.SetOrder( + new Order(order.Prefab, targetEntity, (targetEntity as Item)?.Components.FirstOrDefault(ic => ic.GetType() == order.ItemComponentType), orderGiver: senderCharacter), + orderOption, senderCharacter); + } + } + + if (NetIdUtils.IdMoreRecent(ID, LastID)) + { + GameMain.Client.AddChatMessage( + new OrderChatMessage(order, orderOption, txt, targetEntity, targetCharacter, senderCharacter)); + LastID = ID; + } return; - } - else - { - order = Order.PrefabList[orderIndex]; - } - string orderOption = ""; - if (optionIndex >= 0 && optionIndex < order.Options.Length) - { - orderOption = order.Options[optionIndex]; - } - txt = order.GetChatMessage(targetCharacter?.Name, senderCharacter?.CurrentHull?.DisplayName, givingOrderToSelf: targetCharacter == senderCharacter, orderOption: orderOption); - - if (GameMain.Client.GameStarted && Screen.Selected == GameMain.GameScreen) - { - if (order.TargetAllCharacters) - { - GameMain.GameSession?.CrewManager?.AddOrder( - new Order(order.Prefab, targetEntity, (targetEntity as Item)?.Components.FirstOrDefault(ic => ic.GetType() == order.ItemComponentType), orderGiver: senderCharacter), - order.Prefab.FadeOutTime); - } - else if (targetCharacter != null) - { - targetCharacter.SetOrder( - new Order(order.Prefab, targetEntity, (targetEntity as Item)?.Components.FirstOrDefault(ic => ic.GetType() == order.ItemComponentType), orderGiver: senderCharacter), - orderOption, senderCharacter); - } - } - - if (NetIdUtils.IdMoreRecent(ID, LastID)) - { - GameMain.Client.AddChatMessage( - new OrderChatMessage(order, orderOption, txt, targetEntity, targetCharacter, senderCharacter)); - LastID = ID; - } - return; + case ChatMessageType.ServerMessageBox: + txt = TextManager.GetServerMessage(txt); + break; + case ChatMessageType.ServerMessageBoxInGame: + styleSetting = msg.ReadString(); + txt = TextManager.GetServerMessage(txt); + break; } if (NetIdUtils.IdMoreRecent(ID, LastID)) @@ -105,6 +112,9 @@ namespace Barotrauma.Networking new GUIMessageBox("", txt); } break; + case ChatMessageType.ServerMessageBoxInGame: + new GUIMessageBox("", txt, new string[0], type: GUIMessageBox.Type.InGame, iconStyle: styleSetting); + break; case ChatMessageType.Console: DebugConsole.NewMessage(txt, MessageColor[(int)ChatMessageType.Console]); break; @@ -120,7 +130,7 @@ namespace Barotrauma.Networking break; } LastID = ID; - } + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs index 7a254152f..993d9f289 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs @@ -18,6 +18,7 @@ namespace Barotrauma.Networking public bool Muted; public bool InGame; public bool HasPermissions; + public bool IsOwner; public bool AllowKicking; } @@ -44,6 +45,8 @@ namespace Barotrauma.Networking } } + public bool IsOwner; + public bool AllowKicking; public float Karma; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs index 22a91415b..5de8e14c5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs @@ -121,7 +121,7 @@ namespace Barotrauma.Networking if (GameSettings.VerboseLogging) { - DebugConsole.Log("Received " + all.Length + " bytes of the file " + FileName + " (" + Received + "/" + FileSize + " received)"); + DebugConsole.Log($"Received {all.Length} bytes of the file {FileName} ({Received / 1000}/{FileSize / 1000} kB received)"); } BytesPerSecond = Received / psec; @@ -332,17 +332,14 @@ namespace Barotrauma.Networking } int offset = inc.ReadInt32(); + int bytesToRead = inc.ReadUInt16(); if (offset != activeTransfer.Received) { - if (offset < activeTransfer.Received) - { - GameMain.Client.UpdateFileTransfer(activeTransfer.ID, activeTransfer.Received); - } + DebugConsole.Log($"Received {bytesToRead} bytes of the file {activeTransfer.FileName} (ignoring: offset {offset}, waiting for {activeTransfer.Received})"); + GameMain.Client.UpdateFileTransfer(activeTransfer.ID, activeTransfer.Received); return; } - int bytesToRead = inc.ReadUInt16(); - if (activeTransfer.Received + bytesToRead > activeTransfer.FileSize) { GameMain.Client.CancelFileTransfer(transferId); @@ -358,7 +355,7 @@ namespace Barotrauma.Networking try { - activeTransfer.ReadBytes(inc, bytesToRead); + activeTransfer.ReadBytes(inc, bytesToRead); } catch (Exception e) { @@ -369,9 +366,9 @@ namespace Barotrauma.Networking return; } + GameMain.Client.UpdateFileTransfer(activeTransfer.ID, activeTransfer.Received, reliable: activeTransfer.Status == FileTransferStatus.Finished); if (activeTransfer.Status == FileTransferStatus.Finished) { - GameMain.Client.UpdateFileTransfer(activeTransfer.ID, activeTransfer.Received, true); activeTransfer.Dispose(); if (ValidateReceivedData(activeTransfer, out string errorMessage)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 87983a95b..581a504b7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +using System.Xml.Linq; namespace Barotrauma.Networking { @@ -56,7 +57,9 @@ namespace Barotrauma.Networking protected GUITickBox cameraFollowsSub; - public RoundEndCinematic EndCinematic; + public CameraTransition EndCinematic; + + public bool LateCampaignJoin = false; private ClientPermissions permissions = ClientPermissions.None; private List permittedConsoleCommands = new List(); @@ -80,7 +83,7 @@ namespace Barotrauma.Networking private List otherClients; - private readonly List serverSubmarines = new List(); + public readonly List ServerSubmarines = new List(); private string serverIP, serverName; @@ -158,7 +161,7 @@ namespace Barotrauma.Networking { get { return ownerKey > 0 || steamP2POwner; } } - + public GameClient(string newName, string ip, UInt64 steamId, string serverName = null, int ownerKey = 0, bool steamP2POwner = false) { //TODO: gui stuff should probably not be here? @@ -262,6 +265,7 @@ namespace Barotrauma.Networking //ServerLog = new ServerLog(""); ChatMessage.LastID = 0; + GameMain.NetLobbyScreen?.Release(); GameMain.NetLobbyScreen = new NetLobbyScreen(); } @@ -282,7 +286,7 @@ namespace Barotrauma.Networking } serverName = hostName; - + myCharacter = Character.Controlled; ChatMessage.LastID = 0; @@ -366,11 +370,15 @@ namespace Barotrauma.Networking }; clientPeer.OnRequestPassword = (int salt, int retries) => { - if (pwRetries != retries) { requiresPw = true; } + if (pwRetries != retries) + { + wrongPassword = retries > 0; + requiresPw = true; + } pwRetries = retries; }; clientPeer.OnMessageReceived = ReadDataMessage; - + // Connect client, to endpoint previously requested from user try { @@ -392,7 +400,7 @@ namespace Barotrauma.Networking updateInterval = new TimeSpan(0, 0, 0, 0, 150); CoroutineManager.StartCoroutine(WaitForStartingInfo(), "WaitForStartingInfo"); - } + } private bool ReturnToPreviousMenu(GUIButton button, object obj) { @@ -414,7 +422,7 @@ namespace Barotrauma.Networking return true; } - + private bool connectCancelled; private void CancelConnect() { @@ -423,6 +431,8 @@ namespace Barotrauma.Networking Disconnect(); } + private bool wrongPassword; + // Before main looping starts, we loop here and wait for approval message private IEnumerable WaitForStartingInfo() { @@ -430,12 +440,12 @@ namespace Barotrauma.Networking requiresPw = false; pwRetries = -1; - connectCancelled = false; + connectCancelled = wrongPassword = false; // When this is set to true, we are approved and ready to go canStart = false; - DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 20); - DateTime reqAuthTime = DateTime.Now + new TimeSpan(0, 0, 0, 0, 200); + DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 40); + DateTime reqAuthTime = DateTime.Now + new TimeSpan(0, 0, 0, 0, 200); // Loop until we are approved string connectingText = TextManager.Get("Connecting"); @@ -482,7 +492,7 @@ namespace Barotrauma.Networking reconnectBox?.Close(); reconnectBox = null; break; } - + if (requiresPw && !canStart && !connectCancelled) { GUI.ClearCursorWait(); @@ -499,6 +509,12 @@ namespace Barotrauma.Networking Censor = true }; + if (wrongPassword) + { + new GUITextBlock(new RectTransform(new Vector2(1f, 0.33f), passwordHolder.RectTransform), TextManager.Language == "English" ? TextManager.Get("incorrectpassword") : "Incorrect password", GUI.Style.Red, GUI.Font, textAlignment: Alignment.Center); + passwordHolder.Recalculate(); + } + msgBox.Content.Recalculate(); msgBox.Content.RectTransform.MinSize = new Point(0, msgBox.Content.RectTransform.Children.Sum(c => c.Rect.Height)); msgBox.Content.Parent.RectTransform.MinSize = new Point(0, (int)(msgBox.Content.RectTransform.MinSize.Y / msgBox.Content.RectTransform.RelativeSize.Y)); @@ -537,7 +553,7 @@ namespace Barotrauma.Networking GUI.ClearCursorWait(); if (connectCancelled) { yield return CoroutineStatus.Success; } - + yield return CoroutineStatus.Success; } @@ -613,7 +629,7 @@ namespace Barotrauma.Networking if (gameStarted && Screen.Selected == GameMain.GameScreen) { - EndVoteTickBox.Visible = serverSettings.Voting.AllowEndVoting && HasSpawned; + EndVoteTickBox.Visible = serverSettings.Voting.AllowEndVoting && HasSpawned && !(GameMain.GameSession?.GameMode is CampaignMode); if (respawnManager != null) { @@ -665,10 +681,14 @@ namespace Barotrauma.Networking { ServerPacketHeader header = (ServerPacketHeader)inc.ReadByte(); - if (header != ServerPacketHeader.STARTGAMEFINALIZE && + if (roundInitStatus != RoundInitStatus.Started && + roundInitStatus != RoundInitStatus.NotStarted && + roundInitStatus != RoundInitStatus.Error && + roundInitStatus != RoundInitStatus.Interrupted && + header != ServerPacketHeader.STARTGAMEFINALIZE && header != ServerPacketHeader.ENDGAME && header != ServerPacketHeader.PING_REQUEST && - roundInitStatus == RoundInitStatus.WaitingForStartGameFinalize) + header != ServerPacketHeader.FILE_TRANSFER) { //rewind the header byte we just read inc.BitPosition -= 8; @@ -676,6 +696,9 @@ namespace Barotrauma.Networking return; } + MultiPlayerCampaign campaign = GameMain.NetLobbyScreen.SelectedMode == GameMain.GameSession?.GameMode.Preset ? + GameMain.GameSession?.GameMode as MultiPlayerCampaign : null; + switch (header) { case ServerPacketHeader.PING_REQUEST: @@ -683,7 +706,7 @@ namespace Barotrauma.Networking response.Write((byte)ClientPacketHeader.PING_RESPONSE); byte requestLen = inc.ReadByte(); response.Write(requestLen); - for (int i=0;i c.FileType == FileTransferType.CampaignSave) && - (campaign.LastSaveID == campaign.PendingSaveID); + readyToStart = + campaign != null && + campaign.CampaignID == campaignID && + campaign.LastSaveID == campaignSaveID && + campaign.LastUpdateID == campaignUpdateID; } readyToStartMsg.Write(readyToStart); + DebugConsole.Log(readyToStart ? "Ready to start." : "Not ready to start."); + WriteCharacterInfo(readyToStartMsg); - + clientPeer.Send(readyToStartMsg, DeliveryMethod.Reliable); if (readyToStart && !CoroutineManager.IsCoroutineRunning("WaitForStartRound")) @@ -778,16 +809,37 @@ namespace Barotrauma.Networking } break; case ServerPacketHeader.STARTGAME: - GameMain.Instance.ShowLoading(StartGame(inc), false); + DebugConsole.Log("Received STARTGAME packet."); + if (Screen.Selected == GameMain.GameScreen && GameMain.GameSession?.GameMode is CampaignMode) + { + //start without a loading screen if playing a campaign round + CoroutineManager.StartCoroutine(StartGame(inc)); + } + else + { + GUIMessageBox.CloseAll(); + GameMain.Instance.ShowLoading(StartGame(inc), false); + } break; case ServerPacketHeader.STARTGAMEFINALIZE: + DebugConsole.Log("Received STARTGAMEFINALIZE packet."); if (roundInitStatus == RoundInitStatus.WaitingForStartGameFinalize) { + //waiting for a save file + if (campaign != null && + campaign.PendingSaveID > campaign.LastSaveID && + fileReceiver.ActiveTransfers.Any(t => t.FileType == FileTransferType.CampaignSave)) + { + return; + } ReadStartGameFinalize(inc); } break; case ServerPacketHeader.ENDGAME: - string endMessage = inc.ReadString(); + CampaignMode.TransitionType transitionType = (CampaignMode.TransitionType)inc.ReadByte(); + string endMessage = string.Empty; + + endMessage = inc.ReadString(); bool missionSuccessful = inc.ReadBoolean(); Character.TeamType winningTeam = (Character.TeamType)inc.ReadByte(); if (missionSuccessful && GameMain.GameSession?.Mission != null) @@ -796,8 +848,31 @@ namespace Barotrauma.Networking GameMain.GameSession.Mission.Completed = true; } + byte traitorCount = inc.ReadByte(); + List traitorResults = new List(); + for (int i = 0; i clientUpgrades = UpgradeManager.GetMetadataLevels(mpCampaign.CampaignMetadata); + Dictionary serverUpgrades = new Dictionary(); + + int length = inc.ReadUInt16(); + for (int i = 0; i < length; i++) + { + serverUpgrades.Add(inc.ReadString(), inc.ReadByte()); + } + UpgradeManager.CompareUpgrades(clientUpgrades, serverUpgrades); + } + } + roundInitStatus = RoundInitStatus.Interrupted; - CoroutineManager.StartCoroutine(EndGame(endMessage), "EndGame"); + CoroutineManager.StartCoroutine(EndGame(endMessage, traitorResults, transitionType), "EndGame"); break; case ServerPacketHeader.CAMPAIGN_SETUP_INFO: UInt16 saveCount = inc.ReadUInt16(); @@ -836,6 +911,12 @@ namespace Barotrauma.Networking } } break; + case ServerPacketHeader.RESET_UPGRADES: + campaign?.UpgradeManager.ClientRead(inc); + break; + case ServerPacketHeader.CREW: + campaign?.ClientReadCrew(inc); + break; case ServerPacketHeader.FILE_TRANSFER: fileReceiver.ReadMessage(inc); break; @@ -843,13 +924,17 @@ namespace Barotrauma.Networking ReadTraitorMessage(inc); break; case ServerPacketHeader.MISSION: - GameMain.GameSession.Mission?.ClientRead(inc); + GameMain.GameSession?.Mission?.ClientRead(inc); + break; + case ServerPacketHeader.EVENTACTION: + GameMain.GameSession?.EventManager.ClientRead(inc); break; } } private void ReadStartGameFinalize(IReadMessage inc) { + TaskPool.ListTasks(null); ushort contentToPreloadCount = inc.ReadUInt16(); List contentToPreload = new List(); for (int i = 0; i < contentToPreloadCount; i++) @@ -861,16 +946,59 @@ namespace Barotrauma.Networking GameMain.GameSession.EventManager.PreloadContent(contentToPreload); - int levelEqualityCheckVal = inc.ReadInt32(); - - if (Level.Loaded.EqualityCheckVal != levelEqualityCheckVal) + int subEqualityCheckValue = inc.ReadInt32(); + if (subEqualityCheckValue != (Submarine.MainSub?.Info?.EqualityCheckVal ?? 0)) { - string errorMsg = "Level equality check failed. The level generated at your end doesn't match the level generated by the server (seed: " + Level.Loaded.Seed + + string errorMsg = "Submarine equality check failed. The submarine loaded at your end doesn't match the one loaded by the server." + + " There may have been an error in receiving the up-to-date submarine file from the server."; + GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:SubsDontMatch" + Level.Loaded.Seed, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + throw new Exception(errorMsg); + } + + string missionIdentifier = inc.ReadString() ?? ""; + if (missionIdentifier != (GameMain.GameSession.Mission?.Prefab.Identifier ?? "")) + { + string errorMsg = $"Mission equality check failed. The mission selected at your end doesn't match the one loaded by the server (server: {missionIdentifier ?? "null"}, client: {GameMain.GameSession.Mission?.Prefab.Identifier ?? ""})"; + GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:MissionsDontMatch" + Level.Loaded.Seed, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + throw new Exception(errorMsg); + } + + byte equalityCheckValueCount = inc.ReadByte(); + List levelEqualityCheckValues = new List(); + for (int i = 0; i 0) + if (splitMsg.Length > 0) { if (Enum.TryParse(splitMsg[0], out disconnectReason)) { disconnectReasonIncluded = true; } } @@ -913,8 +1041,8 @@ namespace Barotrauma.Networking disconnectReason != DisconnectReason.InvalidVersion) { GameAnalyticsManager.AddErrorEventOnce( - "GameClient.HandleDisconnectMessage", - GameAnalyticsSDK.Net.EGAErrorSeverity.Debug, + "GameClient.HandleDisconnectMessage", + GameAnalyticsSDK.Net.EGAErrorSeverity.Debug, "Client received a disconnect message. Reason: " + disconnectReason.ToString() + ", message: " + disconnectMsg); } @@ -953,18 +1081,18 @@ namespace Barotrauma.Networking CoroutineManager.StopCoroutines("WaitInServerQueue"); } - bool eventSyncError = + bool eventSyncError = disconnectReason == DisconnectReason.ExcessiveDesyncOldEvent || disconnectReason == DisconnectReason.ExcessiveDesyncRemovedEvent || disconnectReason == DisconnectReason.SyncTimeout; - if (allowReconnect && + if (allowReconnect && (disconnectReason == DisconnectReason.Unknown || eventSyncError)) { if (eventSyncError) { GameMain.NetLobbyScreen.Select(); - GameMain.GameSession?.EndRound(""); + GameMain.GameSession?.EndRound("", null); gameStarted = false; myCharacter = null; } @@ -972,7 +1100,7 @@ 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); + string msg = TextManager.GetServerMessage(disconnectReasonIncluded ? string.Join('/', splitMsg.Skip(1)) : disconnectMsg); msg = string.IsNullOrWhiteSpace(msg) ? TextManager.Get("ConnectionLostReconnecting") : msg + '\n' + TextManager.Get("ConnectionLostReconnecting"); @@ -992,14 +1120,14 @@ namespace Barotrauma.Networking string msg = ""; if (disconnectReason == DisconnectReason.Unknown) { - DebugConsole.NewMessage("Do not attempt reconnect (not allowed)."); + DebugConsole.NewMessage("Not attempting to reconnect (unknown disconnect reason)."); msg = disconnectMsg; } else { - DebugConsole.NewMessage("Do not attempt to reconnect (DisconnectReason doesn't allow reconnection)."); + DebugConsole.NewMessage("Not attempting to reconnect (DisconnectReason doesn't allow reconnection)."); msg = TextManager.Get("DisconnectReason." + disconnectReason.ToString()); - + for (int i = 1; i < splitMsg.Length; i++) { msg += TextManager.GetServerMessage(splitMsg[i]); @@ -1080,10 +1208,10 @@ namespace Barotrauma.Networking var missionPrefab = TraitorMissionPrefab.List.Find(t => t.Identifier == missionIdentifier); Sprite icon = missionPrefab?.Icon; - switch(messageType) + switch (messageType) { case TraitorMessageType.Objective: - var isTraitor = !string.IsNullOrEmpty(message); + var isTraitor = !string.IsNullOrEmpty(message); SpawnAsTraitor = isTraitor; TraitorFirstObjective = message; TraitorMission = missionPrefab; @@ -1139,6 +1267,14 @@ namespace Barotrauma.Networking if (newPermissions == permissions) return; } + bool refreshCampaignUI = false; + + if (permissions.HasFlag(ClientPermissions.ManageCampaign) != newPermissions.HasFlag(ClientPermissions.ManageCampaign) || + permissions.HasFlag(ClientPermissions.ManageRound) != newPermissions.HasFlag(ClientPermissions.ManageRound)) + { + refreshCampaignUI = true; + } + permissions = newPermissions; this.permittedConsoleCommands = new List(permittedConsoleCommands); //don't show the "permissions changed" popup if the client owns the server @@ -1186,7 +1322,7 @@ namespace Barotrauma.Networking CanBeFocused = false }; } - permissionsLabel.RectTransform.NonScaledSize = commandsLabel.RectTransform.NonScaledSize = + permissionsLabel.RectTransform.NonScaledSize = commandsLabel.RectTransform.NonScaledSize = new Point(permissionsLabel.Rect.Width, Math.Max(permissionsLabel.Rect.Height, commandsLabel.Rect.Height)); commandsLabel.RectTransform.IsFixedSize = true; } @@ -1196,7 +1332,7 @@ namespace Barotrauma.Networking OnClicked = msgBox.Close }; - permissionArea.RectTransform.MinSize = new Point(0, Math.Max( leftColumn.RectTransform.Children.Sum(c => c.Rect.Height), rightColumn.RectTransform.Children.Sum(c => c.Rect.Height))); + permissionArea.RectTransform.MinSize = new Point(0, Math.Max(leftColumn.RectTransform.Children.Sum(c => c.Rect.Height), rightColumn.RectTransform.Children.Sum(c => c.Rect.Height))); permissionArea.RectTransform.IsFixedSize = true; int contentHeight = (int)(msgBox.Content.RectTransform.Children.Sum(c => c.Rect.Height + msgBox.Content.AbsoluteSpacing) * 1.05f); msgBox.Content.ChildAnchor = Anchor.TopCenter; @@ -1205,12 +1341,22 @@ namespace Barotrauma.Networking msgBox.InnerFrame.RectTransform.MinSize = new Point(0, (int)(contentHeight / permissionArea.RectTransform.RelativeSize.Y / msgBox.Content.RectTransform.RelativeSize.Y)); } - GameMain.NetLobbyScreen.UpdatePermissions(); + if (refreshCampaignUI) + { + if (GameMain.GameSession?.GameMode is CampaignMode campaign) + { + campaign.CampaignUI?.UpgradeStore?.RefreshAll(); + campaign.CampaignUI?.CrewManagement?.RefreshPermissions(); + } + } + + GameMain.NetLobbyScreen.RefreshEnabledElements(); } private IEnumerable StartGame(IReadMessage inc) { - if (Character != null) Character.Remove(); + Character?.Remove(); + Character = null; HasSpawned = false; eventErrorWritten = false; GameMain.NetLobbyScreen.StopWaitingForStartRound(); @@ -1221,8 +1367,6 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Running; } - GameMain.LightManager.LightingEnabled = true; - //enable spectate button in case we fail to start the round now //(for example, due to a missing sub file or an error) GameMain.NetLobbyScreen.ShowSpectateButton(); @@ -1234,57 +1378,41 @@ namespace Barotrauma.Networking roundInitStatus = RoundInitStatus.Starting; - int seed = inc.ReadInt32(); - string levelSeed = inc.ReadString(); - //int levelEqualityCheckVal = inc.ReadInt32(); - float levelDifficulty = inc.ReadSingle(); - - byte losMode = inc.ReadByte(); - - int missionTypeIndex = inc.ReadByte(); - - string subName = inc.ReadString(); - string subHash = inc.ReadString(); - - bool usingShuttle = inc.ReadBoolean(); - string shuttleName = inc.ReadString(); - string shuttleHash = inc.ReadString(); - - string modeIdentifier = inc.ReadString(); - int missionIndex = inc.ReadInt16(); - - bool respawnAllowed = inc.ReadBoolean(); - - bool disguisesAllowed = inc.ReadBoolean(); - bool rewiringAllowed = inc.ReadBoolean(); - - bool allowRagdollButton = inc.ReadBoolean(); - - serverSettings.ReadMonsterEnabled(inc); - - bool includesFinalize = inc.ReadBoolean(); inc.ReadPadBits(); + int seed = inc.ReadInt32(); + string modeIdentifier = inc.ReadString(); GameModePreset gameMode = GameModePreset.List.Find(gm => gm.Identifier == modeIdentifier); - MultiPlayerCampaign campaign = - GameMain.NetLobbyScreen.SelectedMode == GameMain.GameSession?.GameMode.Preset && gameMode == GameMain.NetLobbyScreen.SelectedMode ? - GameMain.GameSession?.GameMode as MultiPlayerCampaign : - null; - if (gameMode == null) { DebugConsole.ThrowError("Game mode \"" + modeIdentifier + "\" not found!"); - yield return CoroutineStatus.Success; + yield return CoroutineStatus.Failure; } - GameMain.NetLobbyScreen.UsingShuttle = usingShuttle; - GameMain.LightManager.LosMode = (LosMode)losMode; + bool respawnAllowed = inc.ReadBoolean(); + serverSettings.AllowDisguises = inc.ReadBoolean(); + serverSettings.AllowRewiring = inc.ReadBoolean(); + serverSettings.AllowRagdollButton = inc.ReadBoolean(); + GameMain.NetLobbyScreen.UsingShuttle = inc.ReadBoolean(); + GameMain.LightManager.LosMode = (LosMode)inc.ReadByte(); + bool includesFinalize = inc.ReadBoolean(); inc.ReadPadBits(); + GameMain.LightManager.LightingEnabled = true; - serverSettings.AllowDisguises = disguisesAllowed; - serverSettings.AllowRewiring = rewiringAllowed; - serverSettings.AllowRagdollButton = allowRagdollButton; + serverSettings.ReadMonsterEnabled(inc); - if (campaign == null) + Rand.SetSyncedSeed(seed); + + Task loadTask = null; + var roundSummary = (GUIMessageBox.MessageBoxes.Find(c => c?.UserData is RoundSummary)?.UserData) as RoundSummary; + + if (gameMode != GameModePreset.MultiPlayerCampaign) { + string levelSeed = inc.ReadString(); + float levelDifficulty = inc.ReadSingle(); + string subName = inc.ReadString(); + string subHash = inc.ReadString(); + string shuttleName = inc.ReadString(); + string shuttleHash = inc.ReadString(); + int missionIndex = inc.ReadInt16(); if (!GameMain.NetLobbyScreen.TrySelectSub(subName, subHash, GameMain.NetLobbyScreen.SubList)) { yield return CoroutineStatus.Success; @@ -1294,12 +1422,7 @@ namespace Barotrauma.Networking { yield return CoroutineStatus.Success; } - } - Rand.SetSyncedSeed(seed); - - if (campaign == null) - { //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 || @@ -1321,43 +1444,83 @@ namespace Barotrauma.Networking errorMsg += "\n" + "Hash mismatch: " + GameMain.NetLobbyScreen.SelectedSub.MD5Hash?.Hash + " != " + subHash; } } + gameStarted = true; + GameMain.NetLobbyScreen.Select(); DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:FailedToSelectSub" + subName, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); - CoroutineManager.StartCoroutine(EndGame("")); yield return CoroutineStatus.Failure; } if (GameMain.NetLobbyScreen.SelectedShuttle == null || GameMain.NetLobbyScreen.SelectedShuttle.Name != shuttleName || GameMain.NetLobbyScreen.SelectedShuttle.MD5Hash?.Hash != shuttleHash) { + gameStarted = true; + GameMain.NetLobbyScreen.Select(); string errorMsg = "Failed to select shuttle \"" + shuttleName + "\" (hash: " + shuttleHash + ")."; DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:FailedToSelectShuttle" + shuttleName, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); - CoroutineManager.StartCoroutine(EndGame("")); yield return CoroutineStatus.Failure; } - MissionPrefab missionPrefab = missionIndex < 0 ? null : MissionPrefab.List[missionIndex]; - - GameMain.GameSession = missionIndex < 0 ? - new GameSession(GameMain.NetLobbyScreen.SelectedSub, "", gameMode, MissionType.None) : - new GameSession(GameMain.NetLobbyScreen.SelectedSub, "", gameMode, missionPrefab); - - //startRoundTask = Task.Run(async () => { await Task.Yield(); GameMain.GameSession.StartRound(levelSeed, levelDifficulty); }); + GameMain.GameSession = new GameSession(GameMain.NetLobbyScreen.SelectedSub, gameMode, missionPrefab: missionIndex < 0 ? null : MissionPrefab.List[missionIndex]); GameMain.GameSession.StartRound(levelSeed, levelDifficulty); } else { - if (GameMain.GameSession?.CrewManager != null) GameMain.GameSession.CrewManager.Reset(); - /*startRoundTask = Task.Run(async () => + if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign)) { - await Task.Yield(); - GameMain.GameSession.StartRound(campaign.Map.SelectedConnection.Level, - reloadSub: true, - mirrorLevel: campaign.Map.CurrentLocation != campaign.Map.SelectedConnection.Locations[0]); - });*/ - GameMain.GameSession.StartRound(campaign.Map.SelectedConnection.Level, - mirrorLevel: campaign.Map.CurrentLocation != campaign.Map.SelectedConnection.Locations[0]); + throw new InvalidOperationException("Attempted to start a campaign round when a campaign was not active."); + } + + if (GameMain.GameSession?.CrewManager != null) { GameMain.GameSession.CrewManager.Reset(); } + + byte campaignID = inc.ReadByte(); + int nextLocationIndex = inc.ReadInt32(); + int nextConnectionIndex = inc.ReadInt32(); + int selectedLocationIndex = inc.ReadInt32(); + bool mirrorLevel = inc.ReadBoolean(); + + + if (campaign.CampaignID != campaignID) + { + string errorMsg = "Failed to start campaign round (campaign ID does not match)."; + gameStarted = true; + DebugConsole.ThrowError(errorMsg); + GameMain.NetLobbyScreen.Select(); + yield return CoroutineStatus.Failure; + } + else if (campaign.Map == null) + { + string errorMsg = "Failed to start campaign round (campaign map not loaded yet)."; + gameStarted = true; + DebugConsole.ThrowError(errorMsg); + GameMain.NetLobbyScreen.Select(); + yield return CoroutineStatus.Failure; + } + + campaign.Map.SelectLocation(selectedLocationIndex); + + LevelData levelData = nextLocationIndex > -1 ? + campaign.Map.Locations[nextLocationIndex].LevelData : + campaign.Map.Connections[nextConnectionIndex].LevelData; + + if (roundSummary != null) + { + loadTask = campaign.SelectSummaryScreen(roundSummary, levelData, mirrorLevel, null); + roundSummary.ContinueButton.Visible = false; + } + else + { + GameMain.GameSession.StartRound(levelData, mirrorLevel); + } + } + + if (loadTask != null) + { + while (!loadTask.IsCompleted && !loadTask.IsFaulted && !loadTask.IsCanceled) + { + yield return CoroutineStatus.Running; + } } roundInitStatus = RoundInitStatus.WaitingForStartGameFinalize; @@ -1468,6 +1631,11 @@ namespace Barotrauma.Networking gameStarted = true; ServerSettings.ServerDetailsChanged = true; + if (roundSummary != null) + { + roundSummary.ContinueButton.Visible = true; + } + GameMain.GameScreen.Select(); AddChatMessage($"ServerMessage.HowToCommunicate~[chatbutton]={GameMain.Config.KeyBindText(InputType.Chat)}~[radiobutton]={GameMain.Config.KeyBindText(InputType.RadioChat)}", ChatMessageType.Server); @@ -1475,7 +1643,7 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Success; } - public IEnumerable EndGame(string endMessage) + public IEnumerable EndGame(string endMessage, List traitorResults = null, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None) { if (!gameStarted) { @@ -1483,17 +1651,8 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Success; } - if (GameMain.GameSession != null) { GameMain.GameSession.GameMode.End(endMessage); } - - // Enable characters near the main sub for the endCinematic - foreach (Character c in Character.CharacterList) - { - if (Vector2.DistanceSquared(Submarine.MainSub.WorldPosition, c.WorldPosition) < NetConfig.EnableCharacterDistSqr) - { - c.Enabled = true; - } - } - + if (GameMain.GameSession != null) { GameMain.GameSession.EndRound(endMessage, traitorResults, transitionType); } + ServerSettings.ServerDetailsChanged = true; gameStarted = false; @@ -1502,25 +1661,38 @@ namespace Barotrauma.Networking GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; GameMain.LightManager.LosEnabled = false; respawnManager = null; - + if (Screen.Selected == GameMain.GameScreen) { - EndCinematic = new RoundEndCinematic(Submarine.MainSub, GameMain.GameScreen.Cam); + // Enable characters near the main sub for the endCinematic + foreach (Character c in Character.CharacterList) + { + if (Vector2.DistanceSquared(Submarine.MainSub.WorldPosition, c.WorldPosition) < NetConfig.EnableCharacterDistSqr) + { + c.Enabled = true; + } + } + + EndCinematic = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, Alignment.CenterLeft, Alignment.CenterRight); while (EndCinematic.Running && Screen.Selected == GameMain.GameScreen) { yield return CoroutineStatus.Running; } EndCinematic = null; } - + Submarine.Unload(); - GameMain.NetLobbyScreen.Select(); + if (transitionType == CampaignMode.TransitionType.None) + { + GameMain.NetLobbyScreen.Select(); + } myCharacter = null; foreach (Client c in otherClients) { c.InGame = false; c.Character = null; } + yield return CoroutineStatus.Success; } @@ -1529,38 +1701,84 @@ namespace Barotrauma.Networking myID = inc.ReadByte(); UInt16 subListCount = inc.ReadUInt16(); - serverSubmarines.Clear(); + ServerSubmarines.Clear(); for (int i = 0; i < subListCount; i++) { string subName = inc.ReadString(); string subHash = inc.ReadString(); + byte subClass = inc.ReadByte(); bool requiredContentPackagesInstalled = inc.ReadBoolean(); - var matchingSub = - SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.Hash == subHash) ?? - new SubmarineInfo(Path.Combine(SubmarineInfo.SavePath, subName) + ".sub", subHash, tryLoad: false); - + var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.Hash == subHash); + if (matchingSub == null) + { + matchingSub = new SubmarineInfo(Path.Combine(SubmarineInfo.SavePath, subName) + ".sub", subHash, tryLoad: false); + matchingSub.SubmarineClass = (SubmarineClass)subClass; + } matchingSub.RequiredContentPackagesInstalled = requiredContentPackagesInstalled; - serverSubmarines.Add(matchingSub); + ServerSubmarines.Add(matchingSub); } - GameMain.NetLobbyScreen.UpdateSubList(GameMain.NetLobbyScreen.SubList, serverSubmarines); - GameMain.NetLobbyScreen.UpdateSubList(GameMain.NetLobbyScreen.ShuttleList.ListBox, serverSubmarines); + GameMain.NetLobbyScreen.UpdateSubList(GameMain.NetLobbyScreen.SubList, ServerSubmarines); + GameMain.NetLobbyScreen.UpdateSubList(GameMain.NetLobbyScreen.ShuttleList.ListBox, ServerSubmarines); gameStarted = inc.ReadBoolean(); bool allowSpectating = inc.ReadBoolean(); ReadPermissions(inc); - - if (gameStarted && Screen.Selected != GameMain.GameScreen) + + if (gameStarted) { - new GUIMessageBox(TextManager.Get("PleaseWait"), TextManager.Get(allowSpectating ? "RoundRunningSpectateEnabled" : "RoundRunningSpectateDisabled")); - GameMain.NetLobbyScreen.Select(); + 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++) + { + int index; + if (int.TryParse(ownedIndexes[i], out index)) + { + SubmarineInfo sub = GameMain.Client.ServerSubmarines[index]; + if (GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, "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, "owned")) + { + GameMain.NetLobbyScreen.ServerOwnedSubmarines.Add(sub); + } + } + } + } + } + + if (Screen.Selected != GameMain.GameScreen) + { + new GUIMessageBox(TextManager.Get("PleaseWait"), TextManager.Get(allowSpectating ? "RoundRunningSpectateEnabled" : "RoundRunningSpectateDisabled")); + GameMain.NetLobbyScreen.Select(); + } } } private void ReadClientList(IReadMessage inc) { + bool refreshCampaignUI = false; UInt16 listId = inc.ReadUInt16(); List tempClients = new List(); int clientCount = inc.ReadByte(); @@ -1576,6 +1794,7 @@ namespace Barotrauma.Networking bool muted = inc.ReadBoolean(); bool inGame = inc.ReadBoolean(); bool hasPermissions = inc.ReadBoolean(); + bool isOwner = inc.ReadBoolean(); bool allowKicking = inc.ReadBoolean() || IsServerOwner; inc.ReadPadBits(); @@ -1591,6 +1810,7 @@ namespace Barotrauma.Networking Muted = muted, InGame = inGame, HasPermissions = hasPermissions, + IsOwner = isOwner, AllowKicking = allowKicking }); } @@ -1610,9 +1830,11 @@ namespace Barotrauma.Networking SteamID = tc.SteamID, Muted = tc.Muted, InGame = tc.InGame, - AllowKicking = tc.AllowKicking + AllowKicking = tc.AllowKicking, + IsOwner = tc.IsOwner }; ConnectedClients.Add(existingClient); + refreshCampaignUI = true; GameMain.NetLobbyScreen.AddPlayer(existingClient); } existingClient.NameID = tc.NameID; @@ -1622,6 +1844,7 @@ namespace Barotrauma.Networking existingClient.Muted = tc.Muted; existingClient.HasPermissions = tc.HasPermissions; existingClient.InGame = tc.InGame; + existingClient.IsOwner = tc.IsOwner; existingClient.AllowKicking = tc.AllowKicking; GameMain.NetLobbyScreen.SetPlayerNameAndJobPreference(existingClient); if (Screen.Selected != GameMain.NetLobbyScreen && tc.CharacterID > 0) @@ -1652,13 +1875,15 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.RemovePlayer(ConnectedClients[i]); ConnectedClients[i].Dispose(); ConnectedClients.RemoveAt(i); + refreshCampaignUI = true; } } if (updateClientListId) { LastClientListUpdateID = listId; } if (clientPeer is SteamP2POwnerPeer) { - TaskPool.Add(Steamworks.SteamNetworkingUtils.WaitForPingDataAsync(), (task) => + TaskPool.Add("WaitForPingDataAsync (owner)", + Steamworks.SteamNetworkingUtils.WaitForPingDataAsync(), (task) => { Steam.SteamManager.UpdateLobby(serverSettings); }); @@ -1666,6 +1891,15 @@ namespace Barotrauma.Networking Steam.SteamManager.UpdateLobby(serverSettings); } } + + if (refreshCampaignUI) + { + if (GameMain.GameSession?.GameMode is CampaignMode campaign) + { + campaign.CampaignUI?.UpgradeStore?.RefreshAll(); + campaign.CampaignUI?.CrewManagement?.RefreshPermissions(); + } + } } private bool initialUpdateReceived; @@ -1685,7 +1919,7 @@ namespace Barotrauma.Networking { var prevDispatcher = GUI.KeyboardDispatcher.Subscriber; - UInt16 updateID = inc.ReadUInt16(); + UInt16 updateID = inc.ReadUInt16(); UInt16 settingsLen = inc.ReadUInt16(); byte[] settingsData = inc.ReadBytes(settingsLen); @@ -1701,32 +1935,34 @@ namespace Barotrauma.Networking initialUpdateReceived = true; } - string selectSubName = inc.ReadString(); - string selectSubHash = inc.ReadString(); + string selectSubName = inc.ReadString(); + string selectSubHash = inc.ReadString(); - bool usingShuttle = inc.ReadBoolean(); - string selectShuttleName = inc.ReadString(); - string selectShuttleHash = inc.ReadString(); + bool usingShuttle = inc.ReadBoolean(); + string selectShuttleName = inc.ReadString(); + string selectShuttleHash = inc.ReadString(); - bool allowSubVoting = inc.ReadBoolean(); - bool allowModeVoting = inc.ReadBoolean(); + string campaignSubmarineIndexes = inc.ReadString(); - bool voiceChatEnabled = inc.ReadBoolean(); + bool allowSubVoting = inc.ReadBoolean(); + bool allowModeVoting = inc.ReadBoolean(); - bool allowSpectating = inc.ReadBoolean(); + bool voiceChatEnabled = inc.ReadBoolean(); - YesNoMaybe traitorsEnabled = (YesNoMaybe)inc.ReadRangedInteger(0, 2); - MissionType missionType = (MissionType)inc.ReadRangedInteger(0, (int)MissionType.All); - int modeIndex = inc.ReadByte(); + bool allowSpectating = inc.ReadBoolean(); - string levelSeed = inc.ReadString(); - float levelDifficulty = inc.ReadSingle(); + YesNoMaybe traitorsEnabled = (YesNoMaybe)inc.ReadRangedInteger(0, 2); + MissionType missionType = (MissionType)inc.ReadRangedInteger(0, (int)MissionType.All); + int modeIndex = inc.ReadByte(); - byte botCount = inc.ReadByte(); - BotSpawnMode botSpawnMode = inc.ReadBoolean() ? BotSpawnMode.Fill : BotSpawnMode.Normal; + string levelSeed = inc.ReadString(); + float levelDifficulty = inc.ReadSingle(); - bool autoRestartEnabled = inc.ReadBoolean(); - float autoRestartTimer = autoRestartEnabled ? inc.ReadSingle() : 0.0f; + byte botCount = inc.ReadByte(); + BotSpawnMode botSpawnMode = inc.ReadBoolean() ? BotSpawnMode.Fill : BotSpawnMode.Normal; + + bool autoRestartEnabled = inc.ReadBoolean(); + float autoRestartTimer = autoRestartEnabled ? inc.ReadSingle() : 0.0f; //ignore the message if we already a more up-to-date one //or if we're still waiting for the initial update @@ -1757,6 +1993,34 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.SetMissionType(missionType); if (!allowModeVoting) GameMain.NetLobbyScreen.SelectMode(modeIndex); + if (isInitialUpdate && GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign) + { + if (GameMain.Client.IsServerOwner) RequestSelectMode(modeIndex); + } + + if (campaignSubmarineIndexes != null) + { + string[] activeIndexes = campaignSubmarineIndexes.Split(';'); + + GameMain.NetLobbyScreen.CampaignSubmarines = new List(); + for (int i = 0; i < activeIndexes.Length; i++) + { + int index; + if (int.TryParse(activeIndexes[i], out index)) + { + SubmarineInfo sub = GameMain.Client.ServerSubmarines[index]; + if (GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, "campaign")) + { + GameMain.NetLobbyScreen.CampaignSubmarines.Add(sub); + } + } + } + + if (HasPermission(ClientPermissions.ManageCampaign) && !gameStarted && GameMain.NetLobbyScreen?.CampaignSetupUI != null) + { + GameMain.NetLobbyScreen.CampaignSetupUI.RefreshMultiplayerCampaignSubUI(GameMain.NetLobbyScreen.CampaignSubmarines); + } + } GameMain.NetLobbyScreen.SetAllowSpectating(allowSpectating); GameMain.NetLobbyScreen.LevelSeed = levelSeed; @@ -1784,7 +2048,7 @@ namespace Barotrauma.Networking { MultiPlayerCampaign.ClientRead(inc); } - else if (GameMain.NetLobbyScreen.SelectedMode?.Identifier != "multiplayercampaign") + else if (GameMain.NetLobbyScreen.SelectedMode != GameModePreset.MultiPlayerCampaign) { GameMain.NetLobbyScreen.SetCampaignCharacterInfo(null); } @@ -1817,102 +2081,133 @@ namespace Barotrauma.Networking long prevBitLength = 0; long prevByteLength = 0; - ServerNetObject objHeader; - while ((objHeader = (ServerNetObject)inc.ReadByte()) != ServerNetObject.END_OF_MESSAGE) + ServerNetObject? objHeader = null; + try { - bool eventReadFailed = false; - switch (objHeader) + while ((objHeader = (ServerNetObject)inc.ReadByte()) != ServerNetObject.END_OF_MESSAGE) { - case ServerNetObject.SYNC_IDS: - lastSentChatMsgID = inc.ReadUInt16(); - LastSentEntityEventID = inc.ReadUInt16(); - break; - case ServerNetObject.ENTITY_POSITION: - UInt16 id = inc.ReadUInt16(); - uint msgLength = inc.ReadVariableUInt32(); + bool eventReadFailed = false; + switch (objHeader) + { + case ServerNetObject.SYNC_IDS: + lastSentChatMsgID = inc.ReadUInt16(); + LastSentEntityEventID = inc.ReadUInt16(); - int msgEndPos = (int)(inc.BitPosition + msgLength * 8); - - var entity = Entity.FindEntityByID(id) as IServerSerializable; - if (entity != null) - { - entity.ClientRead(objHeader, inc, sendingTime); - } - - //force to the correct position in case the entity doesn't exist - //or the message wasn't read correctly for whatever reason - inc.BitPosition = msgEndPos; - inc.ReadPadBits(); - break; - case ServerNetObject.CLIENT_LIST: - ReadClientList(inc); - break; - case ServerNetObject.ENTITY_EVENT: - case ServerNetObject.ENTITY_EVENT_INITIAL: - if (!entityEventManager.Read(objHeader, inc, sendingTime, entities)) - { - eventReadFailed = true; - break; - } - break; - case ServerNetObject.CHAT_MESSAGE: - ChatMessage.ClientRead(inc); - break; - default: - List errorLines = new List - { - "Error while reading update from server (unknown object header \"" + objHeader + "\"!)", - "Message length: " + inc.LengthBits + " (" + inc.LengthBytes + " bytes)", - prevObjHeader != null ? "Previous object type: " + prevObjHeader.ToString() : "Error occurred on the very first object!", - "Previous object was " + (prevBitLength) + " bits long (" + (prevByteLength) + " bytes)" - }; - if (prevObjHeader == ServerNetObject.ENTITY_EVENT || prevObjHeader == ServerNetObject.ENTITY_EVENT_INITIAL) - { - foreach (IServerSerializable ent in entities) + bool campaignUpdated = inc.ReadBoolean(); + inc.ReadPadBits(); + if (campaignUpdated) { - if (ent == null) - { - errorLines.Add(" - NULL"); - continue; - } - Entity e = ent as Entity; - errorLines.Add(" - " + e.ToString()); + MultiPlayerCampaign.ClientRead(inc); } - } + else if (GameMain.NetLobbyScreen.SelectedMode != GameModePreset.MultiPlayerCampaign) + { + GameMain.NetLobbyScreen.SetCampaignCharacterInfo(null); + } + break; + case ServerNetObject.ENTITY_POSITION: + UInt16 id = inc.ReadUInt16(); + uint msgLength = inc.ReadVariableUInt32(); + int msgEndPos = (int)(inc.BitPosition + msgLength * 8); - foreach (string line in errorLines) - { - DebugConsole.ThrowError(line); - } - errorLines.Add("Last console messages:"); - for (int i = DebugConsole.Messages.Count - 1; i > Math.Max(0, DebugConsole.Messages.Count - 20); i--) - { - errorLines.Add("[" + DebugConsole.Messages[i].Time + "] " + DebugConsole.Messages[i].Text); - } - GameAnalyticsManager.AddErrorEventOnce("GameClient.ReadInGameUpdate", GameAnalyticsSDK.Net.EGAErrorSeverity.Critical, string.Join("\n", errorLines)); + var entity = Entity.FindEntityByID(id) as IServerSerializable; + if (entity != null) + { + entity.ClientRead(objHeader.Value, inc, sendingTime); + } - DebugConsole.ThrowError("Writing object data to \"crashreport_object.bin\", please send this file to us at http://github.com/Regalis11/Barotrauma/issues"); + //force to the correct position in case the entity doesn't exist + //or the message wasn't read correctly for whatever reason + inc.BitPosition = msgEndPos; + inc.ReadPadBits(); + break; + case ServerNetObject.CLIENT_LIST: + ReadClientList(inc); + break; + case ServerNetObject.ENTITY_EVENT: + case ServerNetObject.ENTITY_EVENT_INITIAL: + if (!entityEventManager.Read(objHeader.Value, inc, sendingTime, entities)) + { + eventReadFailed = true; + break; + } + break; + case ServerNetObject.CHAT_MESSAGE: + ChatMessage.ClientRead(inc); + break; + default: + throw new Exception($"Unknown object header \"{objHeader}\"!)"); + } + prevBitLength = inc.BitPosition - prevBitPos; + prevByteLength = inc.BytePosition - prevByteLength; - using (FileStream fl = File.Open("crashreport_object.bin", System.IO.FileMode.Create)) - using (System.IO.BinaryWriter sw = new System.IO.BinaryWriter(fl)) - { - sw.Write(inc.Buffer, (int)(prevBytePos - prevByteLength), (int)(prevByteLength)); - } + prevObjHeader = objHeader; + prevBitPos = inc.BitPosition; + prevBytePos = inc.BytePosition; - throw new Exception("Error while reading update from server: please send us \"crashreport_object.bin\"!"); - } - prevBitLength = inc.BitPosition - prevBitPos; - prevByteLength = inc.BytePosition - prevByteLength; - - prevObjHeader = objHeader; - prevBitPos = inc.BitPosition; - prevBytePos = inc.BytePosition; - - if (eventReadFailed) - { - break; + if (eventReadFailed) + { + break; + } } } + + catch (Exception ex) + { + List errorLines = new List + { + ex.Message, + "Message length: " + inc.LengthBits + " (" + inc.LengthBytes + " bytes)", + "Read position: " + inc.BitPosition, + "Header: " + (objHeader != null ? objHeader.Value.ToString() : "Error occurred on the very first header!"), + prevObjHeader != null ? "Previous header: " + prevObjHeader : "Error occurred on the very first header!", + "Previous object was " + (prevBitLength) + " bits long (" + (prevByteLength) + " bytes)", + " " + }; + errorLines.Add(ex.StackTrace); + errorLines.Add(" "); + if (prevObjHeader == ServerNetObject.ENTITY_EVENT || prevObjHeader == ServerNetObject.ENTITY_EVENT_INITIAL || + objHeader == ServerNetObject.ENTITY_EVENT || objHeader == ServerNetObject.ENTITY_EVENT_INITIAL) + { + foreach (IServerSerializable ent in entities) + { + if (ent == null) + { + errorLines.Add(" - NULL"); + continue; + } + Entity e = ent as Entity; + errorLines.Add(" - " + e.ToString()); + } + } + + foreach (string line in errorLines) + { + DebugConsole.ThrowError(line); + } + errorLines.Add("Last console messages:"); + for (int i = DebugConsole.Messages.Count - 1; i > Math.Max(0, DebugConsole.Messages.Count - 20); i--) + { + errorLines.Add("[" + DebugConsole.Messages[i].Time + "] " + DebugConsole.Messages[i].Text); + } + GameAnalyticsManager.AddErrorEventOnce("GameClient.ReadInGameUpdate", GameAnalyticsSDK.Net.EGAErrorSeverity.Critical, string.Join("\n", errorLines)); + + DebugConsole.ThrowError("Writing object data to \"crashreport_object.log\", please send this file to us at http://github.com/Regalis11/Barotrauma/issues"); + + using (FileStream fl = File.Open("crashreport_object.log", System.IO.FileMode.Create)) + { + using (System.IO.BinaryWriter bw = new System.IO.BinaryWriter(fl)) + using (System.IO.StreamWriter sw = new System.IO.StreamWriter(fl)) + { + bw.Write(inc.Buffer, (int)(prevBytePos - prevByteLength), (int)(prevByteLength)); + sw.WriteLine(""); + foreach (string line in errorLines) + { + sw.WriteLine(line); + } + } + } + throw new Exception("Read error: please send us \"crashreport_object.bin\"!"); + } } private void SendLobbyUpdate() @@ -1936,8 +2231,7 @@ namespace Barotrauma.Networking outmsg.Write(""); } - var campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign; - if (campaign == null || campaign.LastSaveID == 0) + if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0) { outmsg.Write((UInt16)0); } @@ -1982,6 +2276,18 @@ namespace Barotrauma.Networking outmsg.Write(entityEventManager.LastReceivedID); outmsg.Write(LastClientListUpdateID); + if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0) + { + outmsg.Write((UInt16)0); + } + else + { + outmsg.Write(campaign.LastSaveID); + outmsg.Write(campaign.CampaignID); + outmsg.Write(campaign.LastUpdateID); + outmsg.Write(GameMain.NetLobbyScreen.CampaignCharacterDiscarded); + } + Character.Controlled?.ClientWrite(outmsg); GameMain.GameScreen.Cam?.ClientWrite(outmsg); @@ -2048,7 +2354,7 @@ namespace Barotrauma.Networking CancelFileTransfer(transfer.ID); } - public void UpdateFileTransfer(int id, int offset, bool reliable=false) + public void UpdateFileTransfer(int id, int offset, bool reliable = false) { IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ClientPacketHeader.FILE_REQUEST); @@ -2094,7 +2400,17 @@ namespace Barotrauma.Networking ((SubmarineInfo)c.UserData).MD5Hash.Hash == newSub.MD5Hash.Hash); if (subElement == null) continue; - subElement.GetChild().TextColor = new Color(subElement.GetChild().TextColor, 1.0f); + Color newSubTextColor = new Color(subElement.GetChild().TextColor, 1.0f); + subElement.GetChild().TextColor = newSubTextColor; + + GUITextBlock classTextBlock = subElement.GetChildByUserData("classtext") as GUITextBlock; + if (classTextBlock != null) + { + Color newSubClassTextColor = new Color(classTextBlock.TextColor, 0.8f); + classTextBlock.Text = TextManager.Get($"submarineclass.{newSub.SubmarineClass}"); + classTextBlock.TextColor = newSubClassTextColor; + } + subElement.UserData = newSub; subElement.ToolTip = newSub.Description; } @@ -2113,28 +2429,67 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.TrySelectSub(newSub.Name, newSub.MD5Hash.Hash, GameMain.NetLobbyScreen.ShuttleList.ListBox); } + Pair failedCampaignSub = GameMain.NetLobbyScreen.FailedCampaignSubs.Find(s => s.First == newSub.Name && s.Second == newSub.MD5Hash.Hash); + if (failedCampaignSub != null) + { + GameMain.NetLobbyScreen.CampaignSubmarines.Add(newSub); + GameMain.NetLobbyScreen.FailedCampaignSubs.Remove(failedCampaignSub); + } + + Pair failedOwnedSub = GameMain.NetLobbyScreen.FailedOwnedSubs.Find(s => s.First == newSub.Name && s.Second == newSub.MD5Hash.Hash); + if (failedOwnedSub != null) + { + GameMain.NetLobbyScreen.ServerOwnedSubmarines.Add(newSub); + GameMain.NetLobbyScreen.FailedOwnedSubs.Remove(failedOwnedSub); + } + + // Replace a submarine dud with the downloaded version + SubmarineInfo existingServerSub = ServerSubmarines.Find(s => s.Name == newSub.Name && s.MD5Hash?.Hash == newSub.MD5Hash?.Hash); + if (existingServerSub != null) + { + int existingIndex = ServerSubmarines.IndexOf(existingServerSub); + ServerSubmarines.RemoveAt(existingIndex); + ServerSubmarines.Insert(existingIndex, newSub); + existingServerSub.Dispose(); + } + break; case FileTransferType.CampaignSave: - var campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign; - if (campaign == null) { return; } + XDocument gameSessionDoc = SaveUtil.LoadGameSessionDoc(transfer.FilePath); + byte campaignID = (byte)MathHelper.Clamp(gameSessionDoc.Root.GetAttributeInt("campaignid", 0), 0, 255); + if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.CampaignID != campaignID) + { + string savePath = transfer.FilePath; + GameMain.GameSession = new GameSession(null, savePath, GameModePreset.MultiPlayerCampaign); + campaign = (MultiPlayerCampaign)GameMain.GameSession.GameMode; + campaign.CampaignID = campaignID; + GameMain.NetLobbyScreen.ToggleCampaignMode(true); + } GameMain.GameSession.SavePath = transfer.FilePath; - if (GameMain.GameSession.SubmarineInfo == null) + if (GameMain.GameSession.SubmarineInfo == null || campaign.Map == null) { - var gameSessionDoc = SaveUtil.LoadGameSessionDoc(GameMain.GameSession.SavePath); string subPath = Path.Combine(SaveUtil.TempPath, gameSessionDoc.Root.GetAttributeString("submarine", "")) + ".sub"; GameMain.GameSession.SubmarineInfo = new SubmarineInfo(subPath, ""); } - SaveUtil.LoadGame(GameMain.GameSession.SavePath, GameMain.GameSession); + + campaign.LoadState(GameMain.GameSession.SavePath); GameMain.GameSession?.SubmarineInfo?.Reload(); GameMain.GameSession?.SubmarineInfo?.CheckSubsLeftBehind(); + if (GameMain.GameSession?.SubmarineInfo?.Name != null) { GameMain.NetLobbyScreen.TryDisplayCampaignSubmarine(GameMain.GameSession.SubmarineInfo); } campaign.LastSaveID = campaign.PendingSaveID; - DebugConsole.Log("Campaign save received, save ID " + campaign.LastSaveID); + if (Screen.Selected == GameMain.NetLobbyScreen) + { + //reselect to refrest the state of the lobby screen (enable spectate button, etc) + GameMain.NetLobbyScreen.Select(); + } + + DebugConsole.Log("Campaign save received (" + GameMain.GameSession.SavePath + "), save ID " + campaign.LastSaveID); //decrement campaign update ID so the server will send us the latest data //(as there may have been campaign updates after the save file was created) campaign.LastUpdateID--; @@ -2223,6 +2578,7 @@ namespace Barotrauma.Networking VoipClient?.Dispose(); VoipClient = null; GameMain.Client = null; + GameMain.GameSession = null; } public void WriteCharacterInfo(IWriteMessage msg) @@ -2266,6 +2622,25 @@ namespace Barotrauma.Networking Vote(VoteType.Kick, votedClient); } + #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; + } + 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); + } + #endregion + public override void AddChatMessage(ChatMessage message) { base.AddChatMessage(message); @@ -2358,14 +2733,13 @@ namespace Barotrauma.Networking /// /// Tell the server to start the round (permission required) /// - public void RequestStartRound() + public void RequestStartRound(bool continueCampaign = false) { - if (!HasPermission(ClientPermissions.ManageRound)) return; - IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ClientPacketHeader.SERVER_COMMAND); msg.Write((UInt16)ClientPermissions.ManageRound); msg.Write(false); //indicates round start + msg.Write(continueCampaign); clientPeer.Send(msg, DeliveryMethod.Reliable); } @@ -2388,6 +2762,7 @@ namespace Barotrauma.Networking IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ClientPacketHeader.SERVER_COMMAND); msg.Write((UInt16)ClientPermissions.SelectSub); + msg.Write(false); msg.Write(isShuttle); msg.WritePadBits(); msg.Write((UInt16)subIndex); msg.Write((byte)ServerNetObject.END_OF_MESSAGE); @@ -2396,7 +2771,24 @@ namespace Barotrauma.Networking } /// - /// Tell the server to select a submarine (permission required) + /// Tell the server to add / remove a purchasable submarine (permission required) + /// + public void RequestCampaignSub(SubmarineInfo sub, bool add) + { + if (!HasPermission(ClientPermissions.SelectSub) || sub == null) return; + IWriteMessage msg = new WriteOnlyMessage(); + msg.Write((byte)ClientPacketHeader.SERVER_COMMAND); + msg.Write((UInt16)ClientPermissions.SelectSub); + msg.Write(true); + msg.Write(sub.EqualityCheckVal); + msg.Write(add); + msg.Write((byte)ServerNetObject.END_OF_MESSAGE); + + clientPeer.Send(msg, DeliveryMethod.Reliable); + } + + /// + /// Tell the server to select a mode (permission required) /// public void RequestSelectMode(int modeIndex) { @@ -2419,6 +2811,7 @@ namespace Barotrauma.Networking public void SetupNewCampaign(SubmarineInfo sub, string saveName, string mapSeed) { GameMain.NetLobbyScreen.CampaignSetupFrame.Visible = false; + GameMain.NetLobbyScreen.CampaignFrame.Visible = false; saveName = Path.GetFileNameWithoutExtension(saveName); @@ -2437,6 +2830,7 @@ namespace Barotrauma.Networking public void SetupLoadCampaign(string saveName) { GameMain.NetLobbyScreen.CampaignSetupFrame.Visible = false; + GameMain.NetLobbyScreen.CampaignFrame.Visible = false; IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ClientPacketHeader.CAMPAIGN_SETUP_INFO); @@ -2471,6 +2865,8 @@ namespace Barotrauma.Networking return false; } if (button != null) { button.Enabled = false; } + if (campaign != null) LateCampaignJoin = true; + IWriteMessage readyToStartMsg = new WriteOnlyMessage(); readyToStartMsg.Write((byte)ClientPacketHeader.RESPONSE_STARTGAME); @@ -2539,7 +2935,13 @@ namespace Barotrauma.Networking { get { return chatBox; } } - + + public VotingInterface VotingInterface + { + get { return votingInterface; } + } + private VotingInterface votingInterface; + public bool TypingChatMessage(GUITextBox textBox, string text) { return chatBox.TypingChatMessage(textBox, text); @@ -2604,9 +3006,8 @@ namespace Barotrauma.Networking if (gameStarted && Screen.Selected == GameMain.GameScreen) { - bool disableButtons = - Character.Controlled != null && - Character.Controlled.SelectedConstruction?.GetComponent() != null; + var controller = Character.Controlled?.SelectedConstruction?.GetComponent(); + bool disableButtons = Character.Controlled != null && (controller != null && controller.HideHUD); buttonContainer.Visible = !disableButtons; if (!GUI.DisableHUD && !GUI.DisableUpperHUD) @@ -2614,6 +3015,16 @@ namespace Barotrauma.Networking inGameHUD.UpdateManually(deltaTime); chatBox.Update(deltaTime); + if (votingInterface != null) + { + votingInterface.Update(deltaTime); + if (!votingInterface.VoteRunning) + { + votingInterface.Remove(); + votingInterface = null; + } + } + cameraFollowsSub.Visible = Character.Controlled == null; } if (Character.Controlled == null || Character.Controlled.IsDead) @@ -2964,7 +3375,6 @@ namespace Barotrauma.Networking IWriteMessage outMsg = new WriteOnlyMessage(); outMsg.Write((byte)ClientPacketHeader.ERROR); outMsg.Write((byte)error); - outMsg.Write(Level.Loaded == null ? 0 : Level.Loaded.EqualityCheckVal); switch (error) { case ClientNetError.MISSING_EVENT: @@ -3015,6 +3425,7 @@ namespace Barotrauma.Networking errorLines.Add("Campaign ID: " + campaign.CampaignID); errorLines.Add("Campaign save ID: " + campaign.LastSaveID + "(pending: " + campaign.PendingSaveID + ")"); } + errorLines.Add("Mission: " + (GameMain.GameSession?.Mission?.Prefab.Identifier ?? "none")); } if (GameMain.GameSession?.Submarine != null) { @@ -3022,7 +3433,7 @@ namespace Barotrauma.Networking } if (Level.Loaded != null) { - errorLines.Add("Level: " + Level.Loaded.Seed + ", " + Level.Loaded.EqualityCheckVal); + 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) @@ -3033,7 +3444,7 @@ namespace Barotrauma.Networking } errorLines.Add("Entity IDs:"); - List sortedEntities = Entity.GetEntityList(); + List sortedEntities = Entity.GetEntities().ToList(); sortedEntities.Sort((e1, e2) => e1.ID.CompareTo(e2.ID)); foreach (Entity e in sortedEntities) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs index ff72730d9..f2720cc52 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs @@ -4,6 +4,7 @@ using System.Text; using System.Linq; using Barotrauma.Steam; using System.Threading; +using Barotrauma.Items.Components; namespace Barotrauma.Networking { @@ -17,6 +18,7 @@ namespace Barotrauma.Networking private Steamworks.AuthTicket steamAuthTicket; private double timeout; private double heartbeatTimer; + private double connectionStatusTimer; private long sentBytes, receivedBytes; @@ -53,6 +55,9 @@ namespace Barotrauma.Networking Steamworks.SteamNetworking.ResetActions(); Steamworks.SteamNetworking.OnP2PSessionRequest = OnIncomingConnection; + Steamworks.SteamNetworking.OnP2PConnectionFailed = OnConnectionFailed; + + Steamworks.SteamNetworking.AllowP2PPacketRelay(true); ServerConnection = new SteamP2PConnection("Server", hostSteamId); @@ -71,6 +76,7 @@ namespace Barotrauma.Networking timeout = NetworkConnection.TimeoutThreshold; heartbeatTimer = 1.0; + connectionStatusTimer = 0.0; isActive = true; } @@ -78,7 +84,25 @@ namespace Barotrauma.Networking private void OnIncomingConnection(Steamworks.SteamId steamId) { if (!isActive) { return; } - if (steamId == hostSteamId) { Steamworks.SteamNetworking.AcceptP2PSessionWithUser(steamId); } + if (steamId == hostSteamId) + { + Steamworks.SteamNetworking.AcceptP2PSessionWithUser(steamId); + } + else if (initializationStep != ConnectionInitialization.Password && + initializationStep != ConnectionInitialization.Success) + { + DebugConsole.ThrowError($"Connection from incorrect SteamID was rejected: "+ + $"expected {SteamManager.SteamIDUInt64ToString(hostSteamId)}," + + $"got {SteamManager.SteamIDUInt64ToString(steamId)}"); + } + } + + private void OnConnectionFailed(Steamworks.SteamId steamId, Steamworks.P2PSessionError error) + { + if (!isActive) { return; } + if (steamId != hostSteamId) { return; } + Close($"SteamP2P connection failed: {error}"); + OnDisconnectMessageReceived?.Invoke($"SteamP2P connection failed: {error}"); } private void OnP2PData(ulong steamId, byte[] data, int dataLength, int channel) @@ -140,6 +164,30 @@ namespace Barotrauma.Networking timeout -= deltaTime; heartbeatTimer -= deltaTime; + if (initializationStep != ConnectionInitialization.Password && + initializationStep != ConnectionInitialization.Success) + { + connectionStatusTimer -= deltaTime; + if (connectionStatusTimer <= 0.0) + { + var state = Steamworks.SteamNetworking.GetP2PSessionState(hostSteamId); + if (state == null) + { + Close("SteamP2P connection could not be established"); + OnDisconnectMessageReceived?.Invoke("SteamP2P connection could not be established"); + } + else + { + if (state?.P2PSessionError != Steamworks.P2PSessionError.None) + { + Close($"SteamP2P error code: {state?.P2PSessionError}"); + OnDisconnectMessageReceived?.Invoke($"SteamP2P error code: {state?.P2PSessionError}"); + } + } + connectionStatusTimer = 1.0f; + } + } + for (int i = 0; i < 100; i++) { if (!Steamworks.SteamNetworking.IsP2PPacketAvailable()) { break; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs index 5bc6e9ec2..dfecea763 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs @@ -70,6 +70,8 @@ namespace Barotrauma.Networking Steamworks.SteamNetworking.OnP2PSessionRequest = OnIncomingConnection; Steamworks.SteamUser.OnValidateAuthTicketResponse += OnAuthChange; + Steamworks.SteamNetworking.AllowP2PPacketRelay(true); + isActive = true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs index 14d2dc31d..38e0fe399 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs @@ -15,7 +15,7 @@ namespace Barotrauma.Networking public string Port; public string QueryPort; - public Steamworks.Data.PingLocation? PingLocation; + public Steamworks.Data.NetPingLocation? PingLocation; public UInt64 LobbyID; public UInt64 OwnerID; public bool OwnerVerified; @@ -60,7 +60,7 @@ namespace Barotrauma.Networking public bool? RespondedToSteamQuery = null; public Steamworks.Friend? SteamFriend; - public Steamworks.ISteamMatchmakingPingResponse MatchmakingPingResponse; + public Steamworks.SteamMatchmakingPingResponse MatchmakingPingResponse; public string GameVersion; public List ContentPackageNames @@ -401,7 +401,7 @@ namespace Barotrauma.Networking return info; } - public void QueryLiveInfo(Action onServerRulesReceived) + public void QueryLiveInfo(Action onServerRulesReceived, Action onQueryDone) { if (!SteamManager.IsInitialized) { return; } @@ -412,7 +412,7 @@ namespace Barotrauma.Networking MatchmakingPingResponse.Cancel(); } - MatchmakingPingResponse = new Steamworks.ISteamMatchmakingPingResponse( + MatchmakingPingResponse = new Steamworks.SteamMatchmakingPingResponse( (server) => { ServerName = server.Name; @@ -423,22 +423,20 @@ namespace Barotrauma.Networking PingChecked = true; Ping = server.Ping; LobbyID = 0; - TaskPool.Add(server.QueryRulesAsync(), + TaskPool.Add("QueryServerRules (QueryLiveInfo)", server.QueryRulesAsync(), (t) => { + onQueryDone(this); if (t.Status == TaskStatus.Faulted) { TaskPool.PrintTaskExceptions(t, "Failed to retrieve rules for " + ServerName); return; } - var rules = t.Result; + var rules = ((Task>)t).Result; SteamManager.AssignServerRulesToServerInfo(rules, this); - CrossThread.RequestExecutionOnMainThread(() => - { - onServerRulesReceived(this); - }); + onServerRulesReceived(this); }); }, () => @@ -456,9 +454,10 @@ namespace Barotrauma.Networking } if (LobbyID == 0) { - TaskPool.Add(SteamFriend?.RequestInfoAsync(), + TaskPool.Add("RequestSteamP2POwnerInfo", SteamFriend?.RequestInfoAsync(), (t) => { + onQueryDone(this); if ((SteamFriend?.IsPlayingThisGame ?? false) && ((SteamFriend?.GameInfo?.Lobby?.Id ?? 0) != 0)) { LobbyID = SteamFriend?.GameInfo?.Lobby?.Id.Value ?? 0; @@ -471,6 +470,10 @@ namespace Barotrauma.Networking } }); } + else + { + onQueryDone(this); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index 22f0dc0a1..54785afba 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -766,6 +766,14 @@ namespace Barotrauma.Networking TextManager.Get("ServerSettingsAllowFriendlyFire")); GetPropertyData("AllowFriendlyFire").AssignGUIComponent(allowFriendlyFire); + var killableNPCs = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), + TextManager.Get("ServerSettingsKillableNPCs")); + GetPropertyData("KillableNPCs").AssignGUIComponent(killableNPCs); + + var destructibleOutposts = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), + TextManager.Get("ServerSettingsDestructibleOutposts")); + GetPropertyData("DestructibleOutposts").AssignGUIComponent(destructibleOutposts); + var allowRewiring = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), TextManager.Get("ServerSettingsAllowRewiring")); GetPropertyData("AllowRewiring").AssignGUIComponent(allowRewiring); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs index d603cf0a1..5ed8d8b6d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs @@ -10,6 +10,7 @@ using RestSharp.Contrib; using System.Xml.Linq; using Color = Microsoft.Xna.Framework.Color; using System.Runtime.InteropServices; +using NLog.Fluent; namespace Barotrauma.Steam { @@ -29,7 +30,7 @@ namespace Barotrauma.Steam if (isInitialized) { DebugConsole.NewMessage("Logged in as " + GetUsername() + " (SteamID " + SteamIDUInt64ToString(GetSteamID()) + ")"); - + popularTags.Clear(); int i = 0; foreach (KeyValuePair commonness in tagCommonness) @@ -37,19 +38,16 @@ namespace Barotrauma.Steam popularTags.Insert(i, commonness.Key); i++; } - - LogSteamworksNetworkingDelegate = LogSteamworksNetworking; - - IntPtr logSteamworksNetworkingPtr = Marshal.GetFunctionPointerForDelegate(LogSteamworksNetworkingDelegate); - Steamworks.SteamNetworkingUtils.SetDebugOutputFunction(Steamworks.Data.DebugOutputType.Everything, logSteamworksNetworkingPtr); } + + Steamworks.SteamNetworkingUtils.OnDebugOutput += LogSteamworksNetworking; } catch (DllNotFoundException) { isInitialized = false; initializationErrors.Add("SteamDllNotFound"); } - catch (Exception) + catch (Exception e) { isInitialized = false; initializationErrors.Add("SteamClientInitFailed"); @@ -70,13 +68,24 @@ namespace Barotrauma.Steam public static bool NetworkingDebugLog = false; - private static Steamworks.Data.FSteamNetworkingSocketsDebugOutput LogSteamworksNetworkingDelegate; - - private static void LogSteamworksNetworking(Steamworks.Data.DebugOutputType nType, string pszMsg) + private static void LogSteamworksNetworking(Steamworks.NetDebugOutput nType, string pszMsg) { - if (NetworkingDebugLog) { DebugConsole.NewMessage($"({nType}) {pszMsg}", Color.Orange); } + 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; + } + } + + private static void UpdateProjectSpecific(float deltaTime) { if (ugcSubscriptionTasks != null) @@ -98,6 +107,34 @@ namespace Barotrauma.Steam } } + 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, @@ -118,10 +155,16 @@ namespace Barotrauma.Steam { if (lobbyState != LobbyState.NotConnected) { return; } lobbyState = LobbyState.Creating; - TaskPool.Add(Steamworks.SteamMatchmaking.CreateLobbyAsync(serverSettings.MaxPlayers + 10), + TaskPool.Add("CreateLobbyAsync", Steamworks.SteamMatchmaking.CreateLobbyAsync(serverSettings.MaxPlayers + 10), (lobby) => { - currentLobby = lobby.Result; + if (lobbyState != LobbyState.Creating) + { + LeaveLobby(); + return; + } + + currentLobby = ((Task)lobby).Result; if (currentLobby == null) { @@ -218,10 +261,10 @@ namespace Barotrauma.Steam lobbyState = LobbyState.Joining; lobbyID = id; - TaskPool.Add(Steamworks.SteamMatchmaking.JoinLobbyAsync(lobbyID), + TaskPool.Add("JoinLobbyAsync", Steamworks.SteamMatchmaking.JoinLobbyAsync(lobbyID), (lobby) => { - currentLobby = lobby.Result; + currentLobby = ((Task)lobby).Result; lobbyState = LobbyState.Joined; lobbyID = (currentLobby?.Id).Value; if (joinServer) @@ -252,9 +295,10 @@ namespace Barotrauma.Steam //TODO: find a better strategy to fetch all lobbies, this is gonna take forever if we actually have 10000 lobbies Steamworks.Data.LobbyQuery lobbyQuery = Steamworks.SteamMatchmaking.CreateLobbyQuery().FilterDistanceWorldwide().WithMaxResults(10000); - TaskPool.Add(Task.Run(async () => + TaskPool.Add("LobbyQueryRequest", lobbyQuery.RequestAsync(), + (t) => { - Steamworks.Data.Lobby[] lobbies = await lobbyQuery.RequestAsync(); + var lobbies = ((Task)t).Result; foreach (var lobby in lobbies) { if (string.IsNullOrEmpty(lobby.GetData("name"))) { continue; } @@ -270,14 +314,8 @@ namespace Barotrauma.Steam AssignLobbyDataToServerInfo(lobby, serverInfo); - CrossThread.RequestExecutionOnMainThread(() => - { - addToServerList(serverInfo); - }); + addToServerList(serverInfo); } - }), - (t) => - { taskDone(); if (t.Status == TaskStatus.Faulted) { @@ -301,7 +339,7 @@ namespace Barotrauma.Steam if (responsive) { - TaskPool.Add(info.QueryRulesAsync(), + TaskPool.Add($"QueryServerRules (GetServers, {info.Name}, {info.Address})", info.QueryRulesAsync(), (t) => { if (t.Status == TaskStatus.Faulted) @@ -310,7 +348,7 @@ namespace Barotrauma.Steam return; } - var rules = t.Result; + var rules = ((Task>)t).Result; AssignServerRulesToServerInfo(rules, serverInfo); CrossThread.RequestExecutionOnMainThread(() => @@ -331,7 +369,7 @@ namespace Barotrauma.Steam serverQuery.OnResponsiveServer += (info) => onServer(info, true); serverQuery.OnUnresponsiveServer += (info) => onServer(info, false); - TaskPool.Add(serverQuery.RunQueryAsync(), + TaskPool.Add("RunServerQuery", serverQuery.RunQueryAsync(), (t) => { serverQuery.Dispose(); @@ -384,7 +422,7 @@ namespace Barotrauma.Steam string pingLocation = lobby.GetData("pinglocation"); if (!string.IsNullOrEmpty(pingLocation)) { - serverInfo.PingLocation = Steamworks.Data.PingLocation.TryParseFromString(pingLocation); + serverInfo.PingLocation = Steamworks.Data.NetPingLocation.TryParseFromString(pingLocation); } bool? getLobbyBool(string key) @@ -570,7 +608,7 @@ namespace Barotrauma.Steam .WithLongDescription(); if (requireTags != null) { query = query.WithTags(requireTags); } - TaskPool.Add(GetWorkshopItemsAsync(query), (task) => { onItemsFound?.Invoke(task.Result); }); + TaskPool.Add("GetSubscribedWorkshopItems", GetWorkshopItemsAsync(query), (task) => { onItemsFound?.Invoke(((Task>)task).Result); }); } public static void GetPopularWorkshopItems(Action> onItemsFound, int amount, List requireTags = null) @@ -582,8 +620,8 @@ namespace Barotrauma.Steam .WithLongDescription(); if (requireTags != null) query.WithTags(requireTags); - TaskPool.Add(GetWorkshopItemsAsync(query, amount, (item) => !item.IsSubscribed), (task) => { - var entries = task.Result; + TaskPool.Add("GetPopularWorkshopItems", GetWorkshopItemsAsync(query, amount, (item) => !item.IsSubscribed), (task) => { + var entries = ((Task>)task).Result; //count the number of each unique tag foreach (var item in entries) @@ -614,7 +652,7 @@ namespace Barotrauma.Steam } popularTags.Insert(i, tagCommonnessKVP.Key); } - onItemsFound?.Invoke(task.Result); + onItemsFound?.Invoke(entries); }); } @@ -628,7 +666,7 @@ namespace Barotrauma.Steam .WithLongDescription(); if (requireTags != null) query.WithTags(requireTags); - TaskPool.Add(GetWorkshopItemsAsync(query), (task) => { onItemsFound?.Invoke(task.Result); }); + TaskPool.Add("GetPublishedWorkshopItems", GetWorkshopItemsAsync(query), (task) => { onItemsFound?.Invoke(((Task>)task).Result); }); } private static Dictionary ugcSubscriptionTasks; @@ -678,7 +716,7 @@ namespace Barotrauma.Steam { string folderPath = Path.GetDirectoryName(contentPackage.Path); if (!Directory.Exists(folderPath)) { Directory.CreateDirectory(folderPath); } - itemEditor = Steamworks.Ugc.Editor.CreateCommunityFile() + itemEditor = Steamworks.Ugc.Editor.NewCommunityFile .WithPublicVisibility() .ForAppId(AppID) .WithContent(folderPath); @@ -700,7 +738,7 @@ namespace Barotrauma.Steam Directory.CreateDirectory("Mods"); Directory.CreateDirectory(dirPath); - itemEditor = Steamworks.Ugc.Editor.CreateCommunityFile() + itemEditor = Steamworks.Ugc.Editor.NewCommunityFile #if DEBUG .WithPrivateVisibility() #else @@ -827,6 +865,11 @@ namespace Barotrauma.Steam 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; + } #if DEBUG item = item?.WithPrivateVisibility(); @@ -944,13 +987,6 @@ namespace Barotrauma.Steam return false; } - if (contentPackage.CorePackage && !contentPackage.ContainsRequiredCorePackageFiles(out List missingContentTypes)) - { - errorMsg = TextManager.GetWithVariables("ContentPackageMissingCoreFiles", new string[2] { "[packagename]", "[missingfiletypes]" }, - new string[2] { contentPackage.Name, string.Join(", ", missingContentTypes) }, new bool[2] { false, true }); - return false; - } - Task newTask = null; lock (modCopiesInProgress) @@ -963,7 +999,8 @@ namespace Barotrauma.Steam modCopiesInProgress.Add(item.Value.Id, newTask); } - TaskPool.Add(newTask, + TaskPool.Add("CopyWorkShopItemAsync", + newTask, contentPackage, (task, cp) => { @@ -975,9 +1012,10 @@ namespace Barotrauma.Steam GameMain.SteamWorkshopScreen?.SetReinstallButtonStatus(item, true, GUI.Style.Red); return; } - if (!string.IsNullOrWhiteSpace(task.Result)) + string errorMsg = ((Task)task).Result; + if (!string.IsNullOrWhiteSpace(errorMsg)) { - DebugConsole.ThrowError($"Failed to copy \"{item?.Title}\": {task.Result}"); + DebugConsole.ThrowError($"Failed to copy \"{item?.Title}\": {errorMsg}"); GameMain.SteamWorkshopScreen?.SetReinstallButtonStatus(item, true, GUI.Style.Red); return; } @@ -1199,7 +1237,7 @@ namespace Barotrauma.Steam { if (cp.CorePackage) { - GameMain.Config.SelectCorePackage(ContentPackage.List.Find(cpp => cpp.CorePackage && !toRemove.Contains(cpp))); + GameMain.Config.AutoSelectCorePackage(toRemove); } else { @@ -1296,6 +1334,14 @@ namespace Barotrauma.Steam { if (!(item?.IsInstalled ?? false)) { return false; } + lock (modCopiesInProgress) + { + if (modCopiesInProgress.ContainsKey(item.Value.Id)) + { + return true; + } + } + if (!Directory.Exists(item?.Directory)) { DebugConsole.ThrowError("Workshop item \"" + item?.Title + "\" has been installed but the install directory cannot be found. Attempting to redownload..."); @@ -1572,7 +1618,7 @@ namespace Barotrauma.Steam if (type == ContentType.Executable || type == ContentType.ServerExecutable) { - exists |= File.Exists(contentFilePath + ".dll"); + exists |= File.Exists(Path.GetFileNameWithoutExtension(contentFilePath) + ".dll"); } if (exists) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs index c73bde30c..6bd5ff3cd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs @@ -52,6 +52,10 @@ namespace Barotrauma.Networking } } + public readonly bool CanDetectDisconnect; + + public bool Disconnected { get; private set; } + public static void Create(string deviceName, UInt16? storedBufferID=null) { if (Instance != null) @@ -71,6 +75,8 @@ namespace Barotrauma.Networking private VoipCapture(string deviceName) : base(GameMain.Client?.ID ?? 0, true, false) { + Disconnected = false; + VoipConfig.SetupEncoding(); //set up capture device @@ -121,6 +127,13 @@ namespace Barotrauma.Networking throw new Exception("Failed to open capture device: " + alError.ToString() + " (AL)"); } + CanDetectDisconnect = Alc.IsExtensionPresent(captureDevice, "ALC_EXT_disconnect"); + alcError = Alc.GetError(captureDevice); + if (alcError != Alc.NoError) + { + throw new Exception("Error determining if disconnect can be detected: " + alcError.ToString()); + } + Alc.CaptureStart(captureDevice); alcError = Alc.GetError(captureDevice); if (alcError != Alc.NoError) @@ -158,9 +171,27 @@ namespace Barotrauma.Networking { Array.Copy(uncompressedBuffer, 0, prevUncompressedBuffer, 0, VoipConfig.BUFFER_SIZE); Array.Clear(uncompressedBuffer, 0, VoipConfig.BUFFER_SIZE); - while (capturing) + while (capturing && !Disconnected) { int alcError; + + if (CanDetectDisconnect) + { + Alc.GetInteger(captureDevice, Alc.EnumConnected, out int isConnected); + alcError = Alc.GetError(captureDevice); + if (alcError != Alc.NoError) + { + throw new Exception("Failed to determine if capture device is connected: " + alcError.ToString()); + } + + if (isConnected == 0) + { + DebugConsole.ThrowError("Capture device has been disconnected. You can select another available device in the settings."); + Disconnected = true; + break; + } + } + Alc.GetInteger(captureDevice, Alc.EnumCaptureSamples, out int sampleCount); alcError = Alc.GetError(captureDevice); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs index 026f4f287..593b51147 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs @@ -112,11 +112,10 @@ namespace Barotrauma switch (voteType) { - case VoteType.Sub: + case VoteType.Sub: SubmarineInfo sub = data as SubmarineInfo; if (sub == null) { return; } - - msg.Write(sub.Name); + msg.Write(sub.EqualityCheckVal); break; case VoteType.Mode: GameModePreset gameMode = data as GameModePreset; @@ -137,6 +136,25 @@ namespace Barotrauma 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; + msg.Write(true); + msg.Write(voteSub.Name); + } + else + { + if (!(data is int)) { return; } + msg.Write(false); + msg.Write((int)data); + } + + break; } msg.WritePadBits(); @@ -183,6 +201,128 @@ namespace Barotrauma } AllowVoteKick = inc.ReadBoolean(); + byte subVoteStateByte = inc.ReadByte(); + VoteState subVoteState = VoteState.None; + try + { + subVoteState = (VoteState)subVoteStateByte; + } + catch (System.Exception e) + { + DebugConsole.ThrowError("Failed to cast vote type \"" + subVoteStateByte + "\"", e); + } + + if (subVoteState != VoteState.None) + { + byte voteTypeByte = inc.ReadByte(); + VoteType voteType = VoteType.Unknown; + + 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 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), 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; + } + } + } + GameMain.NetworkMember.ConnectedClients.ForEach(c => c.SetVote(VoteType.StartRound, false)); byte readyClientCount = inc.ReadByte(); for (int i = 0; i < readyClientCount; i++) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs index ab81a8534..ca333fd99 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs @@ -1,5 +1,6 @@ using Microsoft.Xna.Framework; using System; +using System.Linq; using System.Xml.Linq; namespace Barotrauma.Particles @@ -72,11 +73,25 @@ namespace Barotrauma.Particles Vector2 velocity = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)) * Prefab.VelocityMax; Vector2 endPosition = Prefab.ParticlePrefab.CalculateEndPosition(startPosition, velocity); + Vector2 endSize = Prefab.ParticlePrefab.CalculateEndSize(); + float spriteExtent = 0.0f; + foreach (Sprite sprite in Prefab.ParticlePrefab.Sprites) + { + if (sprite is SpriteSheet spriteSheet) + { + spriteExtent = Math.Max(spriteExtent, Math.Max(spriteSheet.FrameSize.X * endSize.X, spriteSheet.FrameSize.Y * endSize.Y)); + } + else + { + spriteExtent = Math.Max(spriteExtent, Math.Max(sprite.size.X * endSize.X, sprite.size.Y * endSize.Y)); + } + } + bounds = new Rectangle( - (int)Math.Min(bounds.X, endPosition.X - Prefab.DistanceMax), - (int)Math.Min(bounds.Y, endPosition.Y - Prefab.DistanceMax), - (int)Math.Max(bounds.X, endPosition.X + Prefab.DistanceMax), - (int)Math.Max(bounds.Y, endPosition.Y + Prefab.DistanceMax)); + (int)Math.Min(bounds.X, endPosition.X - Prefab.DistanceMax - spriteExtent / 2), + (int)Math.Min(bounds.Y, endPosition.Y - Prefab.DistanceMax - spriteExtent / 2), + (int)Math.Max(bounds.X, endPosition.X + Prefab.DistanceMax + spriteExtent / 2), + (int)Math.Max(bounds.Y, endPosition.Y + Prefab.DistanceMax + spriteExtent / 2)); } bounds = new Rectangle(bounds.X, bounds.Y, bounds.Width - bounds.X, bounds.Height - bounds.Y); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs index e1a6b2b06..adfaeeaa9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs @@ -298,5 +298,11 @@ namespace Barotrauma.Particles //endPos = x + vt + 1/2 * at^2 return startPosition + velocity * LifeTime + 0.5f * VelocityChangeDisplay * LifeTime * LifeTime; } + + public Vector2 CalculateEndSize() + { + //endPos = x + vt + 1/2 * at^2 + return StartSizeMax + 0.5f * SizeChangeMax * LifeTime * LifeTime; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Program.cs b/Barotrauma/BarotraumaClient/ClientSource/Program.cs index e928d9dc1..7d8e24236 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Program.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Program.cs @@ -57,6 +57,8 @@ namespace Barotrauma Game = new GameMain(args); Game.Run(); Game.Dispose(); + + CrossThread.ProcessTasks(); } private static GameMain Game; @@ -69,8 +71,9 @@ namespace Barotrauma CrashDump(Game, "crashreport.log", (Exception)args.ExceptionObject); Game?.Dispose(); } - catch + catch (Exception e) { + Debug.WriteLine(e.Message); //exception handler is broken, we have a serious problem here!! return; } @@ -96,12 +99,17 @@ namespace Barotrauma DebugConsole.DequeueMessages(); - string exePath = System.Reflection.Assembly.GetEntryAssembly().Location; - var md5 = System.Security.Cryptography.MD5.Create(); Md5Hash exeHash = null; - using (var stream = File.OpenRead(exePath)) + try { - exeHash = new Md5Hash(stream); + string exePath = System.Reflection.Assembly.GetEntryAssembly().Location; + var md5 = System.Security.Cryptography.MD5.Create(); + byte[] exeBytes = File.ReadAllBytes(exePath); + exeHash = new Md5Hash(exeBytes); + } + catch + { + //do nothing, generate the rest of the crash report } StringBuilder sb = new StringBuilder(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignEndScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignEndScreen.cs new file mode 100644 index 000000000..cf7190c75 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignEndScreen.cs @@ -0,0 +1,120 @@ +using Barotrauma.Media; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; + +namespace Barotrauma +{ + class CampaignEndScreen : Screen + { + private Video video; + + private readonly CreditsPlayer creditsPlayer; + + private readonly Camera cam; + + public Action OnFinished; + + private string textOverlay; + private float textOverlayTimer; + private Vector2 textOverlaySize; + + public CampaignEndScreen() + { + creditsPlayer = new CreditsPlayer(new RectTransform(Vector2.One, Frame.RectTransform), "Content/Texts/Credits.xml") + { + AutoRestart = false, + ScrollBarEnabled = false, + AllowMouseWheelScroll = false + }; + new GUIButton(new RectTransform(new Vector2(0.1f), creditsPlayer.RectTransform, Anchor.BottomRight, maxSize: new Point(300, 50)) { AbsoluteOffset = new Point(GUI.IntScale(20)) }, + TextManager.Get("close")) + { + OnClicked = (btn, userdata) => + { + creditsPlayer.Scroll = 1.0f; + return true; + } + }; + cam = new Camera(); + } + + public override void Select() + { + base.Select(); + + textOverlay = ToolBox.WrapText(TextManager.Get("campaignend1"), GameMain.GraphicsWidth / 3, GUI.Font); + textOverlaySize = GUI.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); + } + + public override void Deselect() + { + video?.Dispose(); + video = null; + GUI.HideCursor = false; + SoundPlayer.OverrideMusicType = null; + } + + public override void Update(double deltaTime) + { + if (creditsPlayer.Finished) + { + OnFinished?.Invoke(); + SoundPlayer.OverrideMusicType = null; + } + } + + public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) + { + spriteBatch.Begin(); + graphics.Clear(Color.Black); + if (video.IsPlaying) + { + GUI.HideCursor = !GUI.PauseMenuOpen; + spriteBatch.Draw(video.GetTexture(), new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), Color.White); + } + else + { + SoundPlayer.OverrideMusicType = "ending"; + float duration = 20.0f; + float creditsDelay = 3.0f; + if (textOverlayTimer < duration + creditsDelay) + { + float textAlpha; + float fadeInTime = 5.0f, fadeOutTime = 3.0f; + textOverlayTimer += (float)deltaTime; + if (textOverlayTimer < fadeInTime) + { + textAlpha = textOverlayTimer / fadeInTime; + } + else if (textOverlayTimer > duration - fadeOutTime) + { + textAlpha = Math.Min((duration - textOverlayTimer) / fadeOutTime, 1.0f); + } + else + { + textAlpha = 1.0f; + } + GUI.Font.DrawString(spriteBatch, textOverlay, new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) / 2 - textOverlaySize / 2, Color.White * textAlpha); + } + else + { + GUI.HideCursor = false; + creditsPlayer.Visible = true; + } + } + spriteBatch.End(); + + spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); + GUI.Draw(cam, spriteBatch); + spriteBatch.End(); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs index 776304554..176ba9cd0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using Barotrauma.IO; using System.Linq; using System.Xml.Linq; +using System.Globalization; namespace Barotrauma { @@ -14,6 +15,7 @@ namespace Barotrauma private GUIListBox subList; private GUIListBox saveList; + private List subTickBoxes; private GUITextBox saveNameBox, seedBox; @@ -90,8 +92,10 @@ namespace Barotrauma } else // Spacing to fix the multiplayer campaign setup layout { + CreateMultiplayerCampaignSubList(leftColumn.RectTransform); + //spacing - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.25f), leftColumn.RectTransform), style: null); + //new GUIFrame(new RectTransform(new Vector2(1.0f, 0.25f), leftColumn.RectTransform), style: null); } // New game right side @@ -100,13 +104,11 @@ namespace Barotrauma Stretch = true }; - var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.13f), + var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.12f), (isMultiplayer ? leftColumn : rightColumn).RectTransform) { MaxSize = new Point(int.MaxValue, 60) }, childAnchor: Anchor.TopRight); if (!isMultiplayer) { buttonContainer.IgnoreLayoutGroups = true; } - StartButton = new GUIButton(new RectTransform(isMultiplayer ? new Vector2(0.5f, 1.0f) : Vector2.One, - buttonContainer.RectTransform, Anchor.BottomRight) { MaxSize = new Point(350, 60) }, - TextManager.Get("StartCampaignButton"), style: "GUIButtonLarge") + StartButton = new GUIButton(new RectTransform(new Vector2(0.45f, 1f), buttonContainer.RectTransform, Anchor.BottomRight) { MaxSize = new Point(350, 60) }, TextManager.Get("StartCampaignButton")) { OnClicked = (GUIButton btn, object userData) => { @@ -128,6 +130,12 @@ namespace Barotrauma if (GameMain.NetLobbyScreen.SelectedSub == null) { return false; } selectedSub = GameMain.NetLobbyScreen.SelectedSub; } + + if (selectedSub.SubmarineClass == SubmarineClass.Undefined) + { + new GUIMessageBox(TextManager.Get("error"), TextManager.Get("undefinedsubmarineselected")); + return false; + } if (string.IsNullOrEmpty(selectedSub.MD5Hash.Hash)) { @@ -218,6 +226,106 @@ namespace Barotrauma UpdateLoadMenu(saveFiles); } + private void CreateMultiplayerCampaignSubList(RectTransform parent) + { + GUILayoutGroup subHolder = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.725f), parent)) + { + RelativeSpacing = 0.005f, + Stretch = true + }; + + var subLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), subHolder.RectTransform) { MinSize = new Point(0, 25) }, TextManager.Language == "English" ? "Purchasable submarines" : TextManager.Get("workshoplabelsubmarines"), font: GUI.SubHeadingFont); + + var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), subHolder.RectTransform), isHorizontal: true) + { + Stretch = true + }; + var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: 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 subList.Content.Children) + { + if (!(child.UserData is SubmarineInfo sub)) { continue; } + child.Visible = string.IsNullOrEmpty(text) ? true : sub.DisplayName.ToLower().Contains(text.ToLower()); + } + return true; + }; + + subList = new GUIListBox(new RectTransform(Vector2.One, subHolder.RectTransform)); + subTickBoxes = new List(); + + for (int i = 0; i < GameMain.Client.ServerSubmarines.Count; i++) + { + SubmarineInfo sub = GameMain.Client.ServerSubmarines[i]; + + if (!sub.IsCampaignCompatible) continue; + + var frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), subList.Content.RectTransform) { MinSize = new Point(0, 20) }, + style: "ListBoxElement") + { + ToolTip = sub.Description, + UserData = sub + }; + + int buttonSize = (int)(frame.Rect.Height * 0.8f); + + GUITickBox tickBox = new GUITickBox(new RectTransform(new Vector2(0.8f, 1.0f), frame.RectTransform, Anchor.CenterLeft), ToolBox.LimitString(sub.DisplayName, GUI.Font, subList.Content.Rect.Width - 65)) + { + UserData = sub, + OnSelected = (GUITickBox box) => + { + GameMain.Client.RequestCampaignSub(box.UserData as SubmarineInfo, box.Selected); + return true; + } + }; + subTickBoxes.Add(tickBox); + tickBox.Selected = GameMain.NetLobbyScreen.CampaignSubmarines.Contains(sub); + + frame.RectTransform.MinSize = new Point(0, tickBox.RectTransform.MinSize.Y); + + var subTextBlock = tickBox.TextBlock; + + var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == sub.Name && s.MD5Hash?.Hash == sub.MD5Hash?.Hash); + if (matchingSub == null) matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == sub.Name); + + if (matchingSub == null) + { + subTextBlock.TextColor = new Color(subTextBlock.TextColor, 0.5f); + frame.ToolTip = TextManager.Get("SubNotFound"); + } + else if (matchingSub?.MD5Hash == null || matchingSub.MD5Hash?.Hash != sub.MD5Hash?.Hash) + { + subTextBlock.TextColor = new Color(subTextBlock.TextColor, 0.5f); + frame.ToolTip = TextManager.Get("SubDoesntMatch"); + } + + if (!sub.RequiredContentPackagesInstalled) + { + subTextBlock.TextColor = Color.Lerp(subTextBlock.TextColor, Color.DarkRed, 0.5f); + frame.ToolTip = TextManager.Get("ContentPackageMismatch") + "\n\n" + frame.RawToolTip; + } + + var classText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), frame.RectTransform, Anchor.CenterRight), + TextManager.Get($"submarineclass.{sub.SubmarineClass}"), textAlignment: Alignment.CenterRight, font: GUI.SmallFont) + { + TextColor = subTextBlock.TextColor * 0.8f, + ToolTip = subTextBlock.RawToolTip + }; + } + } + + public void RefreshMultiplayerCampaignSubUI(List campaignSubs) + { + for (int i = 0; i < subTickBoxes.Count; i++) + { + subTickBoxes[i].Selected = campaignSubs.Contains(subTickBoxes[i].UserData as SubmarineInfo); + } + } + public void RandomizeSeed() { seedBox.Text = ToolBox.RandomSeed(8); @@ -239,9 +347,15 @@ namespace Barotrauma (subPreviewContainer.Parent as GUILayoutGroup)?.Recalculate(); subPreviewContainer.ClearChildren(); - SubmarineInfo sub = obj as SubmarineInfo; - if (sub == null) { return true; } - + if (!(obj is SubmarineInfo sub)) { return true; } +#if !DEBUG + if (!isMultiplayer && sub.Price > CampaignMode.MaxInitialSubmarinePrice && !GameMain.DebugDraw) + { + StartButton.Enabled = false; + return false; + } +#endif + StartButton.Enabled = true; sub.CreatePreviewWindow(subPreviewContainer); return true; } @@ -262,10 +376,10 @@ namespace Barotrauma }; msgBox.Buttons[0].OnClicked += msgBox.Close; - DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 10); - while (GameMain.NetLobbyScreen.CampaignUI == null && DateTime.Now < timeOut) + DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 20); + while (Screen.Selected != GameMain.GameScreen && DateTime.Now < timeOut) { - msgBox.Header.Text = headerText + new string('.', ((int)Timing.TotalTime % 3 + 1)); + msgBox.Header.Text = headerText + new string('.', (int)Timing.TotalTime % 3 + 1); yield return CoroutineStatus.Running; } msgBox.Close(); @@ -281,11 +395,13 @@ namespace Barotrauma public void UpdateSubList(IEnumerable submarines) { -#if !DEBUG - var subsToShow = submarines.Where(s => !s.HasTag(SubmarineTag.HideInMenus)); -#else - var subsToShow = submarines; -#endif + var subsToShow = submarines.Where(s => s.IsCampaignCompatibleIgnoreClass).ToList(); + subsToShow.Sort((s1, s2) => + { + int p1 = s1.Price > CampaignMode.MaxInitialSubmarinePrice ? 10 : 0; + int p2 = s2.Price > CampaignMode.MaxInitialSubmarinePrice ? 10 : 0; + return p1.CompareTo(p2) * 100 + s1.Name.CompareTo(s2.Name); + }); subList.ClearChildren(); @@ -299,30 +415,28 @@ namespace Barotrauma UserData = sub }; - if(!sub.RequiredContentPackagesInstalled) + if (!sub.RequiredContentPackagesInstalled) { textBlock.TextColor = Color.Lerp(textBlock.TextColor, Color.DarkRed, .5f); textBlock.ToolTip = TextManager.Get("ContentPackageMismatch") + "\n\n" + textBlock.RawToolTip; } - if (sub.HasTag(SubmarineTag.Shuttle)) + 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) { - textBlock.TextColor = textBlock.TextColor * 0.85f; - - var shuttleText = new GUITextBlock(new RectTransform(new Point(100, textBlock.Rect.Height), textBlock.RectTransform, Anchor.CenterRight) - { - IsFixedSize = false - }, - TextManager.Get("Shuttle", fallBackTag: "RespawnShuttle"), textAlignment: Alignment.Right, font: GUI.SmallFont) - { - TextColor = textBlock.TextColor * 0.8f, - ToolTip = textBlock.RawToolTip - }; + TextColor = sub.Price > CampaignMode.MaxInitialSubmarinePrice ? GUI.Style.Red : textBlock.TextColor * 0.8f, + ToolTip = textBlock.ToolTip + }; +#if !DEBUG + if (sub.Price > CampaignMode.MaxInitialSubmarinePrice && !GameMain.DebugDraw) + { + textBlock.CanBeFocused = false; } +#endif } if (SubmarineInfo.SavedSubmarines.Any()) { - var nonShuttles = subsToShow.Where(s => !s.HasTag(SubmarineTag.Shuttle)).ToList(); + var nonShuttles = subsToShow.Where(s => s.Type == SubmarineType.Player && !s.HasTag(SubmarineTag.Shuttle) && s.Price <= CampaignMode.MaxInitialSubmarinePrice).ToList(); if (nonShuttles.Count > 0) { subList.Select(nonShuttles[Rand.Int(nonShuttles.Count)]); @@ -389,6 +503,7 @@ namespace Barotrauma CanBeFocused = false }; + bool isCompatible = true; if (!isMultiplayer) { nameText.Text = Path.GetFileNameWithoutExtension(saveFile); @@ -406,10 +521,10 @@ namespace Barotrauma saveList.Content.RemoveChild(saveFrame); continue; } - subName = doc.Root.GetAttributeString("submarine", ""); - saveTime = doc.Root.GetAttributeString("savetime", ""); - contentPackageStr = doc.Root.GetAttributeString("selectedcontentpackages", ""); - + subName = doc.Root.GetAttributeString("submarine", ""); + saveTime = doc.Root.GetAttributeString("savetime", ""); + isCompatible = SaveUtil.IsSaveFileCompatible(doc); + contentPackageStr = doc.Root.GetAttributeString("selectedcontentpackages", ""); prevSaveFiles?.Add(saveFile); } else @@ -436,6 +551,11 @@ namespace Barotrauma saveFrame.ToolTip = string.Join("\n", errorMsg, TextManager.Get("campaignmode.contentpackagemismatchwarning")); } } + if (!isCompatible) + { + nameText.TextColor = GUI.Style.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) @@ -516,13 +636,13 @@ namespace Barotrauma } XDocument doc = SaveUtil.LoadGameSessionDoc(fileName); - if (doc == null) + if (doc?.Root == null) { DebugConsole.ThrowError("Error loading save file \"" + fileName + "\". The file may be corrupted."); return false; } - loadGameButton.Enabled = true; + loadGameButton.Enabled = SaveUtil.IsSaveFileCompatible(doc); RemoveSaveFrame(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index ab4b6ac92..306b1f780 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -10,311 +10,92 @@ namespace Barotrauma { class CampaignUI { - public enum Tab { Map, Crew, Store, Repair } - private Tab selectedTab; - private GUIFrame[] tabs; - private GUIFrame topPanel; + private CampaignMode.InteractionType selectedTab; - private GUIListBox characterList; + private GUIFrame[] tabs; + + public CampaignMode.InteractionType SelectedTab => selectedTab; private Point prevResolution; - private MapEntityCategory selectedItemCategory = MapEntityCategory.Equipment; + private GUIComponent locationInfoPanel; - private GUIListBox myItemList; - private GUIListBox storeItemList; - private GUITextBox searchBox; - - private GUIComponent missionPanel; - private GUIComponent selectedLocationInfo; - private GUIListBox selectedMissionInfo; + private GUIListBox missionList; private GUIButton repairHullsButton, replaceShuttlesButton, repairItemsButton; - private GUIFrame characterPreviewFrame; - - private bool displayMissionPanelInMapTab; - - private readonly List tabButtons = new List(); - private readonly List itemCategoryButtons = new List(); - private readonly List missionTickBoxes = new List(); - private GUIRadioButtonGroup missionRadioButtonGroup = new GUIRadioButtonGroup(); + private SubmarineSelection submarineSelection; private Location selectedLocation; public Action StartRound; - public Action OnLocationSelected; - public Level SelectedLevel { get; private set; } - - public GUIComponent MapContainer { get; private set; } - - public GUIButton StartButton { get; private set; } + public LevelData SelectedLevel { get; private set; } + + private GUIButton StartButton { get; set; } public CampaignMode Campaign { get; } - public CampaignUI(CampaignMode campaign, GUIComponent parent) - { - this.Campaign = campaign; + public CrewManagement CrewManagement { get; set; } + private Store Store { get; set; } - var container = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform), style: null); + public UpgradeStore UpgradeStore { get; set; } + + public CampaignUI(CampaignMode campaign, GUIComponent container) + { + Campaign = campaign; + + if (campaign.Map == null) { throw new InvalidOperationException("Failed to create campaign UI (campaign map was null)."); } + if (campaign.Map.CurrentLocation == null) { throw new InvalidOperationException("Failed to create campaign UI (current location not set)."); } CreateUI(container); campaign.Map.OnLocationSelected += SelectLocation; - campaign.Map.OnLocationChanged += (prevLocation, newLocation) => UpdateLocationView(newLocation); campaign.Map.OnMissionSelected += (connection, mission) => { - var selectedTickBox = (missionRadioButtonGroup.UserData as List).FindIndex(m => m == mission); - if (selectedTickBox >= 0) - { - missionRadioButtonGroup.Selected = selectedTickBox; - } + missionList.Select(mission); }; - campaign.CargoManager.OnItemsChanged += RefreshMyItems; } private void CreateUI(GUIComponent container) { container.ClearChildren(); - MapContainer = new GUICustomComponent(new RectTransform(Vector2.One, container.RectTransform), DrawMap, UpdateMap); - new GUIFrame(new RectTransform(Vector2.One, MapContainer.RectTransform), style: "InnerGlow", color: Color.Black * 0.9f) + tabs = new GUIFrame[Enum.GetValues(typeof(CampaignMode.InteractionType)).Length]; + + // map tab ------------------------------------------------------------------------- + + tabs[(int)CampaignMode.InteractionType.Map] = CreateDefaultTabContainer(container, new Vector2(0.9f)); + var mapFrame = new GUIFrame(new RectTransform(Vector2.One, GetTabContainer(CampaignMode.InteractionType.Map).RectTransform, Anchor.TopLeft), color: Color.Black * 0.9f); + new GUICustomComponent(new RectTransform(Vector2.One, mapFrame.RectTransform), DrawMap, UpdateMap); + new GUIFrame(new RectTransform(Vector2.One, mapFrame.RectTransform), style: "InnerGlow", color: Color.Black * 0.9f) { CanBeFocused = false }; - // top panel ------------------------------------------------------------------------- - - topPanel = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.15f), container.RectTransform, Anchor.TopCenter), style: null) - { - CanBeFocused = false - }; - var topPanelContent = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.9f), topPanel.RectTransform, Anchor.BottomCenter), style: null) - { - CanBeFocused = false - }; - - var outpostBtn = new GUIButton(new RectTransform(new Vector2(0.15f, 0.55f), topPanelContent.RectTransform), - TextManager.Get("Outpost"), textAlignment: Alignment.Center, style: "GUISlopedHeader") - { - OnClicked = (btn, userdata) => { SelectTab(Tab.Map); return true; } - }; - outpostBtn.TextBlock.Font = GUI.LargeFont; - outpostBtn.TextBlock.AutoScaleHorizontal = true; - - var tabButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 0.4f), topPanelContent.RectTransform, Anchor.BottomLeft), isHorizontal: true); - - int i = 0; - var tabValues = Enum.GetValues(typeof(Tab)); - foreach (Tab tab in tabValues) - { - var tabButton = new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), tabButtonContainer.RectTransform), - "", - style: i == 0 ? "GUISlopedTabButtonLeft" : (i == tabValues.Length - 1 ? "GUISlopedTabButtonRight" : "GUISlopedTabButtonMid")) - { - UserData = tab, - OnClicked = (btn, userdata) => { SelectTab((Tab)userdata); return true; }, - Selected = tab == Tab.Map - }; - var buttonSprite = tabButton.Style.Sprites[GUIComponent.ComponentState.None][0]; - tabButton.RectTransform.MaxSize = new Point( - (int)(tabButton.Rect.Height * (buttonSprite.Sprite.size.X / buttonSprite.Sprite.size.Y)), int.MaxValue); - - //the text needs to be positioned differently in the buttons at the edges due to the "slopes" in the button - if (i == 0 || i == tabValues.Length - 1) - { - new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.9f), tabButton.RectTransform, i == 0 ? Anchor.CenterLeft : Anchor.CenterRight) { RelativeOffset = new Vector2(0.05f, 0.0f) }, - TextManager.Get(tab.ToString()), textColor: tabButton.TextColor, font: GUI.LargeFont, textAlignment: Alignment.Center, style: null) - { - UserData = "buttontext", - Padding = new Vector4(GUI.Scale * 1) - }; - } - else - { - new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.9f), tabButton.RectTransform, Anchor.Center), - TextManager.Get(tab.ToString()), textColor: tabButton.TextColor, font: GUI.LargeFont, textAlignment: Alignment.Center, style: null) - { - UserData = "buttontext", - Padding = new Vector4(GUI.Scale * 1) - }; - } - - tabButtons.Add(tabButton); - i++; - } - GUITextBlock.AutoScaleAndNormalize(tabButtons.Select(t => t.GetChildByUserData("buttontext") as GUITextBlock)); - tabButtons.FirstOrDefault().RectTransform.SizeChanged += () => - { - GUITextBlock.AutoScaleAndNormalize(tabButtons.Select(t => t.GetChildByUserData("buttontext") as GUITextBlock), defaultScale: 1.0f); - }; - // crew tab ------------------------------------------------------------------------- - tabs = new GUIFrame[Enum.GetValues(typeof(Tab)).Length]; - tabs[(int)Tab.Crew] = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.7f), container.RectTransform, Anchor.TopLeft) - { - RelativeOffset = new Vector2(0.0f, topPanel.RectTransform.RelativeSize.Y) - }, color: Color.Black * 0.9f); - new GUIFrame(new RectTransform(new Vector2(1.25f, 1.25f), tabs[(int)Tab.Crew].RectTransform, Anchor.Center), style: "OuterGlow", color: Color.Black * 0.7f) - { - UserData = "outerglow", - CanBeFocused = false - }; - - var crewContent = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), tabs[(int)Tab.Crew].RectTransform, Anchor.Center)) - { - Stretch = true, - RelativeSpacing = 0.02f - }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), crewContent.RectTransform), "", font: GUI.LargeFont) - { - TextGetter = GetMoney - }; - - characterList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.9f), crewContent.RectTransform)) - { - OnSelected = SelectCharacter - }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), characterList.Content.RectTransform), - TextManager.Get("CampaignMenuCrew"), font: GUI.LargeFont) - { - UserData = "mycrew", - CanBeFocused = false, - AutoScaleHorizontal = true - }; - if (Campaign is SinglePlayerCampaign) - { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), characterList.Content.RectTransform), - TextManager.Get("CampaignMenuHireable"), font: GUI.LargeFont) - { - UserData = "hire", - CanBeFocused = false, - AutoScaleHorizontal = true - }; - } + var crewTab = new GUIFrame(new RectTransform(Vector2.One, container.RectTransform), color: Color.Black * 0.9f); + tabs[(int)CampaignMode.InteractionType.Crew] = crewTab; + CrewManagement = new CrewManagement(this, crewTab); // store tab ------------------------------------------------------------------------- - - tabs[(int)Tab.Store] = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.7f), container.RectTransform, Anchor.TopLeft) - { - RelativeOffset = new Vector2(0.1f, topPanel.RectTransform.RelativeSize.Y) - }, color: Color.Black * 0.9f); - new GUIFrame(new RectTransform(new Vector2(1.25f, 1.25f), tabs[(int)Tab.Store].RectTransform, Anchor.Center), style: "OuterGlow", color: Color.Black * 0.7f) - { - UserData = "outerglow", - CanBeFocused = false - }; - - var storeContent = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), tabs[(int)Tab.Store].RectTransform, Anchor.Center)) - { - UserData = "content", - Stretch = true, - RelativeSpacing = 0.015f - }; - - var storeContentTop = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), storeContent.RectTransform) { MinSize = new Point(0, (int)(30 * GUI.Scale)) }, isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - Stretch = true - }; - - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), storeContentTop.RectTransform), "", font: GUI.LargeFont) - { - TextGetter = GetMoney - }; - var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 0.4f), storeContentTop.RectTransform) { MinSize = new Point(0, (int)(25 * GUI.Scale)) }, 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); - searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform), createClearButton: true); - searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; - searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; - searchBox.OnTextChanged += (textBox, text) => { FilterStoreItems(null, text); return true; }; - - var storeItemLists = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.8f), storeContent.RectTransform), isHorizontal: true) - { - RelativeSpacing = 0.03f, - Stretch = true - }; - myItemList = new GUIListBox(new RectTransform(new Vector2(0.5f, 1.0f), storeItemLists.RectTransform)) - { - AutoHideScrollBar = false - }; - storeItemList = new GUIListBox(new RectTransform(new Vector2(0.5f, 1.0f), storeItemLists.RectTransform)) - { - AutoHideScrollBar = false, - OnSelected = BuyItem - }; - - var categoryButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.1f, 0.9f), tabs[(int)Tab.Store].RectTransform, Anchor.CenterLeft, Pivot.CenterRight)) - { - RelativeSpacing = 0.02f - }; - - List itemCategories = Enum.GetValues(typeof(MapEntityCategory)).Cast().ToList(); - //don't show categories with no buyable items - itemCategories.RemoveAll(c => - !ItemPrefab.Prefabs.Any(ep => ep.Category.HasFlag(c) && ep.CanBeBought)); - foreach (MapEntityCategory category in itemCategories) - { - var categoryButton = new GUIButton(new RectTransform(new Point(categoryButtonContainer.Rect.Width, categoryButtonContainer.Rect.Width), categoryButtonContainer.RectTransform), - "", style: "ItemCategory" + category.ToString()) - { - UserData = category, - OnClicked = (btn, userdata) => - { - MapEntityCategory newCategory = (MapEntityCategory)userdata; - if (newCategory != selectedItemCategory) - { - searchBox.Text = ""; - storeItemList.ScrollBar.BarScroll = 0f; - } - - FilterStoreItems((MapEntityCategory)userdata, searchBox.Text); - return true; - } - }; - itemCategoryButtons.Add(categoryButton); - - categoryButton.RectTransform.SizeChanged += () => - { - var sprite = categoryButton.Frame.sprites[GUIComponent.ComponentState.None].First(); - categoryButton.RectTransform.NonScaledSize = - new Point(categoryButton.Rect.Width, (int)(categoryButton.Rect.Width * ((float)sprite.Sprite.SourceRect.Height / sprite.Sprite.SourceRect.Width))); - }; - - new GUITextBlock(new RectTransform(new Vector2(0.95f, 0.256f), categoryButton.RectTransform, Anchor.BottomCenter) { RelativeOffset = new Vector2(0.0f, 0.02f) }, - TextManager.Get("MapEntityCategory." + category), textAlignment: Alignment.Center, textColor: categoryButton.TextColor) - { - Padding = Vector4.Zero, - AutoScaleHorizontal = true, - Color = Color.Transparent, - HoverColor = Color.Transparent, - PressedColor = Color.Transparent, - SelectedColor = Color.Transparent, - CanBeFocused = true - }; - } - FillStoreItemList(); - FilterStoreItems(MapEntityCategory.Equipment, ""); + + var storeTab = new GUIFrame(new RectTransform(Vector2.One, container.RectTransform), color: Color.Black * 0.9f); + tabs[(int)CampaignMode.InteractionType.Store] = storeTab; + Store = new Store(this, storeTab); // repair tab ------------------------------------------------------------------------- - tabs[(int)Tab.Repair] = new GUIFrame(new RectTransform(new Vector2(0.35f, 0.5f), container.RectTransform, Anchor.TopLeft) - { - RelativeOffset = new Vector2(0.02f, topPanel.RectTransform.RelativeSize.Y) - }, color: Color.Black * 0.9f); - new GUIFrame(new RectTransform(new Vector2(1.25f, 1.25f), tabs[(int)Tab.Repair].RectTransform, Anchor.Center), style: "OuterGlow", color: Color.Black * 0.7f) + tabs[(int)CampaignMode.InteractionType.Repair] = CreateDefaultTabContainer(container, new Vector2(0.7f)); + var repairFrame = new GUIFrame(new RectTransform(Vector2.One, GetTabContainer(CampaignMode.InteractionType.Repair).RectTransform, Anchor.TopLeft), color: Color.Black * 0.9f); + new GUIFrame(new RectTransform(new Vector2(1.25f, 1.25f), repairFrame.RectTransform, Anchor.Center), style: "OuterGlow", color: Color.Black * 0.7f) { UserData = "outerglow", CanBeFocused = false }; - var repairContent = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.85f), tabs[(int)Tab.Repair].RectTransform, Anchor.Center)) + var repairContent = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.85f), repairFrame.RectTransform, Anchor.Center)) { RelativeSpacing = 0.05f, Stretch = true @@ -468,211 +249,54 @@ namespace Barotrauma GUITextBlock.AutoScaleAndNormalize(repairHullsLabel, repairItemsLabel, replaceShuttlesLabel); GUITextBlock.AutoScaleAndNormalize(repairHullsButton.GetChild().TextBlock, repairItemsButton.GetChild().TextBlock, replaceShuttlesButton.GetChild().TextBlock); + // upgrade tab ------------------------------------------------------------------------- + + tabs[(int)CampaignMode.InteractionType.Upgrade] = new GUIFrame(new RectTransform(Vector2.One, container.RectTransform), color: Color.Black * 0.9f); + UpgradeStore = new UpgradeStore(this, GetTabContainer(CampaignMode.InteractionType.Upgrade)); + + // Submarine buying tab + tabs[(int)CampaignMode.InteractionType.PurchaseSub] = new GUIFrame(new RectTransform(Vector2.One, container.RectTransform, Anchor.TopLeft), color: Color.Black * 0.9f); // mission info ------------------------------------------------------------------------- - missionPanel = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.5f), container.RectTransform, Anchor.TopRight) - { - RelativeOffset = new Vector2(0.0f, topPanel.RectTransform.RelativeSize.Y) - }, color: Color.Black * 0.7f) + locationInfoPanel = new GUIFrame(new RectTransform(new Vector2(0.35f, 0.75f), GetTabContainer(CampaignMode.InteractionType.Map).RectTransform, Anchor.CenterRight) + { RelativeOffset = new Vector2(0.02f, 0.0f) }, + color: Color.Black) { Visible = false }; - new GUIFrame(new RectTransform(new Vector2(1.25f, 1.25f), missionPanel.RectTransform, Anchor.Center), style: "OuterGlow", color: Color.Black * 0.7f) - { - UserData = "outerglow", - CanBeFocused = false - }; - - new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.15f), missionPanel.RectTransform, Anchor.TopRight, Pivot.BottomRight) - { RelativeOffset = new Vector2(0.1f, -0.05f) }, TextManager.Get("Mission"), - textAlignment: Alignment.Center, font: GUI.LargeFont, style: "GUISlopedHeader") - { - UserData = "missionlabel", - AutoScaleHorizontal = true - }; - var missionPanelContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), missionPanel.RectTransform, Anchor.Center)) - { - Stretch = true, - RelativeSpacing = 0.05f - }; - - selectedLocationInfo = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.75f), missionPanelContent.RectTransform)) - { - RelativeSpacing = 0.02f, - Stretch = true - }; - selectedMissionInfo = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.25f), missionPanel.RectTransform, Anchor.BottomRight, Pivot.TopRight) - { MinSize = new Point(0, (int)(150 * GUI.Scale)) }) - { - Visible = false - }; - selectedMissionInfo.RectTransform.MaxSize = new Point(int.MaxValue, selectedMissionInfo.Rect.Height * 2); - new GUIFrame(new RectTransform(new Vector2(1.25f, 1.25f), selectedMissionInfo.RectTransform, Anchor.Center), style: "OuterGlow", color: Color.Black * 0.9f) - { - UserData = "outerglow", - CanBeFocused = false - }; - // ------------------------------------------------------------------------- - topPanel.RectTransform.SetAsLastChild(); - - SelectTab(Tab.Map); - - UpdateLocationView(Campaign.Map.CurrentLocation); - - menuPanelParent?.ClearChildren(); - missionPanelParent?.ClearChildren(); - if (menuPanelParent != null) - { - SetMenuPanelParent(menuPanelParent); - } - if (missionPanelParent != null) - { - SetMissionPanelParent(missionPanelParent); - } + SelectTab(CampaignMode.InteractionType.Map); prevResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); } - private RectTransform missionPanelParent, menuPanelParent; - - public void SetMissionPanelParent(RectTransform parent) + private GUIFrame CreateDefaultTabContainer(GUIComponent container, Vector2 frameSize, bool visible = true) { - missionPanel.RectTransform.Parent = parent; - missionPanel.RectTransform.RelativeOffset = Vector2.Zero; - missionPanel.RectTransform.RelativeSize = Vector2.One; - var outerGlow = missionPanel.GetChildByUserData("outerglow"); - if (outerGlow != null) { outerGlow.Visible = false; } - var label = missionPanel.GetChildByUserData("missionlabel"); - if (label != null) { label.Visible = false; } - - displayMissionPanelInMapTab = true; - - selectedMissionInfo.RectTransform.RelativeOffset = Vector2.Zero; - selectedMissionInfo.RectTransform.SetPosition(Anchor.BottomLeft, Pivot.BottomRight); - missionPanelParent = parent; - } - public void SetMenuPanelParent(RectTransform parent) - { - for (int i = 0; i < tabs.Length; i++) + var innerFrame = new GUIFrame(new RectTransform(frameSize, container.RectTransform, Anchor.Center)) { - var panel = tabs[i]; - if (panel == null) { continue; } - panel.RectTransform.Parent = parent; - panel.RectTransform.RelativeOffset = Vector2.Zero; - panel.RectTransform.RelativeSize = Vector2.One; - var outerGlow = panel.GetChildByUserData("outerglow"); - if (outerGlow != null) { outerGlow.Visible = false; } - - if (i == (int)Tab.Store) - { - panel.RectTransform.RelativeSize *= new Vector2(1.5f, 1.0f); - panel.RectTransform.SetPosition(Anchor.TopRight); - var content = panel.GetChildByUserData("content"); - if (content != null) { content.RectTransform.RelativeSize = Vector2.One; } - new GUIFrame(new RectTransform(new Vector2(1.107f, 1.0f), panel.RectTransform, Anchor.TopRight), style: null) - { - Color = Color.Black, - CanBeFocused = false - }.SetAsFirstChild(); - } - } - menuPanelParent = parent; + Visible = visible + }; + new GUIFrame(new RectTransform(innerFrame.Rect.Size - GUIStyle.ItemFrameMargin, innerFrame.RectTransform, Anchor.Center), style: null) + { + UserData = "container" + }; + return innerFrame; } - private void UpdateLocationView(Location location) + public GUIComponent GetTabContainer(CampaignMode.InteractionType tab) { - if (location == null) - { - string errorMsg = "Failed to update CampaignUI location view (location was null)\n" + Environment.StackTrace; - DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("CampaignUI.UpdateLocationView:LocationNull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); - return; - } - - if (characterPreviewFrame != null) - { - characterPreviewFrame.Parent?.RemoveChild(characterPreviewFrame); - characterPreviewFrame = null; - } - - if (characterList != null) - { - if (Campaign is SinglePlayerCampaign) - { - var hireableCharacters = location.GetHireableCharacters(); - foreach (GUIComponent child in characterList.Content.Children.ToList()) - { - if (child.UserData is CharacterInfo character) - { - if (GameMain.GameSession.CrewManager != null) - { - if (GameMain.GameSession.CrewManager.GetCharacterInfos().Contains(character)) { continue; } - } - } - else if (child.UserData as string == "mycrew" || child.UserData as string == "hire") - { - continue; - } - characterList.RemoveChild(child); - } - if (!hireableCharacters.Any()) - { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), characterList.Content.RectTransform), TextManager.Get("HireUnavailable"), textAlignment: Alignment.Center) - { - CanBeFocused = false - }; - } - else - { - foreach (CharacterInfo c in hireableCharacters) - { - var frame = c.CreateCharacterFrame(characterList.Content, c.Name + " (" + c.Job.Name + ")", c); - new GUITextBlock(new RectTransform(Vector2.One, frame.RectTransform, Anchor.TopRight), c.Salary.ToString(), textAlignment: Alignment.CenterRight); - } - } - } - characterList.UpdateScrollBarSize(); - } - - RefreshMyItems(); - - bool purchaseableItemsFound = false; - foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) - { - PriceInfo priceInfo = itemPrefab.GetPrice(Campaign.Map.CurrentLocation); - if (priceInfo != null) { purchaseableItemsFound = true; break; } - } - - //disable store tab if there's nothing to buy - tabButtons.Find(btn => (Tab)btn.UserData == Tab.Store).Enabled = purchaseableItemsFound; - - if (selectedTab == Tab.Store && !purchaseableItemsFound) - { - //switch out from store tab if there's nothing to buy - SelectTab(Tab.Map); - } - else - { - //refresh store view - FillStoreItemList(); - - MapEntityCategory? category = null; - //only select a specific category if the search box is empty - //(items from all categories are shown when searching) - if (string.IsNullOrEmpty(searchBox.Text)) { category = selectedItemCategory; } - FilterStoreItems(category, searchBox.Text); - } + var tabFrame = tabs[(int)tab]; + return tabFrame?.GetChildByUserData("container") ?? tabFrame; } private void DrawMap(SpriteBatch spriteBatch, GUICustomComponent mapContainer) { if (GameMain.GraphicsWidth != prevResolution.X || GameMain.GraphicsHeight != prevResolution.Y) { - CreateUI(MapContainer.Parent); + CreateUI(tabs[(int)CampaignMode.InteractionType.Map].Parent); } GameMain.GameSession?.Map?.Draw(spriteBatch, mapContainer); @@ -680,372 +304,208 @@ namespace Barotrauma private void UpdateMap(float deltaTime, GUICustomComponent mapContainer) { - GameMain.GameSession?.Map?.Update(deltaTime, mapContainer); + var map = GameMain.GameSession?.Map; + if (map == null) { return; } + if (selectedLocation != null && selectedLocation == map.CurrentDisplayLocation) + { + map.SelectLocation(-1); + } + map.Update(deltaTime, mapContainer); } - - public void UpdateCharacterLists() + + public void Update(float deltaTime) { - //remove the player's crew from the listbox (everything between the "mycrew" and "hire" labels) - foreach (GUIComponent child in characterList.Content.Children.ToList()) + switch (SelectedTab) { - if (child.UserData as string == "mycrew") - { - continue; - } - else if (child.UserData as string == "hire") - { + case CampaignMode.InteractionType.PurchaseSub: + submarineSelection?.Update(); + break; + + case CampaignMode.InteractionType.Crew: + CrewManagement?.Update(); + break; + + case CampaignMode.InteractionType.Store: + Store?.Update(); break; - } - characterList.RemoveChild(child); } - foreach (CharacterInfo c in GameMain.GameSession.CrewManager.GetCharacterInfos().Reverse()) + } + + public void RefreshLocationInfo() + { + if (selectedLocation != null && Campaign?.Map?.SelectedConnection != null) { - var frame = c.CreateCharacterFrame(characterList.Content, c.Name + " (" + c.Job.Name + ") ", c); - //add after the "mycrew" label - frame.RectTransform.RepositionChildInHierarchy(1); + SelectLocation(selectedLocation, Campaign.Map.SelectedConnection); } - characterList.UpdateScrollBarSize(); } public void SelectLocation(Location location, LocationConnection connection) { - selectedLocationInfo.ClearChildren(); - //don't select the map panel if the tabs are displayed in the same place as the map, and we're looking at some other tab - if (!displayMissionPanelInMapTab || selectedTab == Tab.Map) + locationInfoPanel.ClearChildren(); + //don't select the map panel if we're looking at some other tab + if (selectedTab == CampaignMode.InteractionType.Map) { - SelectTab(Tab.Map); - missionPanel.Visible = location != null; + SelectTab(CampaignMode.InteractionType.Map); + locationInfoPanel.Visible = location != null; } + Location prevSelectedLocation = selectedLocation; + float prevMissionListScroll = missionList?.BarScroll ?? 0.0f; + selectedLocation = location; if (location == null) { return; } - - var container = selectedLocationInfo; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), container.RectTransform), location.Name, font: GUI.LargeFont) + + int padding = GUI.IntScale(20); + + var content = new GUILayoutGroup(new RectTransform(locationInfoPanel.Rect.Size - new Point(padding * 2), locationInfoPanel.RectTransform, Anchor.Center), childAnchor: Anchor.TopRight) + { + Stretch = true, + RelativeSpacing = 0.02f, + }; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), location.Name, font: GUI.LargeFont) { AutoScaleHorizontal = true }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), container.RectTransform), location.Type.Name, font: GUI.SubHeadingFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), location.Type.Name, font: GUI.SubHeadingFont); Sprite portrait = location.Type.GetPortrait(location.PortraitId); - new GUIImage(new RectTransform(new Vector2(1.0f, 0.6f), - container.RectTransform), portrait, scaleToFit: true); + portrait.EnsureLazyLoaded(); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), container.RectTransform), TextManager.Get("SelectMission"), font: GUI.SubHeadingFont) + var portraitContainer = new GUICustomComponent(new RectTransform(new Vector2(1.0f, 0.3f), content.RectTransform), onDraw: (sb, customComponent) => { - AutoScaleHorizontal = true + portrait.Draw(sb, customComponent.Rect.Center.ToVector2(), Color.Gray, portrait.size / 2, scale: Math.Max(customComponent.Rect.Width / portrait.size.X, customComponent.Rect.Height / portrait.size.Y)); + }) + { + HideElementsOutsideFrame = true }; - var missionFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.3f), container.RectTransform), style: "InnerFrame"); - var missionContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), missionFrame.RectTransform, Anchor.Center)) + var textContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), portraitContainer.RectTransform, Anchor.Center)) { - RelativeSpacing = 0.02f, - Stretch = true + RelativeSpacing = 0.05f }; - - SelectedLevel = connection?.Level; - if (connection != null) + + if (connection?.LevelData != null) { - List availableMissions = Campaign.Map.CurrentLocation.GetMissionsInConnection(connection).ToList(); - if (!availableMissions.Contains(null)) { availableMissions.Add(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); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), biomeLabel.RectTransform), connection.Biome.DisplayName, textAlignment: Alignment.CenterRight); - Mission selectedMission = Campaign.Map.CurrentLocation.SelectedMission != null && availableMissions.Contains(Campaign.Map.CurrentLocation.SelectedMission) ? - Campaign.Map.CurrentLocation.SelectedMission : null; - missionTickBoxes.Clear(); - missionRadioButtonGroup = new GUIRadioButtonGroup - { - UserData = availableMissions - }; + var difficultyLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), textContent.RectTransform), + TextManager.Get("LevelDifficulty"), font: GUI.SubHeadingFont, textAlignment: Alignment.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), difficultyLabel.RectTransform), ((int)connection.LevelData.Difficulty) + " %", textAlignment: Alignment.CenterRight); + } - for (int i = 0; i < availableMissions.Count; i++) + missionList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.4f), content.RectTransform)) + { + Spacing = (int)(5 * GUI.yScale) + }; + + SelectedLevel = connection?.LevelData; + Location currentDisplayLocation = Campaign.CurrentDisplayLocation; + if (connection != null && connection.Locations.Contains(currentDisplayLocation)) + { + List availableMissions = currentDisplayLocation.GetMissionsInConnection(connection).ToList(); + if (!availableMissions.Contains(null)) { availableMissions.Insert(0, null); } + + Mission selectedMission = currentDisplayLocation.SelectedMission != null && availableMissions.Contains(currentDisplayLocation.SelectedMission) ? + currentDisplayLocation.SelectedMission : null; + + missionList.Content.ClearChildren(); + + foreach (Mission mission in availableMissions) { - var mission = availableMissions[i]; - var tickBox = new GUITickBox(new RectTransform(new Vector2(0.65f, 0.1f), missionContent.RectTransform), - mission?.Name ?? TextManager.Get("NoMission"), style: "GUIRadioButton") + var missionPanel = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), missionList.Content.RectTransform), style: null) { - Enabled = GameMain.Client == null || GameMain.Client.HasPermission(Networking.ClientPermissions.ManageCampaign) + UserData = mission }; - tickBox.Font = tickBox.Rect.Width < 150 ? GUI.SmallFont : GUI.Font; - tickBox.TextBlock.Wrap = true; - missionTickBoxes.Add(tickBox); - missionRadioButtonGroup.AddRadioButton(i, tickBox); + var missionTextContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), missionPanel.RectTransform, Anchor.Center)) + { + Stretch = true, + CanBeFocused = true + }; + + var missionName = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), mission?.Name ?? TextManager.Get("NoMission"), font: GUI.SubHeadingFont, wrap: true); + if (mission != null) + { + if (MapGenerationParams.Instance?.MissionIcon != null) + { + var icon = new GUIImage(new RectTransform(Vector2.One * 0.9f, missionName.RectTransform, anchor: Anchor.CenterLeft, scaleBasis: ScaleBasis.Smallest) { AbsoluteOffset = new Point((int)missionName.Padding.X, 0) }, + MapGenerationParams.Instance.MissionIcon, scaleToFit: true) + { + Color = MapGenerationParams.Instance.IndicatorColor * 0.5f, + SelectedColor = MapGenerationParams.Instance.IndicatorColor, + HoverColor = Color.Lerp(MapGenerationParams.Instance.IndicatorColor, Color.White, 0.5f) + }; + missionName.Padding = new Vector4(missionName.Padding.X + icon.Rect.Width * 1.5f, missionName.Padding.Y, missionName.Padding.Z, missionName.Padding.W); + } + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), + TextManager.GetWithVariable("missionreward", "[reward]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", mission.Reward)), wrap: true); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), mission.Description, wrap: true); + } + missionPanel.RectTransform.MinSize = new Point(0, (int)(missionTextContent.Children.Sum(c => c.Rect.Height) / missionTextContent.RectTransform.RelativeSize.Y) + GUI.IntScale(20)); + foreach (GUIComponent child in missionTextContent.Children) + { + var textBlock = child as GUITextBlock; + textBlock.Color = textBlock.SelectedColor = textBlock.HoverColor = Color.Transparent; + textBlock.HoverTextColor = textBlock.TextColor; + textBlock.TextColor *= 0.5f; + } + missionPanel.OnAddedToGUIUpdateList = (c) => + { + missionTextContent.Children.ForEach(child => child.State = c.State); + }; + + if (mission != availableMissions.Last()) + { + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), missionList.Content.RectTransform), style: "HorizontalLine") + { + CanBeFocused = false + }; + } + } + missionList.Select(selectedMission); + if (prevSelectedLocation == selectedLocation) + { + missionList.BarScroll = prevMissionListScroll; } - missionFrame.RectTransform.MinSize = - new Point(0, (int)(missionContent.RectTransform.Children.Sum(c => c.MinSize.Y * 1.02f) / missionContent.RectTransform.RelativeSize.Y)); - - if (GameMain.Client == null || GameMain.Client.HasPermission(Networking.ClientPermissions.ManageCampaign)) + if (Campaign.AllowedToManageCampaign()) { - missionRadioButtonGroup.OnSelect = (rbg, missionInd) => + missionList.OnSelected = (component, userdata) => { - int ind = missionInd ?? -1; - if (ind < 0) { return; } - var mission = availableMissions[ind]; - if (Campaign.Map.CurrentLocation.SelectedMission == mission) { return; } - if (rbg.Selected == missionInd) { return; } - RefreshMissionTab(mission); + Mission mission = userdata as Mission; + if (Campaign.Map.CurrentLocation.SelectedMission == mission) { return false; } + Campaign.Map.CurrentLocation.SelectedMission = mission; + //RefreshMissionInfo(mission); if ((Campaign is MultiPlayerCampaign multiPlayerCampaign) && !multiPlayerCampaign.SuppressStateSending && - GameMain.Client != null && GameMain.Client.HasPermission(Networking.ClientPermissions.ManageCampaign)) + Campaign.AllowedToManageCampaign()) { GameMain.Client?.SendCampaignState(); } + return true; }; } - - missionRadioButtonGroup.Selected = availableMissions.IndexOf(selectedMission); - - RefreshMissionTab(selectedMission); - - StartButton = new GUIButton(new RectTransform(new Vector2(0.3f, 0.7f), missionContent.RectTransform, Anchor.CenterRight), - TextManager.Get("StartCampaignButton"), style: "GUIButtonLarge") - { - IgnoreLayoutGroups = true, - OnClicked = (GUIButton btn, object obj) => { StartRound?.Invoke(); return true; }, - Enabled = true - }; - if (GameMain.Client != null) - { - StartButton.Visible = !GameMain.Client.GameStarted && - (GameMain.Client.HasPermission(Networking.ClientPermissions.ManageRound) || - GameMain.Client.HasPermission(Networking.ClientPermissions.ManageCampaign)); - } } - OnLocationSelected?.Invoke(location, connection); - } - - - public void RefreshMissionTab(Mission selectedMission) - { - System.Diagnostics.Debug.Assert( - selectedMission == null || - (GameMain.GameSession.Map?.SelectedConnection != null && - GameMain.GameSession.Map.CurrentLocation.AvailableMissions.Contains(selectedMission))); - - GameMain.GameSession.Map.CurrentLocation.SelectedMission = selectedMission; - - var selectedTickBoxIndex = (missionRadioButtonGroup.UserData as List).FindIndex(m => m == selectedMission); - if (selectedTickBoxIndex >= 0) + StartButton = new GUIButton(new RectTransform(new Vector2(0.5f, 0.1f), content.RectTransform), + TextManager.Get("StartCampaignButton"), style: "GUIButtonLarge") { - missionRadioButtonGroup.Selected = selectedTickBoxIndex; - } - - selectedMissionInfo.ClearChildren(); - var container = selectedMissionInfo.Content; - selectedMissionInfo.Visible = selectedMission != null; - selectedMissionInfo.Spacing = (int)(10 * GUI.Scale); - if (selectedMission == null) { return; } - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), container.RectTransform), - selectedMission.Name, font: GUI.LargeFont) - { - AutoScaleHorizontal = true, - CanBeFocused = false - }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), container.RectTransform), - TextManager.GetWithVariable("Reward", "[reward]", selectedMission.Reward.ToString())) - { - CanBeFocused = false - }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), container.RectTransform), - selectedMission.Description, wrap: true) - { - CanBeFocused = false + OnClicked = (GUIButton btn, object obj) => { StartRound?.Invoke(); return true; }, + Enabled = true, + Visible = Campaign.AllowedToEndRound() }; - //scale down mission info box if it's much taller than the text - float missionInfoHeight = selectedMissionInfo.Content.Children.Sum(c => c.Rect.Height + selectedMissionInfo.Spacing); - selectedMissionInfo.Content.Children.ForEach(c => c.RectTransform.IsFixedSize = true); - selectedMissionInfo.RectTransform.Resize(new Point(selectedMissionInfo.Rect.Width, (int)(missionInfoHeight + 15 * GUI.Scale))); - selectedMissionInfo.UpdateScrollBarSize(); - - if (StartButton != null) + if (Level.Loaded != null && + connection.LevelData == Level.Loaded.LevelData && + currentDisplayLocation == Campaign.Map?.CurrentLocation) { - StartButton.Enabled = true; - StartButton.Visible = GameMain.Client == null || - GameMain.Client.HasPermission(Networking.ClientPermissions.ManageRound) || - GameMain.Client.HasPermission(Networking.ClientPermissions.ManageCampaign); + StartButton.Visible = false; + missionList.Enabled = false; } } - private GUIComponent CreateItemFrame(PurchasedItem pi, PriceInfo priceInfo, GUIListBox listBox) - { - GUIFrame frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), listBox.Content.RectTransform), style: "ListBoxElement") - { - UserData = pi, - ToolTip = pi.ItemPrefab.Description - }; - frame.RectTransform.MinSize = new Point(0, (int)(GUI.Scale * 50)); - - var content = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 1.0f), frame.RectTransform, Anchor.Center), - isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - AbsoluteSpacing = (int)(5 * GUI.Scale), - Stretch = true - }; - - ScalableFont font = listBox.Content.Rect.Width < 280 ? GUI.SmallFont : GUI.Font; - - Sprite itemIcon = pi.ItemPrefab.InventoryIcon ?? pi.ItemPrefab.sprite; - if (itemIcon != null) - { - GUIImage img = new GUIImage(new RectTransform(new Point((int)(content.Rect.Height * 0.8f)), content.RectTransform), itemIcon, scaleToFit: true) - { - Color = itemIcon == pi.ItemPrefab.InventoryIcon ? pi.ItemPrefab.InventoryIconColor : pi.ItemPrefab.SpriteColor - }; - img.RectTransform.MaxSize = img.Rect.Size; - //img.Scale = Math.Min(Math.Min(40.0f / img.SourceRect.Width, 40.0f / img.SourceRect.Height), 1.0f); - } - - GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), content.RectTransform), - pi.ItemPrefab.Name, font: font) - { - ToolTip = pi.ItemPrefab.Description - }; - - new GUITextBlock(new RectTransform(new Vector2(0.2f, 1.0f), content.RectTransform), - priceInfo.BuyPrice.ToString(), font: font, textAlignment: Alignment.CenterRight) - { - ToolTip = pi.ItemPrefab.Description - }; - - //If its the store menu, quantity will always be 0 - GUINumberInput amountInput = null; - if (pi.Quantity > 0) - { - amountInput = new GUINumberInput(new RectTransform(new Vector2(0.3f, 1.0f), content.RectTransform), - GUINumberInput.NumberType.Int) - { - MinValueInt = 0, - MaxValueInt = CargoManager.MaxQuantity, - UserData = pi, - IntValue = pi.Quantity - }; - amountInput.TextBox.OnSelected += (sender, key) => { suppressBuySell = true; }; - amountInput.TextBox.OnDeselected += (sender, key) => { suppressBuySell = false; amountInput.OnValueChanged?.Invoke(amountInput); }; - amountInput.OnValueChanged += (numberInput) => - { - if (suppressBuySell) { return; } - PurchasedItem purchasedItem = numberInput.UserData as PurchasedItem; - if (GameMain.Client != null && !GameMain.Client.HasPermission(Networking.ClientPermissions.ManageCampaign)) - { - numberInput.IntValue = purchasedItem.Quantity; - return; - } - //Attempting to buy - if (numberInput.IntValue > purchasedItem.Quantity) - { - int quantity = numberInput.IntValue - purchasedItem.Quantity; - //Cap the numberbox based on the amount we can afford. - quantity = Campaign.Money <= 0 ? - 0 : Math.Min((int)(Campaign.Money / (float)priceInfo.BuyPrice), quantity); - for (int i = 0; i < quantity; i++) - { - BuyItem(numberInput, purchasedItem); - } - } - //Attempting to sell - else - { - int quantity = purchasedItem.Quantity - numberInput.IntValue; - for (int i = 0; i < quantity; i++) - { - SellItem(numberInput, purchasedItem); - } - } - }; - frame.HoverColor = frame.SelectedColor = Color.Transparent; - } - listBox.RecalculateChildren(); - content.Recalculate(); - content.RectTransform.RecalculateChildren(true, true); - amountInput?.LayoutGroup.Recalculate(); - textBlock.Text = ToolBox.LimitString(textBlock.Text, textBlock.Font, textBlock.Rect.Width); - //content.RectTransform.IsFixedSize = true; - content.RectTransform.Children.ForEach(c => c.IsFixedSize = true); - - return frame; - } - - private bool BuyItem(GUIComponent component, object obj) - { - if (!(obj is PurchasedItem pi) || pi.ItemPrefab == null) { return false; } - - if (GameMain.Client != null && !GameMain.Client.HasPermission(Networking.ClientPermissions.ManageCampaign)) - { - return false; - } - - var purchasedItem = Campaign.CargoManager.PurchasedItems.Find(pi2 => pi2.ItemPrefab == pi.ItemPrefab); - if (purchasedItem != null && purchasedItem.Quantity >= CargoManager.MaxQuantity) { return false; } - - PriceInfo priceInfo = pi.ItemPrefab.GetPrice(Campaign.Map.CurrentLocation); - if (priceInfo == null || priceInfo.BuyPrice > Campaign.Money) { return false; } - - Campaign.CargoManager.PurchaseItem(pi.ItemPrefab, 1); - GameMain.Client?.SendCampaignState(); - - return false; - } - - private bool SellItem(GUIComponent component, object obj) - { - if (!(obj is PurchasedItem pi) || pi.ItemPrefab == null) { return false; } - - if (GameMain.Client != null && !GameMain.Client.HasPermission(Networking.ClientPermissions.ManageCampaign)) - { - return false; - } - - Campaign.CargoManager.SellItem(pi, 1); - GameMain.Client?.SendCampaignState(); - - return false; - } - - private bool suppressBuySell; - - private void RefreshMyItems() - { - HashSet existingItemFrames = new HashSet(); - foreach (PurchasedItem pi in Campaign.CargoManager.PurchasedItems) - { - var itemFrame = myItemList.Content.Children.FirstOrDefault(c => - c.UserData is PurchasedItem pi2 && pi.ItemPrefab == pi2.ItemPrefab); - if (itemFrame == null) - { - var priceInfo = pi.ItemPrefab.GetPrice(Campaign.Map.CurrentLocation); - if (priceInfo == null) { continue; } - itemFrame = CreateItemFrame(pi, priceInfo, myItemList); - itemFrame.Flash(GUI.Style.Green); - } - else - { - itemFrame.UserData = itemFrame.GetChild(0).GetChild().UserData = pi; - } - existingItemFrames.Add(itemFrame); - - suppressBuySell = true; - var numInput = itemFrame.GetChild(0).GetChild(); - if (numInput.IntValue != pi.Quantity) { itemFrame.Flash(GUI.Style.Green); } - numInput.IntValue = (itemFrame.UserData as PurchasedItem).Quantity = pi.Quantity; - suppressBuySell = false; - } - - var removedItemFrames = myItemList.Content.Children.Except(existingItemFrames).ToList(); - foreach (GUIComponent removedItemFrame in removedItemFrames) - { - myItemList.Content.RemoveChild(removedItemFrame); - } - - myItemList.Content.RectTransform.SortChildren((x, y) => - (x.GUIComponent.UserData as PurchasedItem).ItemPrefab.Name.CompareTo((y.GUIComponent.UserData as PurchasedItem).ItemPrefab.Name)); - myItemList.Content.RectTransform.SortChildren((x, y) => - (x.GUIComponent.UserData as PurchasedItem).ItemPrefab.Category.CompareTo((y.GUIComponent.UserData as PurchasedItem).ItemPrefab.Category)); - myItemList.UpdateScrollBarSize(); - } - - public void SelectTab(Tab tab) + public void SelectTab(CampaignMode.InteractionType tab) { selectedTab = tab; for (int i = 0; i < tabs.Length; i++) @@ -1056,23 +516,18 @@ namespace Barotrauma } } - missionPanel.Visible = tab == Tab.Map && selectedLocation != null; - - foreach (GUIButton button in tabButtons) - { - button.Selected = (Tab)button.UserData == tab; - } + locationInfoPanel.Visible = tab == CampaignMode.InteractionType.Map && selectedLocation != null; switch (selectedTab) { - case Tab.Repair: - repairHullsButton.Enabled = + case CampaignMode.InteractionType.Repair: + repairHullsButton.Enabled = (Campaign.PurchasedHullRepairs || Campaign.Money >= CampaignMode.HullRepairCost) && - (GameMain.Client == null || GameMain.Client.HasPermission(Networking.ClientPermissions.ManageCampaign)); + Campaign.AllowedToManageCampaign(); repairHullsButton.GetChild().Selected = Campaign.PurchasedHullRepairs; - repairItemsButton.Enabled = + repairItemsButton.Enabled = (Campaign.PurchasedItemRepairs || Campaign.Money >= CampaignMode.ItemRepairCost) && - (GameMain.Client == null || GameMain.Client.HasPermission(Networking.ClientPermissions.ManageCampaign)); + Campaign.AllowedToManageCampaign(); repairItemsButton.GetChild().Selected = Campaign.PurchasedItemRepairs; if (GameMain.GameSession?.SubmarineInfo == null || !GameMain.GameSession.SubmarineInfo.SubsLeftBehind) @@ -1084,172 +539,27 @@ namespace Barotrauma { replaceShuttlesButton.Enabled = (Campaign.PurchasedLostShuttles || Campaign.Money >= CampaignMode.ShuttleReplaceCost) && - (GameMain.Client == null || GameMain.Client.HasPermission(Networking.ClientPermissions.ManageCampaign)); + Campaign.AllowedToManageCampaign(); replaceShuttlesButton.GetChild().Selected = Campaign.PurchasedLostShuttles; } break; + case CampaignMode.InteractionType.Store: + Store.RefreshItemsToSell(); + Store.Refresh(); + break; + case CampaignMode.InteractionType.Crew: + CrewManagement.UpdateCrew(); + break; + case CampaignMode.InteractionType.PurchaseSub: + if (submarineSelection == null) submarineSelection = new SubmarineSelection(false, () => Campaign.ShowCampaignUI = false, tabs[(int)CampaignMode.InteractionType.PurchaseSub].RectTransform); + submarineSelection.RefreshSubmarineDisplay(true); + break; } } - private void FillStoreItemList() + public static string GetMoney() { - float prevStoreItemScroll = storeItemList.BarScroll; - float prevMyItemScroll = myItemList.BarScroll; - - HashSet existingItemFrames = new HashSet(); - foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) - { - PriceInfo priceInfo = itemPrefab.GetPrice(Campaign.Map.CurrentLocation); - if (priceInfo == null) { continue; } - - var itemFrame = myItemList.Content.GetChildByUserData(priceInfo); - if (itemFrame == null) - { - itemFrame = CreateItemFrame(new PurchasedItem(itemPrefab, 0), priceInfo, storeItemList); - } - existingItemFrames.Add(itemFrame); - } - - var removedItemFrames = storeItemList.Content.Children.Except(existingItemFrames).ToList(); - foreach (GUIComponent removedItemFrame in removedItemFrames) - { - storeItemList.Content.RemoveChild(removedItemFrame); - } - - storeItemList.Content.RectTransform.SortChildren( - (x, y) => (x.GUIComponent.UserData as PurchasedItem).ItemPrefab.Name.CompareTo((y.GUIComponent.UserData as PurchasedItem).ItemPrefab.Name)); - - storeItemList.BarScroll = prevStoreItemScroll; - myItemList.BarScroll = prevMyItemScroll; + return TextManager.GetWithVariable("PlayerCredits", "[credits]", (GameMain.GameSession?.Campaign == null) ? "0" : string.Format(CultureInfo.InvariantCulture, "{0:N0}", GameMain.GameSession.Campaign.Money)); } - - private void FilterStoreItems(MapEntityCategory? category, string filter) - { - if (category.HasValue) - { - selectedItemCategory = category.Value; - } - foreach (GUIComponent child in storeItemList.Content.Children) - { - var item = child.UserData as PurchasedItem; - if (item?.ItemPrefab?.Name == null) { continue; } - child.Visible = - (!category.HasValue || item.ItemPrefab.Category.HasFlag(category.Value)) && - (string.IsNullOrEmpty(filter) || item.ItemPrefab.Name.ToLower().Contains(searchBox.Text.ToLower())); - } - foreach (GUIButton btn in itemCategoryButtons) - { - btn.Selected = (MapEntityCategory)btn.UserData == selectedItemCategory; - } - storeItemList.UpdateScrollBarSize(); - //storeItemList.BarScroll = 0.0f; - } - - public string GetMoney() - { - return TextManager.GetWithVariable("PlayerCredits", "[credits]", (GameMain.GameSession == null) ? "0" : string.Format(CultureInfo.InvariantCulture, "{0:N0}", Campaign.Money)); - } - - private bool SelectCharacter(GUIComponent component, object selection) - { - GUIComponent prevInfoFrame = null; - foreach (GUIComponent child in tabs[(int)selectedTab].Children) - { - if (!(child.UserData is CharacterInfo)) { continue; } - - prevInfoFrame = child; - } - - if (prevInfoFrame != null) { tabs[(int)selectedTab].RemoveChild(prevInfoFrame); } - - if (!(selection is CharacterInfo characterInfo)) { return false; } - if (Character.Controlled != null && characterInfo == Character.Controlled.Info) { return false; } - - if (characterPreviewFrame == null || characterPreviewFrame.UserData != characterInfo) - { - characterPreviewFrame = new GUIFrame(new RectTransform(new Vector2(0.75f, 0.5f), tabs[(int)selectedTab].RectTransform, Anchor.TopRight, Pivot.TopLeft)) - { - UserData = characterInfo - }; - var characterPreviewContent = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.8f), characterPreviewFrame.RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.02f) }, style: null); - - characterInfo.CreateInfoFrame(characterPreviewContent, true); - } - - var currentCrew = GameMain.GameSession.CrewManager.GetCharacterInfos(); - if (currentCrew.Contains(characterInfo)) - { - new GUIButton(new RectTransform(new Vector2(0.5f, 0.1f), characterPreviewFrame.RectTransform, Anchor.BottomCenter) { RelativeOffset = new Vector2(0.0f, 0.05f) }, - TextManager.Get("FireButton")) - { - Color = GUI.Style.Red, - UserData = characterInfo, - Enabled = currentCrew.Count() > 1, //can't fire if there's only one character in the crew - OnClicked = (btn, obj) => - { - var confirmDialog = new GUIMessageBox( - TextManager.Get("FireWarningHeader"), - TextManager.GetWithVariable("FireWarningText", "[charactername]", ((CharacterInfo)obj).Name), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); - confirmDialog.Buttons[0].UserData = (CharacterInfo)obj; - confirmDialog.Buttons[0].OnClicked = FireCharacter; - confirmDialog.Buttons[0].OnClicked += confirmDialog.Close; - confirmDialog.Buttons[1].OnClicked = confirmDialog.Close; - return true; - } - }; - } - else - { - new GUIButton(new RectTransform(new Vector2(0.5f, 0.1f), characterPreviewFrame.RectTransform, Anchor.BottomCenter) { RelativeOffset = new Vector2(0.0f, 0.05f) }, - TextManager.Get("HireButton")) - { - Enabled = Campaign.Money >= characterInfo.Salary, - UserData = characterInfo, - OnClicked = HireCharacter - }; - } - - return true; - } - - private bool HireCharacter(GUIButton button, object selection) - { - if (!(selection is CharacterInfo characterInfo)) { return false; } - - if (!(Campaign is SinglePlayerCampaign spCampaign)) - { - DebugConsole.ThrowError("Characters can only be hired in the single player campaign.\n" + Environment.StackTrace); - return false; - } - - if (spCampaign.TryHireCharacter(GameMain.GameSession.Map.CurrentLocation, characterInfo)) - { - UpdateLocationView(GameMain.GameSession.Map.CurrentLocation); - SelectCharacter(null, null); - characterList.Content.RemoveChild(characterList.Content.FindChild(characterInfo)); - UpdateCharacterLists(); - } - - return false; - } - - private bool FireCharacter(GUIButton button, object selection) - { - if (!(selection is CharacterInfo characterInfo)) return false; - - if (!(Campaign is SinglePlayerCampaign spCampaign)) - { - DebugConsole.ThrowError("Characters can only be fired in the single player campaign.\n" + Environment.StackTrace); - return false; - } - - spCampaign.FireCharacter(characterInfo); - SelectCharacter(null, null); - UpdateCharacterLists(); - - return false; - } - } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index b2af0381d..d177ab06c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -159,7 +159,7 @@ namespace Barotrauma.CharacterEditor OnPostSpawn(); } OpenDoors(); - GameMain.Instance.OnResolutionChanged += OnResolutionChanged; + GameMain.Instance.ResolutionChanged += OnResolutionChanged; Instance = this; if (!GameMain.Config.EditorDisclaimerShown) @@ -266,7 +266,7 @@ namespace Barotrauma.CharacterEditor Reset(Character.CharacterList.Where(c => VanillaCharacters.Any(vchar => vchar == c.ConfigPath))); #endif } - GameMain.Instance.OnResolutionChanged -= OnResolutionChanged; + GameMain.Instance.ResolutionChanged -= OnResolutionChanged; GameMain.LightManager.LightingEnabled = true; ClearWidgets(); ClearSelection(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CreditsPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CreditsPlayer.cs index 7e0cdd03a..18ae1af3d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CreditsPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CreditsPlayer.cs @@ -12,9 +12,39 @@ namespace Barotrauma private float scrollSpeed; + public bool AutoRestart = true; + + public bool Finished + { + get { return listBox.BarScroll >= 1.0f; } + } + + public bool ScrollBarEnabled + { + get { return listBox.ScrollBarEnabled; } + set { listBox.ScrollBarEnabled = value; } + } + + public bool AllowMouseWheelScroll + { + get { return listBox.AllowMouseWheelScroll; } + set { listBox.AllowMouseWheelScroll = value; } + } + + public float Scroll + { + get { return listBox.BarScroll; } + set { listBox.BarScroll = value; } + } + + public CreditsPlayer(RectTransform rectT, string configFile) : base(null, rectT) { - GameMain.Instance.OnResolutionChanged += () => { ClearChildren(); Load(); }; + GameMain.Instance.ResolutionChanged += () => + { + ClearChildren(); + Load(); + }; var doc = XMLExtensions.TryLoadXml(configFile); if (doc == null) { return; } @@ -35,7 +65,7 @@ namespace Barotrauma foreach (XElement subElement in configElement.Elements()) { - GUIComponent.FromXML(subElement, listBox.Content.RectTransform); + FromXML(subElement, listBox.Content.RectTransform); } foreach (GUIComponent child in listBox.Content.Children) { @@ -53,9 +83,11 @@ namespace Barotrauma protected override void Update(float deltaTime) { + if (!Visible) { return; } + listBox.BarScroll += scrollSpeed / listBox.TotalSize * deltaTime; - if (listBox.BarScroll >= 1.0f) + if (AutoRestart && listBox.BarScroll >= 1.0f) { listBox.BarScroll = 0.0f; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EditorNode.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EditorNode.cs new file mode 100644 index 000000000..5e92d8477 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EditorNode.cs @@ -0,0 +1,662 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Xml.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace Barotrauma +{ + internal class EditorNode + { + public Vector2 Position { get; set; } + + public Vector2 Size { get; set; } + + public int ID; + + private const int HeaderSize = 32; + + public Rectangle HeaderRectangle => new Rectangle(Position.ToPoint(), new Point((int) Size.X, HeaderSize)); + public Rectangle Rectangle => new Rectangle(new Point((int) Position.X, (int) Position.Y + HeaderSize), Size.ToPoint()); + public string Name { get; protected set; } + + public bool CanAddConnections { get; set; } + + public readonly List Connections = new List(); + + public readonly List RemovableTypes = new List(); + + public bool IsHighlighted; + + public bool IsSelected; + + protected EditorNode(string name) + { + Name = name; + Position = Vector2.Zero; + } + + public virtual XElement Save() + { + throw new NotImplementedException(); + } + + public XElement SaveConnections() + { + XElement allConnections = new XElement("Connections", new XAttribute("i", ID)); + foreach (NodeConnection connection in Connections) + { + XElement connectionElement = new XElement("Connection"); + connectionElement.Add(new XAttribute("i", connection.ID)); + connectionElement.Add(new XAttribute("type", connection.Type.Label)); + + if (connection.EndConversation) + { + connectionElement.Add(new XAttribute("endconversation", connection.EndConversation)); + } + + if (!string.IsNullOrWhiteSpace(connection.OptionText)) + { + connectionElement.Add(new XAttribute("optiontext", connection.OptionText)); + } + + if (!string.IsNullOrWhiteSpace(connection.OverrideValue?.ToString())) + { + connectionElement.Add(new XAttribute("overridevalue", connection.OverrideValue?.ToString())); + connectionElement.Add(new XAttribute("valuetype", connection.OverrideValue?.GetType().ToString())); + } + + foreach (var nodeConnection in connection.ConnectedTo) + { + XElement connectedTo = new XElement("ConnectedTo", + new XAttribute("i", nodeConnection.ID), + new XAttribute("node", nodeConnection.Parent.ID)); + connectionElement.Add(connectedTo); + } + + allConnections.Add(connectionElement); + } + + return allConnections; + } + + public void LoadConnections(XElement element) + { + foreach (XElement subElement in element.Elements()) + { + int id = subElement.GetAttributeInt("i", -1); + string? connectionType = subElement.GetAttributeString("type", null); + bool endConversation = subElement.GetAttributeBool("endconversation", false); + + if (id < 0) { continue; } + + NodeConnection? connection = Connections.Find(c => c.ID == id); + if (connection == null) + { + if (string.Equals(connectionType, NodeConnectionType.Option.Label, StringComparison.InvariantCultureIgnoreCase)) + { + connection = new NodeConnection(this, NodeConnectionType.Option) { ID = id, EndConversation = endConversation }; + Connections.Add(connection); + } + else + { + continue; + } + } + + string? optionText = subElement.GetAttributeString("optiontext", null); + string? overrideValue = subElement.GetAttributeString("overridevalue", null); + string? valueType = subElement.GetAttributeString("valuetype", null); + + if (optionText != null) { connection.OptionText = optionText; } + + if (overrideValue != null && valueType != null) + { + Type? type = Type.GetType(valueType); + if (type != null) + { + if (type.IsEnum) + { + Array enums = Enum.GetValues(type); + foreach (object? @enum in enums) + { + if (string.Equals(@enum?.ToString(), overrideValue, StringComparison.InvariantCultureIgnoreCase)) + { + connection.OverrideValue = @enum; + } + } + } + else + { + connection.OverrideValue = Convert.ChangeType(overrideValue, type); + } + } + } + + foreach (XElement connectedTo in subElement.Elements()) + { + int id2 = connectedTo.GetAttributeInt("i", -1); + int node = connectedTo.GetAttributeInt("node", -1); + if (id2 < 0 || node < 0) { continue; } + + EditorNode? otherNode = EventEditorScreen.nodeList.Find(editorNode => editorNode.ID == node); + NodeConnection? otherConnection = otherNode?.Connections.Find(c => c.ID == id2); + if (otherConnection != null) + { + connection.ConnectedTo.Add(otherConnection); + } + } + } + } + + public static EditorNode? Load(XElement element) + { + return element.Name.ToString().ToLowerInvariant() switch + { + "eventnode" => EventNode.LoadEventNode(element), + "valuenode" => ValueNode.LoadValueNode(element), + "customnode" => CustomNode.LoadCustomNode(element), + _ => null + }; + } + + public virtual XElement? ToXML() + { + XElement newElement = new XElement(Name); + foreach (var connection in Connections) + { + if (connection.Type == NodeConnectionType.Value) + { + if (connection.GetValue() != null) + { + newElement.Add(new XAttribute(connection.Attribute?.ToLowerInvariant(), connection.GetValue())); + } + } + } + + newElement.Add(new XAttribute("_npos", XMLExtensions.Vector2ToString(Position))); + + return newElement; + } + + public void Connect(EditorNode otherNode, NodeConnectionType type) + { + NodeConnection? conn = Connections.Find(connection => connection.Type == type && !connection.ConnectedTo.Any()); + NodeConnection? found = otherNode.Connections.Find(connection => connection.Type == NodeConnectionType.Activate); + if (found != null) + { + conn?.ConnectedTo.Add(found); + } + } + + public void Connect(NodeConnection connection, NodeConnection ownConnection) + { + connection.ConnectedTo.Add(ownConnection); + } + + public void Disconnect(NodeConnection conn) + { + foreach (var connection in EventEditorScreen.nodeList.SelectMany(editorNode => editorNode.Connections.Where(connection => connection.ConnectedTo.Contains(conn)))) + { + connection.ConnectedTo.Remove(conn); + } + } + + public void ClearConnections() + { + foreach (NodeConnection conn in Connections) + { + conn.ClearConnections(); + } + } + + public virtual Rectangle GetDrawRectangle() + { + return Rectangle; + } + + public NodeConnection? GetConnectionOnMouse(Vector2 mousePos) + { + return Connections.FirstOrDefault(eventNodeConnection => eventNodeConnection.DrawRectangle.Contains(mousePos)); + } + + public void Draw(SpriteBatch spriteBatch) + { + DrawBack(spriteBatch); + DrawFront(spriteBatch); + } + + protected virtual void DrawFront(SpriteBatch spriteBatch) { } + + protected virtual Color BackgroundColor => new Color(150, 150, 150); + + private void DrawBack(SpriteBatch spriteBatch) + { + Color outlineColor = Color.White * 0.8f; + Color fontColor = Color.White; + Color headerColor = IsHighlighted ? new Color(100, 100, 100) : new Color(120, 120, 120); + if (IsSelected) + { + headerColor = new Color(80, 80, 80); + } + + float camZoom = Screen.Selected is EventEditorScreen eventEditor ? eventEditor.Cam.Zoom : 1.0f; + + Rectangle bodyRect = GetDrawRectangle(); + + GUI.DrawRectangle(spriteBatch, HeaderRectangle, headerColor, isFilled: true, depth: 1.0f); + GUI.DrawRectangle(spriteBatch, bodyRect, BackgroundColor, isFilled: true, depth: 1.0f); + + GUI.DrawRectangle(spriteBatch, HeaderRectangle, outlineColor, isFilled: false, depth: 1.0f, thickness: (int) Math.Max(1, 1.25f / camZoom)); + GUI.DrawRectangle(spriteBatch, bodyRect, outlineColor, isFilled: false, depth: 1.0f, thickness: (int) Math.Max(1, 1.25f / camZoom)); + + int x = 0, y = 0; + foreach (NodeConnection connection in Connections) + { + switch (connection.Type.NodeSide) + { + case NodeConnectionType.Side.Left: + connection.Draw(spriteBatch, Rectangle, y); + y++; + break; + case NodeConnectionType.Side.Right: + connection.Draw(spriteBatch, Rectangle, x); + x++; + break; + } + } + + Vector2 headerSize = GUI.SubHeadingFont.MeasureString(Name); + GUI.SubHeadingFont.DrawString(spriteBatch, Name, HeaderRectangle.Location.ToVector2() + (HeaderRectangle.Size.ToVector2() / 2) - (headerSize / 2), fontColor); + } + + public virtual void AddOption() + { + Connections.Add(new NodeConnection(this, NodeConnectionType.Option)); + } + + public void RemoveOption(NodeConnection connection) + { + int index = Connections.IndexOf(connection); + foreach (var nodeConnection in Connections.Skip(index)) + { + nodeConnection.ID--; + } + + Connections.Remove(connection); + } + + public EditorNode? GetNext() + { + var nextNode = Connections.Find(connection => connection.Type == NodeConnectionType.Next); + return nextNode?.ConnectedTo.FirstOrDefault()?.Parent; + } + + public EditorNode? GetNext(NodeConnectionType type) + { + var nextNode = Connections.Find(connection => connection.Type == type); + return nextNode?.ConnectedTo.FirstOrDefault()?.Parent; + } + + public static bool IsInstanceOf(Type type1, Type type2) + { + return type1.IsAssignableFrom(type2) || type1.IsSubclassOf(type2); + } + + public EditorNode? GetParent() + { + var myNode = Connections.Find(connection => connection.Type == NodeConnectionType.Activate); + if (myNode == null) { return null; } + + foreach (EditorNode editorNode in EventEditorScreen.nodeList) + { + List childConnection = editorNode.Connections.Where(connection => connection.Type == NodeConnectionType.Next || + connection.Type == NodeConnectionType.Option || + connection.Type == NodeConnectionType.Failure || + connection.Type == NodeConnectionType.Success || + connection.Type == NodeConnectionType.Add).ToList(); + if (childConnection.Any(connection => connection != null && connection.ConnectedTo.Contains(myNode))) + { + return editorNode; + } + } + + return null; + } + } + + internal class EventNode : EditorNode + { + private readonly Type type; + + public EventNode(Type type, string name) : base(name) + { + this.type = type; + Size = new Vector2(256, 256); + PropertyInfo[] properties = type.GetProperties().Where(info => info.CustomAttributes.Any(data => data.AttributeType == typeof(Serialize))).ToArray(); + + Connections.Add(new NodeConnection(this, NodeConnectionType.Activate)); + Connections.Add(new NodeConnection(this, NodeConnectionType.Next)); + + foreach (PropertyInfo property in properties) + { + Connections.Add(new NodeConnection(this, NodeConnectionType.Value, property.Name, property.PropertyType, property)); + } + + if (IsInstanceOf(type, typeof(BinaryOptionAction))) + { + Connections.Add(new NodeConnection(this, NodeConnectionType.Success)); + Connections.Add(new NodeConnection(this, NodeConnectionType.Failure)); + } + + if (IsInstanceOf(type, typeof(ConversationAction))) + { + CanAddConnections = true; + RemovableTypes.Add(NodeConnectionType.Option); + } + + if (IsInstanceOf(type, typeof(StatusEffectAction)) || IsInstanceOf(type, typeof(MissionAction))) + { + Connections.Add(new NodeConnection(this, NodeConnectionType.Add)); + } + } + + public override XElement Save() + { + XElement newElement = new XElement(nameof(EventNode), + new XAttribute("i", ID), + new XAttribute("type", type.ToString()), + new XAttribute("name", Name), + new XAttribute("xpos", Position.X), + new XAttribute("ypos", Position.Y)); + + return newElement; + } + + public static EditorNode? LoadEventNode(XElement element) + { + if (!string.Equals(element.Name.ToString(), nameof(EventNode), StringComparison.InvariantCultureIgnoreCase)) { return null; } + + Type? t = Type.GetType(element.GetAttributeString("type", string.Empty)); + if (t == null) { return null; } + + EventNode newNode = new EventNode(t, element.GetAttributeString("name", string.Empty)) { ID = element.GetAttributeInt("i", -1) }; + float posX = element.GetAttributeFloat("xpos", 0f); + float posY = element.GetAttributeFloat("ypos", 0f); + newNode.Position = new Vector2(posX, posY); + return newNode; + } + + public override Rectangle GetDrawRectangle() + { + return ScaleRectFromConnections(Connections, Rectangle); + } + + public static Rectangle ScaleRectFromConnections(List connections, Rectangle baseRect) + { + // determine how big this box should get based on how many input/output nodes the sides have + int y = connections.Count(connection => connection.Type.NodeSide == NodeConnectionType.Side.Left), + x = connections.Count(connection => connection.Type.NodeSide == NodeConnectionType.Side.Right); + int maxHeight = Math.Max(x, y); + + Rectangle bodyRect = baseRect; + bodyRect.Height = bodyRect.Height / 8 * maxHeight; + return bodyRect; + } + + public Tuple[] GetOptions() + { + IEnumerable myNode = Connections.Where(connection => connection.Type == NodeConnectionType.Option).ToArray(); + List> list = new List>(); + if (myNode != null) + { + foreach (NodeConnection connection in myNode) + { + if (connection.ConnectedTo.Any()) + { + foreach (NodeConnection nodeConnection in connection.ConnectedTo) + { + list.Add(Tuple.Create((EditorNode?) nodeConnection.Parent, connection.OptionText, connection.EndConversation)); + } + } + else + { + list.Add(Tuple.Create(null, connection.OptionText, connection.EndConversation)); + } + } + } + + return list.ToArray(); + } + } + + internal class ValueNode : EditorNode + { + private object? nodeValue; + + public object? Value + { + get => nodeValue; + set + { + nodeValue = value; + if (value is string str) + { + WrappedText = TextManager.Get(str, true) is { } translated ? translated : str; + } + else + { + WrappedText = value?.ToString() ?? string.Empty; + } + valueTextSize = GUI.SubHeadingFont.MeasureString(WrappedText); + } + } + + private Vector2 valueTextSize = Vector2.Zero; + + public Type Type { get; } + + public ValueNode(Type type, string name) : base(name) + { + Type = type; + Value = type.IsValueType ? Activator.CreateInstance(type) : null; + Size = new Vector2(256, 32); + Connections.Add(new NodeConnection(this, NodeConnectionType.Out, "Output", Type)); + } + + public override XElement Save() + { + XElement newElement = new XElement(nameof(ValueNode)); + newElement.Add(new XAttribute("i", ID)); + if (Value != null) + { + newElement.Add(new XAttribute("value", Value)); + } + + newElement.Add(new XAttribute("type", Type.ToString())); + newElement.Add(new XAttribute("name", Name)); + newElement.Add(new XAttribute("xpos", Position.X)); + newElement.Add(new XAttribute("ypos", Position.Y)); + return newElement; + } + + public override XElement? ToXML() { return null; } + + public static EditorNode? LoadValueNode(XElement element) + { + if (!string.Equals(element.Name.ToString(), nameof(ValueNode), StringComparison.InvariantCultureIgnoreCase)) { return null; } + + string? value = element.GetAttributeString("value", null); + Type? type = Type.GetType(element.GetAttributeString("type", string.Empty)); + if (type != null) + { + ValueNode newNode = new ValueNode(type, element.GetAttributeString("name", string.Empty)) { ID = element.GetAttributeInt("i", -1) }; + float posX = element.GetAttributeFloat("xpos", 0f); + float posY = element.GetAttributeFloat("ypos", 0f); + newNode.Position = new Vector2(posX, posY); + + if (value != null) + { + if (type.IsEnum) + { + Array enums = Enum.GetValues(type); + foreach (object? @enum in enums) + { + if (string.Equals(@enum?.ToString(), value, StringComparison.InvariantCultureIgnoreCase)) + { + newNode.Value = @enum; + } + } + } + else + { + newNode.Value = Convert.ChangeType(value, type); + } + } + + return newNode; + } + + return null; + } + + protected override Color BackgroundColor => new Color(50, 50, 50); + + private string? wrappedText; + + private string? WrappedText + { + get => wrappedText; + set + { + string valueText = value ?? "null"; + int width = Rectangle.Width; + if (width == 0) + { + wrappedText = valueText; + return; + } + + if (width > 16) + { + width -= 16; + } + + valueText = ToolBox.WrapText(valueText, width, GUI.SubHeadingFont); + wrappedText = valueText; + } + } + + public override Rectangle GetDrawRectangle() + { + Rectangle drawRectangle = Rectangle; + Vector2 size = GUI.SubHeadingFont.MeasureString(WrappedText); + drawRectangle.Height = (int) Math.Max(size.Y + 16, drawRectangle.Height); + return drawRectangle; + } + + protected override void DrawFront(SpriteBatch spriteBatch) + { + base.DrawFront(spriteBatch); + 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); + } + } + + class SpecialNode : EditorNode + { + public SpecialNode(string name) : base(name) + { + Size = new Vector2(256, 256); + } + + public override Rectangle GetDrawRectangle() + { + return EventNode.ScaleRectFromConnections(Connections, Rectangle); + } + } + + class CustomNode : SpecialNode + { + public CustomNode(string name) : base(name) + { + CanAddConnections = true; + RemovableTypes.Add(NodeConnectionType.Value); + Connections.Add(new NodeConnection(this, NodeConnectionType.Activate)); + Connections.Add(new NodeConnection(this, NodeConnectionType.Next)); + Connections.Add(new NodeConnection(this, NodeConnectionType.Add)); + } + + public CustomNode() : this("Custom") + { + Prompt(s => + { + Name = s; + return true; + }); + } + + public override void AddOption() + { + Prompt(s => + { + Connections.Add(new NodeConnection(this, NodeConnectionType.Value, s, typeof(string))); + return true; + }); + } + + public override XElement Save() + { + XElement newElement = new XElement(nameof(CustomNode)); + newElement.Add(new XAttribute("i", ID)); + newElement.Add(new XAttribute("name", Name)); + newElement.Add(new XAttribute("xpos", Position.X)); + newElement.Add(new XAttribute("ypos", Position.Y)); + foreach (NodeConnection connection in Connections.FindAll(connection => connection.Type == NodeConnectionType.Value)) + { + newElement.Add(new XElement("Value", new XAttribute("name", connection.Attribute))); + } + return newElement; + } + + public static EditorNode? LoadCustomNode(XElement element) + { + if (!string.Equals(element.Name.ToString(), nameof(CustomNode), StringComparison.OrdinalIgnoreCase)) { return null; } + + CustomNode newNode = new CustomNode(element.GetAttributeString("name", string.Empty)) { ID = element.GetAttributeInt("i", -1) }; + float posX = element.GetAttributeFloat("xpos", 0f); + float posY = element.GetAttributeFloat("ypos", 0f); + newNode.Position = new Vector2(posX, posY); + foreach (XElement valueElement in element.Elements()) + { + newNode.Connections.Add(new NodeConnection(newNode, NodeConnectionType.Value, valueElement.GetAttributeString("name", string.Empty), typeof(string))); + } + return newNode; + } + + private static void Prompt(Func OnAccepted) + { + var msgBox = new GUIMessageBox(TextManager.Get("Name"), "", new[] { TextManager.Get("Ok"), TextManager.Get("Cancel") }, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175)); + var layout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.25f), msgBox.Content.RectTransform), isHorizontal: true); + GUITextBox nameInput = new GUITextBox(new RectTransform(Vector2.One, layout.RectTransform)); + + msgBox.Buttons[1].OnClicked = delegate + { + msgBox.Close(); + return true; + }; + + msgBox.Buttons[0].OnClicked = delegate + { + OnAccepted.Invoke(nameInput.Text); + msgBox.Close(); + return true; + }; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs new file mode 100644 index 000000000..e0d8b17ee --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs @@ -0,0 +1,1077 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Xml.Linq; +using Barotrauma.Extensions; +using Barotrauma.IO; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using Directory = System.IO.Directory; + +namespace Barotrauma +{ + internal class EventEditorScreen : Screen + { + private GUIFrame GuiFrame = null!; + + private GUIListBox? contextMenu; + + public override Camera Cam { get; } + public static string? DrawnTooltip { get; set; } + + public static readonly List nodeList = new List(); + + private readonly List selectedNodes = new List(); + + public static Vector2 DraggingPosition = Vector2.Zero; + public static NodeConnection? DraggedConnection; + + private EditorNode? draggedNode; + private Vector2 dragOffset; + + private Dictionary markedNodes = new Dictionary(); + + private static string projectName = string.Empty; + + private int CreateID() + { + int maxId = nodeList.Any() ? nodeList.Max(node => node.ID) : 0; + return ++maxId; + } + + private Point screenResolution; + + public EventEditorScreen() + { + Cam = new Camera(); + nodeList.Clear(); + CreateGUI(); + } + + private void CreateGUI() + { + GuiFrame = new GUIFrame(new RectTransform(new Vector2(0.2f, 0.4f), GUICanvas.Instance) { MinSize = new Point(300, 400) }); + GUILayoutGroup layoutGroup = new GUILayoutGroup(RectTransform(0.9f, 0.9f, GuiFrame, Anchor.Center)) { Stretch = true }; + + // === BUTTONS === // + GUILayoutGroup buttonLayout = new GUILayoutGroup(RectTransform(1.0f, 0.50f, layoutGroup)) { RelativeSpacing = 0.04f }; + GUIButton newProjectButton = new GUIButton(RectTransform(1.0f, 0.33f, buttonLayout), TextManager.Get("EventEditor.NewProject")); + GUIButton saveProjectButton = new GUIButton(RectTransform(1.0f, 0.33f, buttonLayout), TextManager.Get("EventEditor.SaveProject")); + GUIButton loadProjectButton = new GUIButton(RectTransform(1.0f, 0.33f, buttonLayout), TextManager.Get("EventEditor.LoadProject")); + GUIButton exportProjectButton = new GUIButton(RectTransform(1.0f, 0.33f, buttonLayout), TextManager.Get("EventEditor.Export")); + + + // === 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); + + 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); + GUIButton loadButton = new GUIButton(RectTransform(0.2f, 1.0f, loadDropdownLayout), TextManager.Get("Load")); + + // === 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); + + 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); + GUIButton addActionButton = new GUIButton(RectTransform(0.2f, 1.0f, addActionDropdownLayout), TextManager.Get("EventEditor.Add")); + + // === 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); + + 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); + GUIButton addValueButton = new GUIButton(RectTransform(0.2f, 1.0f, addValueDropdownLayout), TextManager.Get("EventEditor.Add")); + + // === 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); + 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()) + { + loadDropdown.AddItem(eventPrefab.Identifier, eventPrefab); + } + + // Add all types that inherit the EventAction class + foreach (Type type in Assembly.GetExecutingAssembly().GetTypes().Where(type => type.IsSubclassOf(typeof(EventAction)))) + { + addActionDropdown.AddItem(type.Name, type); + } + + addSpecialDropdown.AddItem("Custom", typeof(CustomNode)); + + addValueDropdown.AddItem(nameof(Single), typeof(float)); + addValueDropdown.AddItem(nameof(Boolean), typeof(bool)); + addValueDropdown.AddItem(nameof(String), typeof(string)); + addValueDropdown.AddItem(nameof(SpawnType), typeof(SpawnType)); + addValueDropdown.AddItem(nameof(LimbType), typeof(LimbType)); + addValueDropdown.AddItem(nameof(ReputationAction.ReputationType), typeof(ReputationAction.ReputationType)); + addValueDropdown.AddItem(nameof(SpawnAction.SpawnLocationType), typeof(SpawnAction.SpawnLocationType)); + + loadButton.OnClicked += (button, o) => Load(loadDropdown.SelectedData as EventPrefab); + addActionButton.OnClicked += (button, o) => AddAction(addActionDropdown.SelectedData as Type); + addValueButton.OnClicked += (button, o) => AddValue(addValueDropdown.SelectedData as Type); + addSpecialButton.OnClicked += (button, o) => AddSpecial(addSpecialDropdown.SelectedData as Type); + exportProjectButton.OnClicked += ExportEventToFile; + saveProjectButton.OnClicked += SaveProjectToFile; + newProjectButton.OnClicked += TryCreateNewProject; + loadProjectButton.OnClicked += (button, o) => + { + FileSelection.OnFileSelected = (file) => + { + XDocument? document = XMLExtensions.TryLoadXml(file); + if (document?.Root != null) + { + Load(document.Root); + } + }; + + string directory = Path.GetFullPath("EventProjects"); + if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } + + FileSelection.ClearFileTypeFilters(); + FileSelection.AddFileTypeFilter("Scripted Event", "*.sevproj"); + FileSelection.SelectFileTypeFilter("*.sevproj"); + FileSelection.CurrentDirectory = directory; + FileSelection.Open = true; + return true; + }; + screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); + } + + private bool ExportEventToFile(GUIButton button, object o) + { + XElement? save = ExportXML(); + if (save != null) + { + try + { + string directory = Path.GetFullPath("EventProjects"); + if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } + + string exportPath = Path.Combine(directory, "Exported"); + if (!Directory.Exists(exportPath)) { Directory.CreateDirectory(exportPath); } + + var msgBox = new GUIMessageBox(TextManager.Get("EventEditor.ExportProjectPrompt"), "", new[] { TextManager.Get("Cancel"), TextManager.Get("EventEditor.Export") }, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175)); + var layout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.25f), msgBox.Content.RectTransform), isHorizontal: true); + GUITextBox nameInput = new GUITextBox(new RectTransform(Vector2.One, layout.RectTransform)) { Text = projectName }; + + // Cancel button + msgBox.Buttons[0].OnClicked = delegate + { + msgBox.Close(); + return true; + }; + + // Ok button + msgBox.Buttons[1].OnClicked = delegate + { + foreach (var illegalChar in Path.GetInvalidFileNameChars()) + { + if (!nameInput.Text.Contains(illegalChar)) { continue; } + + GUI.AddMessage(TextManager.GetWithVariable("SubNameIllegalCharsWarning", "[illegalchar]", illegalChar.ToString()), GUI.Style.Red); + return false; + } + + msgBox.Close(); + string path = Path.Combine(exportPath, $"{nameInput.Text}.xml"); + File.WriteAllText(path, save.ToString()); + AskForConfirmation(TextManager.Get("EventEditor.OpenTextHeader"), TextManager.Get("EventEditor.OpenTextBody"), () => + { + ToolBox.OpenFileWithShell(path); + return true; + }); + GUI.AddMessage($"XML exported to {path}", GUI.Style.Green); + return true; + }; + } + catch (Exception e) + { + DebugConsole.ThrowError("Failed to export event", e); + } + } + else + { + GUI.AddMessage("Unable to export because the project contains errors", GUI.Style.Red); + } + + return true; + } + + private bool TryCreateNewProject(GUIButton button, object o) + { + AskForConfirmation(TextManager.Get("EventEditor.NewProject"), TextManager.Get("EventEditor.NewProjectPrompt"), () => + { + nodeList.Clear(); + markedNodes.Clear(); + selectedNodes.Clear(); + projectName = TextManager.Get("EventEditor.Unnamed"); + return true; + }); + return true; + } + + public static GUIMessageBox AskForConfirmation(string header, string body, Func onConfirm) + { + string[] 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 + msgBox.Buttons[1].OnClicked = delegate + { + msgBox.Close(); + return true; + }; + + // Ok button + msgBox.Buttons[0].OnClicked = delegate + { + onConfirm.Invoke(); + msgBox.Close(); + return true; + }; + return msgBox; + } + + private void NotifyPrompt(string header, string 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 + { + msgBox.Close(); + return true; + }; + } + + private bool SaveProjectToFile(GUIButton button, object o) + { + string directory = Path.GetFullPath("EventProjects"); + + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + var msgBox = new GUIMessageBox(TextManager.Get("EventEditor.NameFilePrompt"), "", new[] { TextManager.Get("Cancel"), TextManager.Get("Save") }, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175)); + var layout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.25f), msgBox.Content.RectTransform), isHorizontal: true); + GUITextBox nameInput = new GUITextBox(new RectTransform(Vector2.One, layout.RectTransform)) { Text = projectName }; + + // Cancel button + msgBox.Buttons[0].OnClicked = delegate + { + msgBox.Close(); + return true; + }; + + // Ok button + msgBox.Buttons[1].OnClicked = delegate + { + foreach (var illegalChar in Path.GetInvalidFileNameChars()) + { + if (!nameInput.Text.Contains(illegalChar)) { continue; } + + GUI.AddMessage(TextManager.GetWithVariable("SubNameIllegalCharsWarning", "[illegalchar]", illegalChar.ToString()), GUI.Style.Red); + return false; + } + + msgBox.Close(); + projectName = nameInput.Text; + 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); + return true; + }; + return true; + } + + private bool Load(EventPrefab? prefab) + { + if (prefab == null) { return false; } + + AskForConfirmation(TextManager.Get("EventEditor.NewProject"), TextManager.Get("EventEditor.NewProjectPrompt"), () => + { + nodeList.Clear(); + selectedNodes.Clear(); + markedNodes.Clear(); + + bool hadNodes = true; + CreateNodes(prefab.ConfigElement, ref hadNodes); + if (!hadNodes) + { + NotifyPrompt(TextManager.Get("EventEditor.RandomGenerationHeader"), TextManager.Get("EventEditor.RandomGenerationBody")); + } + return true; + }); + return true; + } + + private bool AddAction(Type? type) + { + if (type == null) { return false; } + + Vector2 spawnPos = Cam.WorldViewCenter; + spawnPos.Y = -spawnPos.Y; + EventNode newNode = new EventNode(type, type.Name) { ID = CreateID() }; + newNode.Position = spawnPos - newNode.Size / 2; + nodeList.Add(newNode); + return true; + } + + private bool AddValue(Type? type) + { + if (type == null) { return false; } + + Vector2 spawnPos = Cam.WorldViewCenter; + spawnPos.Y = -spawnPos.Y; + ValueNode newValue = new ValueNode(type, type.Name) { ID = CreateID() }; + newValue.Position = spawnPos - newValue.Size / 2; + nodeList.Add(newValue); + return true; + } + + private bool AddSpecial(Type? type) + { + if (type == null) { return false; } + Vector2 spawnPos = Cam.WorldViewCenter; + spawnPos.Y = -spawnPos.Y; + + ConstructorInfo? constructor = type.GetConstructor(new Type[0]); + SpecialNode? newNode = null; + if (constructor != null) + { + newNode = constructor.Invoke(new object[0]) as SpecialNode; + } + if (newNode != null) + { + newNode.ID = CreateID(); + newNode.Position = spawnPos - newNode.Size / 2; + nodeList.Add(newNode); + return true; + } + return false; + } + + private void CreateNodes(XElement element, ref bool hadNodes, EditorNode? parent = null, int ident = 0) + { + EditorNode? lastNode = null; + foreach (XElement subElement in element.Elements()) + { + bool skip = true; + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "failure": + case "success": + case "option": + CreateNodes(subElement, ref hadNodes, parent, ident); + break; + default: + skip = false; + break; + } + + if (!skip) + { + Vector2 defaultNodePos = new Vector2(-16000, -16000); + EditorNode newNode; + Type? t = Type.GetType($"Barotrauma.{subElement.Name}"); + if (t != null && EditorNode.IsInstanceOf(t, typeof(EventAction))) + { + newNode = new EventNode(t, subElement.Name.ToString()) { Position = new Vector2(ident, 0), ID = CreateID() }; + } + else + { + newNode = new CustomNode(subElement.Name.ToString()) { Position = new Vector2(ident, 0), ID = CreateID() }; + foreach (XAttribute attribute in subElement.Attributes()) + { + newNode.Connections.Add(new NodeConnection(newNode, NodeConnectionType.Value, attribute.Name.ToString(), typeof(string))); + } + } + + Vector2 npos = subElement.GetAttributeVector2("_npos", defaultNodePos); + if (npos != defaultNodePos) + { + newNode.Position = npos; + } + else + { + hadNodes = false; + } + + XElement? parentElement = subElement.Parent; + + foreach (XElement xElement in subElement.Elements()) + { + if (xElement.Name.ToString().ToLowerInvariant() == "option") + { + NodeConnection optionConnection = new NodeConnection(newNode, NodeConnectionType.Option) + { + OptionText = xElement.GetAttributeString("text", string.Empty), + EndConversation = xElement.GetAttributeBool("endconversation", false) + }; + newNode.Connections.Add(optionConnection); + } + } + + foreach (NodeConnection connection in newNode.Connections) + { + if (connection.Type == NodeConnectionType.Value) + { + foreach (XAttribute attribute in subElement.Attributes()) + { + if (string.Equals(connection.Attribute, attribute.Name.ToString(), StringComparison.InvariantCultureIgnoreCase) && connection.ValueType != null) + { + if (connection.ValueType.IsEnum) + { + Array values = Enum.GetValues(connection.ValueType); + foreach (object? @enum in values) + { + if (string.Equals(@enum?.ToString(), attribute.Value, StringComparison.InvariantCultureIgnoreCase)) + { + connection.OverrideValue = @enum; + } + } + } + else + { + connection.OverrideValue = Convert.ChangeType(attribute.Value, connection.ValueType); + } + } + } + } + } + + if (npos == defaultNodePos) + { + hadNodes = false; + bool Predicate(EditorNode node) => Rectangle.Union(node.GetDrawRectangle(), node.HeaderRectangle).Intersects(Rectangle.Union(newNode.GetDrawRectangle(), newNode.HeaderRectangle)); + + while (nodeList.Any(Predicate)) + { + EditorNode? otherNode = nodeList.Find(Predicate); + if (otherNode != null) + { + newNode.Position += new Vector2(128, otherNode.GetDrawRectangle().Height + otherNode.HeaderRectangle.Height + new Random().Next(128, 256)); + } + } + } + + if (parentElement?.FirstElement() == subElement) + { + switch (parentElement?.Name.ToString().ToLowerInvariant()) + { + case "failure": + parent?.Connect(newNode, NodeConnectionType.Failure); + break; + case "success": + parent?.Connect(newNode, NodeConnectionType.Success); + break; + case "option": + if (parent != null) + { + NodeConnection? activateConnection = newNode.Connections.Find(connection => connection.Type == NodeConnectionType.Activate); + NodeConnection? optionConnection = parent.Connections.FirstOrDefault(connection => + connection.Type == NodeConnectionType.Option && string.Equals(connection.OptionText, parentElement.GetAttributeString("text", string.Empty), StringComparison.Ordinal)); + + if (activateConnection != null) + { + optionConnection?.ConnectedTo.Add(activateConnection); + } + } + break; + default: + parent?.Connect(newNode, NodeConnectionType.Add); + break; + } + } + else + { + lastNode?.Connect(newNode, NodeConnectionType.Next); + } + + lastNode = newNode; + nodeList.Add(newNode); + ident += 600; + CreateNodes(subElement, ref hadNodes, newNode, ident); + } + else + { + + } + } + } + + private static RectTransform RectTransform(float x, float y, GUIComponent parent, Anchor anchor = Anchor.TopRight) + { + return new RectTransform(new Vector2(x, y), parent.RectTransform, anchor); + } + + public override void Select() + { + Cam.Position = Vector2.Zero; + nodeList.Clear(); + projectName = TextManager.Get("EventEditor.Unnamed"); + base.Select(); + } + + public override void Deselect() + { + nodeList.Clear(); + base.Deselect(); + } + + public override void AddToGUIUpdateList() + { + GuiFrame.AddToGUIUpdateList(); + contextMenu?.AddToGUIUpdateList(); + } + + private XElement? ExportXML() + { + XElement mainElement = new XElement("ScriptedEvent", new XAttribute("identifier", projectName.RemoveWhitespace().ToLower())); + EditorNode? startNode = null; + foreach (EditorNode eventNode in nodeList.Where(node => node is EventNode || node is SpecialNode)) + { + if (eventNode.GetParent() == null) + { + if (startNode != null) + { + DebugConsole.ThrowError("You have more than one start node, only one will be picked while the others will get ignored."); + } + startNode ??= eventNode; + } + } + + if (startNode == null) { return null; } + + ExportChildNodes(startNode, mainElement); + + return mainElement; + } + + private void ExportChildNodes(EditorNode startNode, XElement parent) + { + XElement? newElement = startNode.ToXML(); + if (newElement == null) { return; } + parent.Add(newElement); + + EditorNode? success = startNode.GetNext(NodeConnectionType.Success); + EditorNode? failure = startNode.GetNext(NodeConnectionType.Failure); + EditorNode? add = startNode.GetNext(NodeConnectionType.Add); + Tuple[] options = startNode is EventNode eNode ? eNode.GetOptions() : new Tuple[0]; + + if (success != null) + { + XElement successElement = new XElement("Success"); + ExportChildNodes(success, successElement); + newElement.Add(successElement); + } + + if (failure != null) + { + XElement failureElement = new XElement("Failure"); + ExportChildNodes(failure, failureElement); + newElement.Add(failureElement); + } + + if (add is CustomNode custom) + { + ExportChildNodes(custom, newElement); + } + + foreach (var (node, text, end) in options) + { + XElement optionElement = new XElement("Option"); + optionElement.Add(new XAttribute("text", text)); + if (end) { optionElement.Add(new XAttribute("endconversation", true)); } + + if (node != null) + { + ExportChildNodes((EventNode) node, optionElement); + } + + newElement.Add(optionElement); + } + + EditorNode? next = startNode.GetNext(); + if (next != null) + { + ExportChildNodes(next, parent); + } + } + + private XElement SaveEvent(string name) + { + XElement mainElement = new XElement("SavedEvent", new XAttribute("name", name)); + XElement nodes = new XElement("Nodes"); + foreach (var editorNode in nodeList) + { + nodes.Add(editorNode.Save()); + } + + mainElement.Add(nodes); + + XElement connections = new XElement("AllConnections"); + foreach (var editorNode in nodeList) + { + connections.Add(editorNode.SaveConnections()); + } + + mainElement.Add(connections); + return mainElement; + } + + private void Load(XElement saveElement) + { + nodeList.Clear(); + projectName = saveElement.GetAttributeString("name", TextManager.Get("EventEditor.Unnamed")); + foreach (XElement element in saveElement.Elements()) + { + switch (element.Name.ToString().ToLowerInvariant()) + { + case "nodes": + { + foreach (XElement subElement in element.Elements()) + { + EditorNode? node = EditorNode.Load(subElement); + if (node != null) + { + nodeList.Add(node); + } + } + + break; + } + case "allconnections": + { + foreach (XElement subElement in element.Elements()) + { + int id = subElement.GetAttributeInt("i", -1); + EditorNode? node = nodeList.Find(editorNode => editorNode.ID == id); + node?.LoadConnections(subElement); + } + + break; + } + } + } + } + + private void CreateContextMenu(EditorNode node, NodeConnection? connection = null) + { + contextMenu = new GUIListBox(new RectTransform(new Vector2(0.1f, 0.1f), GUI.Canvas) { ScreenSpaceOffset = PlayerInput.MousePosition.ToPoint() }, style: "GUIToolTip") { Padding = new Vector4(5) }; + + new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), + TextManager.Get("EventEditor.Edit"), font: GUI.SmallFont) { UserData = "edit", Enabled = node is ValueNode || connection?.Type == NodeConnectionType.Value || connection?.Type == NodeConnectionType.Option }; + + new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), + TextManager.Get("EventEditor.MarkEnding"), font: GUI.SmallFont) { UserData = "markend", Enabled = connection != null && connection.Type == NodeConnectionType.Option }; + + new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), + TextManager.Get("EventEditor.RemoveConnection"), font: GUI.SmallFont) { UserData = "remcon", Enabled = connection != null }; + + new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), + TextManager.Get("EventEditor.AddOption"), font: GUI.SmallFont) { UserData = "addoption", Enabled = node.CanAddConnections }; + + new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), + TextManager.Get("EventEditor.RemoveOption"), font: GUI.SmallFont) { UserData = "removeoption", Enabled = connection != null && node.RemovableTypes.Contains(connection.Type) }; + + new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), + TextManager.Get("EventEditor.Delete"), font: GUI.SmallFont) { UserData = "delete", Enabled = true }; + + foreach (var guiComponent in contextMenu.Content.Children) + { + if (guiComponent is GUITextBlock child) + { + if (!child.Enabled) + { + child.TextColor *= 0.5f; + } + } + } + + foreach (GUIComponent c in contextMenu.Content.Children) + { + if (c is GUITextBlock block) + { + block.RectTransform.NonScaledSize = new Point((int) (block.TextSize.X + block.Padding.X * 2), (int) (18 * GUI.Scale)); + } + } + + int biggestSize = contextMenu.Content.Children.Max(c => c.Rect.Width + (int) contextMenu.Padding.X * 2); + contextMenu.Content.Children.ForEach(c => c.RectTransform.MinSize = new Point(biggestSize, c.Rect.Height)); + contextMenu.RectTransform.NonScaledSize = new Point(biggestSize, (int) (contextMenu.Content.Children.Sum(c => c.Rect.Height) + (contextMenu.Padding.X * 2))); + + contextMenu.OnSelected = (component, obj) => + { + if (!component.Enabled) { return false; } + + switch (obj as string) + { + case "edit": + CreateEditMenu(node as ValueNode, connection); + break; + case "markend" when connection != null: + connection.EndConversation = !connection.EndConversation; + break; + case "remcon" when connection != null: + connection.ClearConnections(); + connection.OverrideValue = null; + connection.OptionText = connection.OptionText; + break; + case "addoption": + node.AddOption(); + break; + case "removeoption": + connection?.Parent.RemoveOption(connection); + break; + case "delete": + nodeList.Remove(node); + node.ClearConnections(); + + break; + } + + contextMenu = null; + return true; + }; + } + + private static void CreateEditMenu(ValueNode? node, NodeConnection? connection = null) + { + object? newValue; + Type? type; + if (node != null) + { + newValue = node.Value; + type = node.Type; + } + else if (connection != null) + { + newValue = connection.OverrideValue; + type = connection.ValueType; + } + else + { + return; + } + + if (connection?.Type == NodeConnectionType.Option) + { + newValue = connection.OptionText; + type = typeof(string); + } + + if (type == null) { return; } + + Vector2 size = type == typeof(string) ? new Vector2(0.2f, 0.3f) : new Vector2(0.2f, 0.175f); + var msgBox = new GUIMessageBox(TextManager.Get("EventEditor.Edit"), "", new[] { TextManager.Get("Cancel"), TextManager.Get("OK") }, size, minSize: new Point(300, 175)); + + + Vector2 layoutSize = type == typeof(string) ? new Vector2(1f, 0.5f) : new Vector2(1f, 0.25f); + var layout = new GUILayoutGroup(new RectTransform(layoutSize, msgBox.Content.RectTransform), isHorizontal: true); + + 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); } + + valueInput.OnSelected += (component, o) => + { + newValue = o; + return true; + }; + } + else + { + if (type == typeof(string)) + { + GUIListBox listBox = new GUIListBox(new RectTransform(Vector2.One, layout.RectTransform)) { CanBeFocused = false }; + GUITextBox valueInput = new GUITextBox(new RectTransform(Vector2.One, listBox.Content.RectTransform, Anchor.TopRight), wrap: true, style: "GUITextBoxNoBorder"); + valueInput.OnTextChanged += (component, o) => + { + Vector2 textSize = valueInput.Font.MeasureString(valueInput.WrappedText); + valueInput.RectTransform.NonScaledSize = new Point(valueInput.RectTransform.NonScaledSize.X, (int) textSize.Y + 10); + listBox.UpdateScrollBarSize(); + listBox.BarScroll = 1.0f; + newValue = o; + return true; + }; + valueInput.Text = newValue?.ToString() ?? ""; + } + else if (type == typeof(float) || type == typeof(int)) + { + GUINumberInput valueInput = new GUINumberInput(new RectTransform(Vector2.One, layout.RectTransform), GUINumberInput.NumberType.Float) { FloatValue = (float) (newValue ?? 0.0f) }; + valueInput.OnValueChanged += component => { newValue = component.FloatValue; }; + } + else if (type == typeof(bool)) + { + GUITickBox valueInput = new GUITickBox(new RectTransform(Vector2.One, layout.RectTransform), "Value") { Selected = (bool) (newValue ?? false) }; + valueInput.OnSelected += component => + { + newValue = component.Selected; + return true; + }; + } + } + + // Cancel button + msgBox.Buttons[0].OnClicked = (button, o) => + { + msgBox.Close(); + return true; + }; + + // Ok button + msgBox.Buttons[1].OnClicked = (button, o) => + { + if (node != null) + { + node.Value = newValue; + } + else if (connection != null) + { + if (connection.Type == NodeConnectionType.Option) + { + connection.OptionText = newValue?.ToString(); + } + else + { + connection.ClearConnections(); + connection.OverrideValue = newValue; + } + } + + msgBox.Close(); + return true; + }; + } + + public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) + { + DrawnTooltip = string.Empty; + Cam.UpdateTransform(); + + // "world" space + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, transformMatrix: Cam.Transform); + graphics.Clear(new Color(0.2f, 0.2f, 0.2f, 1.0f)); + + foreach (EditorNode node in nodeList.Where(node => node is SpecialNode)) + { + node.Draw(spriteBatch); + } + + // Render value nodes below event nodes + foreach (EditorNode node in nodeList.Where(node => node is ValueNode)) + { + node.Draw(spriteBatch); + } + + foreach (EditorNode node in nodeList.Where(node => node is EventNode)) + { + node.Draw(spriteBatch); + } + + draggedNode?.Draw(spriteBatch); + foreach (var (node, _) in markedNodes) + { + node.Draw(spriteBatch); + } + + spriteBatch.End(); + + // GUI + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState); + GUI.Draw(Cam, spriteBatch); + + 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); + } + + spriteBatch.End(); + } + + public override void Update(double deltaTime) + { + if (GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y) + { + CreateGUI(); + } + + Cam.MoveCamera((float) deltaTime, true, true); + Vector2 mousePos = Cam.ScreenToWorld(PlayerInput.MousePosition); + mousePos.Y = -mousePos.Y; + + foreach (EditorNode node in nodeList) + { + if (PlayerInput.PrimaryMouseButtonDown()) + { + NodeConnection? connection = node.GetConnectionOnMouse(mousePos); + if (connection != null && connection.Type.NodeSide == NodeConnectionType.Side.Right) + { + if (connection.Type != NodeConnectionType.Out) + { + if (connection.ConnectedTo.Any()) { return; } + } + + DraggedConnection = connection; + } + } + + // ReSharper disable once AssignmentInConditionalExpression + if (node.IsHighlighted = node.HeaderRectangle.Contains(mousePos)) + { + if (PlayerInput.PrimaryMouseButtonDown()) + { + // Ctrl + clicking the headers add them to the "selection" that allows us to drag multiple nodes at once + if (PlayerInput.IsCtrlDown()) + { + if (selectedNodes.Contains(node)) + { + selectedNodes.Remove(node); + } + else + { + selectedNodes.Add(node); + } + + node.IsSelected = selectedNodes.Contains(node); + break; + } + + draggedNode = node; + dragOffset = draggedNode.Position - mousePos; + foreach (EditorNode selectedNode in selectedNodes) + { + if (!markedNodes.ContainsKey(selectedNode)) + { + markedNodes.Add(selectedNode, selectedNode.Position - mousePos); + } + } + } + } + + if (PlayerInput.SecondaryMouseButtonClicked()) + { + NodeConnection? connection = node.GetConnectionOnMouse(mousePos); + if (node.GetDrawRectangle().Contains(mousePos) || connection != null) + { + CreateContextMenu(node, node.GetConnectionOnMouse(mousePos)); + break; + } + } + } + + if (PlayerInput.SecondaryMouseButtonClicked()) + { + foreach (var selectedNode in selectedNodes) + { + selectedNode.IsSelected = false; + } + + selectedNodes.Clear(); + } + + if (draggedNode != null) + { + if (!PlayerInput.PrimaryMouseButtonHeld()) + { + draggedNode = null; + markedNodes.Clear(); + } + else + { + Vector2 offsetChange = Vector2.Zero; + draggedNode.IsHighlighted = true; + draggedNode.Position = mousePos + dragOffset; + + if (PlayerInput.KeyHit(Keys.Up)) { offsetChange.Y--; } + + if (PlayerInput.KeyHit(Keys.Down)) { offsetChange.Y++; } + + if (PlayerInput.KeyHit(Keys.Left)) { offsetChange.X--; } + + if (PlayerInput.KeyHit(Keys.Right)) { offsetChange.X++; } + + dragOffset += offsetChange; + + foreach (var (editorNode, offset) in markedNodes.Where(pair => pair.Key != draggedNode)) + { + editorNode.Position = mousePos + offset; + } + + if (offsetChange != Vector2.Zero) + { + foreach (var (key, value) in markedNodes.ToList()) + { + markedNodes[key] = value + offsetChange; + } + } + } + } + + if (DraggedConnection != null) + { + if (!PlayerInput.PrimaryMouseButtonHeld()) + { + foreach (EditorNode node in nodeList) + { + var nodeOnMouse = node.GetConnectionOnMouse(mousePos); + if (nodeOnMouse != null && nodeOnMouse != DraggedConnection && nodeOnMouse.Type.NodeSide == NodeConnectionType.Side.Left) + { + if (!DraggedConnection.CanConnect(nodeOnMouse)) { continue; } + + nodeOnMouse.ClearConnections(); + DraggedConnection.Parent.Connect(DraggedConnection, nodeOnMouse); + break; + } + } + + DraggedConnection = null; + } + else + { + DraggingPosition = mousePos; + } + } + else + { + DraggingPosition = Vector2.Zero; + } + + if (contextMenu != null) + { + Rectangle expandedRect = contextMenu.Rect; + expandedRect.Inflate(20, 20); + if (!expandedRect.Contains(PlayerInput.MousePosition)) + { + contextMenu = null; + } + } + + if (PlayerInput.MidButtonHeld()) + { + Vector2 moveSpeed = PlayerInput.MouseSpeed * (float) deltaTime * 60.0f / Cam.Zoom; + moveSpeed.X = -moveSpeed.X; + Cam.Position += moveSpeed; + } + + base.Update(deltaTime); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/NodeConnection.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/NodeConnection.cs new file mode 100644 index 000000000..d7be315f2 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/NodeConnection.cs @@ -0,0 +1,351 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace Barotrauma +{ + internal class NodeConnectionType + { + public static readonly NodeConnectionType Activate = new NodeConnectionType(Side.Left, "Activate"); + public static readonly NodeConnectionType Value = new NodeConnectionType(Side.Left, "Value"); + public static readonly NodeConnectionType Option = new NodeConnectionType(Side.Right, "Option", new[] { Activate }); + public static readonly NodeConnectionType Add = new NodeConnectionType(Side.Right, "Add", new[] { Activate }); + public static readonly NodeConnectionType Success = new NodeConnectionType(Side.Right, "Success", new[] { Activate }); + public static readonly NodeConnectionType Failure = new NodeConnectionType(Side.Right, "Failure", new[] { Activate }); + public static readonly NodeConnectionType Next = new NodeConnectionType(Side.Right, "Next", new[] { Activate }); + public static readonly NodeConnectionType Out = new NodeConnectionType(Side.Right, "Out", new[] { Value }); + + public enum Side + { + Left, + Right + } + + public Side NodeSide { get; } + + public string Label { get; } + + public NodeConnectionType[]? AllowedConnections { get; } + + private NodeConnectionType(Side side, string name, NodeConnectionType[]? allowedConnections = null) + { + NodeSide = side; + Label = name; + AllowedConnections = allowedConnections; + } + } + + internal class NodeConnection + { + public string Attribute { get; } + + public int ID { get; set; } + + public bool EndConversation { get; set; } + + private string? optionText; + + public string? OptionText + { + get => optionText; + set + { + optionText = value; + actualValue = WrappedValue = TextManager.Get(value, true) is { } translated ? translated : value; + } + } + + public NodeConnectionType Type { get; } + + public Type? ValueType { get; } + + private object? overrideValue; + private object? actualValue; + + public object? OverrideValue + { + get => overrideValue; + set + { + overrideValue = value; + if (value is string str) + { + actualValue = WrappedValue = TextManager.Get(str, true) is { } translated ? translated : str; + } + else + { + actualValue = WrappedValue = value?.ToString() ?? string.Empty; + } + } + } + + private string? wrappedValue; + + private string? WrappedValue + { + get => wrappedValue; + set + { + string valueText = value ?? string.Empty; + if (string.IsNullOrWhiteSpace(valueText)) + { + wrappedValue = null; + return; + } + Vector2 textSize = GUI.SmallFont.MeasureString(valueText); + bool wasWrapped = false; + while (textSize.X > 96) + { + wasWrapped = true; + valueText = $"{valueText}...".Substring(0, valueText.Length - 4); + textSize = GUI.SmallFont.MeasureString($"{valueText}..."); + } + + if (wasWrapped) + { + valueText = valueText.TrimEnd(' ') + "..."; + } + + + wrappedValue = valueText; + } + } + + public PropertyInfo? PropertyInfo { get; } + + public Rectangle DrawRectangle = Rectangle.Empty; + + public readonly EditorNode Parent; + + public readonly List ConnectedTo = new List(); + + private readonly Color bgColor = Color.DarkGray * 0.8f; + + private readonly Color outlineColor = Color.White * 0.8f; + + public object? GetValue() + { + if (OverrideValue != null) + { + return OverrideValue; + } + + foreach (EditorNode editorNode in EventEditorScreen.nodeList) + { + var outNode = editorNode.Connections.Find(connection => connection.Type == NodeConnectionType.Out); + if (outNode != null && outNode.ConnectedTo.Contains(this)) + { + return (outNode.Parent as ValueNode)?.Value; + } + } + + return null; + } + + public void ClearConnections() + { + foreach (var connection in EventEditorScreen.nodeList.SelectMany(editorNode => editorNode.Connections.Where(connection => connection.ConnectedTo.Contains(this)))) + { + connection.ConnectedTo.Remove(this); + } + + ConnectedTo.Clear(); + } + + public NodeConnection(EditorNode parent, NodeConnectionType type, string attribute = "", Type? valueType = null, PropertyInfo? propertyInfo = null) + { + Type = type; + ValueType = valueType; + Attribute = attribute; + PropertyInfo = propertyInfo; + Parent = parent; + ID = parent.Connections.Count; + } + + private Point GetRenderPos(Rectangle parentRectangle, int yOffset) + { + int x = Type.NodeSide == NodeConnectionType.Side.Left ? parentRectangle.Left - 15 : parentRectangle.Right - 1; + return new Point(x, parentRectangle.Y + 8 + parentRectangle.Height / 8 * yOffset); + } + + public void Draw(SpriteBatch spriteBatch, Rectangle parentRectangle, int yOffset) + { + float camZoom = Screen.Selected is EventEditorScreen eventEditor ? eventEditor.Cam.Zoom : 1.0f; + 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)); + + string label = string.IsNullOrWhiteSpace(Attribute) ? Type.Label : Attribute; + float xPos = parentRectangle.Center.X > pos.X ? 24 : -8 - GUI.SmallFont.MeasureString(label).X; + + if (Type != NodeConnectionType.Out) + { + Vector2 size = GUI.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); + + Vector2 mousePos = Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition); + mousePos.Y = -mousePos.Y; + if (bgRect.Contains(mousePos)) + { + CustomAttributeData? attribute = PropertyInfo?.CustomAttributes.FirstOrDefault(); + if (attribute?.AttributeType == typeof(Serialize)) + { + if (attribute.ConstructorArguments.Count > 2) + { + string? description = attribute.ConstructorArguments[2].Value as string; + if (!string.IsNullOrWhiteSpace(description)) + { + EventEditorScreen.DrawnTooltip = description; + } + } + } + } + } + + if (OverrideValue != null) + { + DrawLabel(spriteBatch, new Vector2(DrawRectangle.Center.X - 96, pos.Y + (DrawRectangle.Height / 2) - (20 / 2)), WrappedValue ?? "null", actualValue?.ToString() ?? string.Empty); + } + + if (OptionText != null) + { + DrawLabel(spriteBatch, new Vector2(DrawRectangle.Center.X, pos.Y + (DrawRectangle.Height / 2) - (20 / 2)), WrappedValue ?? "null", actualValue?.ToString() ?? string.Empty); + } + + if (Parent.IsHighlighted) + { + DrawConnections(spriteBatch, yOffset, Math.Max(8.0f, 8.0f / camZoom), Color.Red); + } + + DrawConnections(spriteBatch, yOffset, width: Math.Max(2.0f, 2.0f / camZoom)); + + if (EventEditorScreen.DraggedConnection == this) + { + DrawSquareLine(spriteBatch, EventEditorScreen.DraggingPosition, yOffset, width: Math.Max(2.0f, 2.0f / camZoom)); + } + } + + private void DrawConnections(SpriteBatch spriteBatch, int yOffset, float width = 2, Color? overrideColor = null) + { + foreach (NodeConnection? eventNodeConnection in ConnectedTo) + { + if (eventNodeConnection != null) + { + DrawSquareLine(spriteBatch, new Vector2(eventNodeConnection.DrawRectangle.Left + 1, eventNodeConnection.DrawRectangle.Center.Y), yOffset, width, overrideColor); + } + } + } + + private void DrawLabel(SpriteBatch spriteBatch, Vector2 pos, string text, string fullText) + { + 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 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); + DrawRectangle = Rectangle.Union(DrawRectangle, drawRect); + + if (!string.IsNullOrWhiteSpace(fullText)) + { + Vector2 mousePos = Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition); + mousePos.Y = -mousePos.Y; + if (DrawRectangle.Contains(mousePos)) + { + EventEditorScreen.DrawnTooltip = fullText; + } + } + } + + private void DrawSquareLine(SpriteBatch spriteBatch, Vector2 position, int yOffset, float width = 2, Color? overrideColor = null) + { + // draw a line between 2 nodes using points + // the order of this array is messed up, I know + // order of points is from start node to end node: 0, 4, 1, 2, 5, 3 + Vector2[] points = new Vector2[6]; + int xOffset = 24 * (yOffset + 1); + points[0] = new Vector2(DrawRectangle.Right, DrawRectangle.Center.Y); + points[3] = position; + points[1] = points[0]; + points[2] = points[3]; + + points[4] = points[1]; + points[5] = points[2]; + + points[1].X += (points[2].X - points[1].X) / 2; + points[1].X = Math.Max(points[1].X, points[0].X + xOffset); + points[2].X = points[1].X; + + // if the node is "behind" us do some magic to make the line curve to prevent overlapping + if (points[1].X <= points[0].X + xOffset) + { + points[4].X += xOffset; + points[1].X = points[4].X; + points[1].Y += (points[2].Y - points[1].Y) / 2; + } + + if (points[2].X >= points[3].X - xOffset) + { + points[5].X -= xOffset; + points[2].X = points[5].X; + points[2].Y -= points[2].Y - points[1].Y; + } + + Color drawColor = Parent is ValueNode ? GetPropertyColor(ValueType) : GUI.Style.Red; + + if (overrideColor != null) + { + drawColor = overrideColor.Value; + } + + GUI.DrawLine(spriteBatch, points[0], points[4], drawColor, width: (int)width); + GUI.DrawLine(spriteBatch, points[4], points[1], drawColor, width: (int)width); + GUI.DrawLine(spriteBatch, points[1], points[2], drawColor, width: (int)width); + GUI.DrawLine(spriteBatch, points[2], points[5], drawColor, width: (int)width); + GUI.DrawLine(spriteBatch, points[5], points[3], drawColor, width: (int)width); + } + + private static readonly Color defaultColor = new Color(139, 233, 253); + private static readonly Color yellowColor = new Color(241, 250, 140); + private static readonly Color pinkColor = new Color(255, 121, 198); + private static readonly Color purpleColor = new Color(189, 147, 249); + + public static Color GetPropertyColor(Type? valueType) + { + Color color = defaultColor; + if (valueType == typeof(bool)) + color = pinkColor; + else if (valueType == typeof(string)) + color = yellowColor; + else if (valueType == typeof(int) || + valueType == typeof(float) || + valueType == typeof(double)) + color = purpleColor; + else if (valueType == null) color = Color.White; + return color; + } + + public bool CanConnect(NodeConnection otherNode) + { + if (otherNode.OverrideValue != null) + { + return false; + } + + return Type.AllowedConnections == null || Type.AllowedConnections.Contains(otherNode.Type); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index d3c8f91f5..209191df6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -20,6 +20,8 @@ namespace Barotrauma private Texture2D damageStencil; private Texture2D distortTexture; + private float fadeToBlackState; + public Effect PostProcessEffect { get; private set; } public Effect GradientEffect { get; private set; } @@ -29,7 +31,7 @@ namespace Barotrauma cam.Translate(new Vector2(-10.0f, 50.0f)); CreateRenderTargets(graphics); - GameMain.Instance.OnResolutionChanged += () => + GameMain.Instance.ResolutionChanged += () => { CreateRenderTargets(graphics); }; @@ -111,10 +113,10 @@ namespace Barotrauma sw.Restart(); spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); - - if (Character.Controlled != null && cam != null) Character.Controlled.DrawHUD(spriteBatch, cam); - if (GameMain.GameSession != null) GameMain.GameSession.Draw(spriteBatch); + if (Character.Controlled != null && cam != null) { Character.Controlled.DrawHUD(spriteBatch, cam); } + + if (GameMain.GameSession != null) { GameMain.GameSession.Draw(spriteBatch); } if (Character.Controlled == null && !GUI.DisableHUD) { @@ -402,6 +404,31 @@ namespace Barotrauma PostProcessEffect.CurrentTechnique.Passes[0].Apply(); } Quad.Render(); + + if (fadeToBlackState > 0.0f) + { + spriteBatch.Begin(SpriteSortMode.Deferred); + GUI.DrawRectangle(spriteBatch, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), Color.Lerp(Color.TransparentBlack, Color.Black, fadeToBlackState), isFilled: true); + spriteBatch.End(); + } + } + + partial void UpdateProjSpecific(double deltaTime) + { + if (ConversationAction.FadeScreenToBlack) + { + fadeToBlackState = Math.Min(fadeToBlackState + (float)deltaTime, 1.0f); + } + else + { + fadeToBlackState = Math.Max(fadeToBlackState - (float)deltaTime, 0.0f); + } + + if (!PlayerInput.PrimaryMouseButtonHeld()) + { + Inventory.draggingSlot = null; + Inventory.draggingItem = null; + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index 8dcd69bc0..c9a688413 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -1,8 +1,10 @@ -using Barotrauma.Lights; +using Barotrauma.Extensions; +using Barotrauma.Lights; using Barotrauma.RuinGeneration; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; +using System.Collections.Generic; using System.Linq; using System.Xml.Linq; #if DEBUG @@ -27,7 +29,7 @@ namespace Barotrauma private LevelGenerationParams selectedParams; private LevelObjectPrefab selectedLevelObject; - private GUIListBox paramsList, ruinParamsList, levelObjectList; + private GUIListBox paramsList, ruinParamsList, outpostParamsList, levelObjectList; private GUIListBox editorContainer; private GUIButton spriteEditDoneButton; @@ -65,7 +67,9 @@ namespace Barotrauma return true; }; - ruinParamsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.2f), paddedLeftPanel.RectTransform)); + var ruinTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedLeftPanel.RectTransform), TextManager.Get("leveleditor.ruinparams"), font: GUI.SubHeadingFont); + + ruinParamsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedLeftPanel.RectTransform)); ruinParamsList.OnSelected += (GUIComponent component, object obj) => { var ruinGenerationParams = obj as RuinGenerationParams; @@ -74,7 +78,111 @@ namespace Barotrauma return true; }; - new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), paddedLeftPanel.RectTransform), + var outpostTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedLeftPanel.RectTransform), TextManager.Get("leveleditor.outpostparams"), font: GUI.SubHeadingFont); + GUITextBlock.AutoScaleAndNormalize(ruinTitle, outpostTitle); + + outpostParamsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.2f), paddedLeftPanel.RectTransform)); + outpostParamsList.OnSelected += (GUIComponent component, object obj) => + { + var outpostGenerationParams = obj as OutpostGenerationParams; + editorContainer.ClearChildren(); + var outpostParamsEditor = new SerializableEntityEditor(editorContainer.Content.RectTransform, outpostGenerationParams, false, true, elementHeight: 20); + + // location type ------------------------- + + var locationTypeGroup = new GUILayoutGroup(new RectTransform(new Point(editorContainer.Content.Rect.Width, 20)), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true + }; + + 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); } + + 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) + { + locationTypeDropDown.AddItem(TextManager.Capitalize(locationType), locationType); + if (outpostGenerationParams.AllowedLocationTypes.Contains(locationType)) + { + locationTypeDropDown.SelectItem(locationType); + } + } + if (!outpostGenerationParams.AllowedLocationTypes.Any()) + { + locationTypeDropDown.SelectItem("any"); + } + + locationTypeDropDown.OnSelected += (_, __) => + { + outpostGenerationParams.SetAllowedLocationTypes(locationTypeDropDown.SelectedDataMultiple.Cast()); + locationTypeDropDown.Text = ToolBox.LimitString(locationTypeDropDown.Text, locationTypeDropDown.Font, locationTypeDropDown.Rect.Width); + return true; + }; + locationTypeGroup.RectTransform.MinSize = new Point(locationTypeGroup.Rect.Width, locationTypeGroup.RectTransform.Children.Max(c => c.MinSize.Y)); + + outpostParamsEditor.AddCustomContent(locationTypeGroup, 100); + + // 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); + outpostParamsEditor.AddCustomContent(moduleLabel, 100); + + 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 GUINumberInput(new RectTransform(new Vector2(0.5f, 1f), moduleCountGroup.RectTransform), GUINumberInput.NumberType.Int) + { + MinValueInt = 0, + MaxValueInt = 100, + IntValue = moduleCount.Value, + OnValueChanged = (numInput) => + { + outpostGenerationParams.SetModuleCount(moduleCount.Key, numInput.IntValue); + if (numInput.IntValue == 0) + { + outpostParamsList.Select(outpostParamsList.SelectedData); + } + } + }; + moduleCountGroup.RectTransform.MinSize = new Point(moduleCountGroup.Rect.Width, moduleCountGroup.RectTransform.Children.Max(c => c.MinSize.Y)); + outpostParamsEditor.AddCustomContent(moduleCountGroup, 100); + } + + // add module count ------------------------- + + 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); } + foreach (var sub in SubmarineInfo.SavedSubmarines) + { + if (sub.OutpostModuleInfo == null) { continue; } + foreach (string 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) + { + if (outpostGenerationParams.ModuleCounts.Any(mc => mc.Key.Equals(flag, StringComparison.OrdinalIgnoreCase))) { continue; } + moduleTypeDropDown.AddItem(TextManager.Capitalize(flag), flag); + } + moduleTypeDropDown.OnSelected += (_, userdata) => + { + outpostGenerationParams.SetModuleCount(userdata as string, 1); + outpostParamsList.Select(outpostParamsList.SelectedData); + return true; + }; + addModuleCountGroup.RectTransform.MinSize = new Point(addModuleCountGroup.Rect.Width, addModuleCountGroup.RectTransform.Children.Max(c => c.MinSize.Y)); + outpostParamsEditor.AddCustomContent(addModuleCountGroup, 100); + + return true; + }; + + var createLevelObjButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), paddedLeftPanel.RectTransform), TextManager.Get("leveleditor.createlevelobj")) { OnClicked = (btn, obj) => @@ -83,6 +191,7 @@ namespace Barotrauma return true; } }; + GUITextBlock.AutoScaleAndNormalize(createLevelObjButton.TextBlock); lightingEnabled = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.025f), paddedLeftPanel.RectTransform), TextManager.Get("leveleditor.lightingenabled")); @@ -130,7 +239,9 @@ namespace Barotrauma { Submarine.Unload(); GameMain.LightManager.ClearLights(); - Level.CreateRandom(seedBox.Text, generationParams: selectedParams).Generate(mirror: false); + LevelData levelData = LevelData.CreateRandom(seedBox.Text, generationParams: selectedParams); + levelData.ForceOutpostGenerationParams = outpostParamsList.SelectedData as OutpostGenerationParams; + Level.Generate(levelData, mirror: false); GameMain.LightManager.AddLight(pointerLightSource); cam.Position = new Vector2(Level.Loaded.Size.X / 2, Level.Loaded.Size.Y / 2); foreach (GUITextBlock param in paramsList.Content.Children) @@ -142,6 +253,56 @@ namespace Barotrauma } }; + new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), paddedRightPanel.RectTransform), + TextManager.Get("leveleditor.test")) + { + OnClicked = (btn, obj) => + { + if (Level.Loaded?.LevelData == null) { return false; } + + GameMain.GameScreen.Select(); + + var currEntities = Entity.GetEntities().ToList(); + if (Submarine.MainSub != null) + { + var toRemove = Entity.GetEntities().Where(e => e.Submarine == Submarine.MainSub).ToList(); + foreach (Entity ent in toRemove) + { + ent.Remove(); + } + 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.SelectedContentPackages, ContentType.Wreck).ToList(); + nonPlayerFiles.AddRange(ContentPackage.GetFilesOfType(GameMain.Config.SelectedContentPackages, ContentType.Outpost)); + nonPlayerFiles.AddRange(ContentPackage.GetFilesOfType(GameMain.Config.SelectedContentPackages, ContentType.OutpostModule)); + SubmarineInfo subInfo = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name.Equals(GameMain.Config.QuickStartSubmarineName, StringComparison.InvariantCultureIgnoreCase)); + subInfo ??= SubmarineInfo.SavedSubmarines.GetRandom(s => + s.IsPlayer && !s.HasTag(SubmarineTag.Shuttle) && + !nonPlayerFiles.Any(f => f.Path.CleanUpPath().Equals(s.FilePath.CleanUpPath(), StringComparison.InvariantCultureIgnoreCase))); + GameSession gameSession = new GameSession(subInfo, "", GameModePreset.TestMode, null); + gameSession.StartRound(Level.Loaded.LevelData); + (gameSession.GameMode as TestGameMode).OnRoundEnd = () => + { + GameMain.LevelEditorScreen.Select(); + Submarine.MainSub.Remove(); + + var toRemove = Entity.GetEntities().Where(e => !currEntities.Contains(e)).ToList(); + foreach (Entity ent in toRemove) + { + ent.Remove(); + } + + Submarine.MainSub = null; + }; + + GameMain.GameSession = gameSession; + + return true; + } + }; + bottomPanel = new GUIFrame(new RectTransform(new Vector2(0.75f, 0.22f), Frame.RectTransform, Anchor.BottomLeft) { MaxSize = new Point(GameMain.GraphicsWidth - rightPanel.Rect.Width, 1000) }, style: "GUIFrameBottom"); @@ -182,6 +343,7 @@ namespace Barotrauma editingSprite = null; UpdateParamsList(); UpdateRuinParamsList(); + UpdateOutpostParamsList(); UpdateLevelObjectsList(); } @@ -200,7 +362,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.Name) + genParams.Identifier) { Padding = Vector4.Zero, UserData = genParams @@ -224,6 +386,22 @@ namespace Barotrauma } } + private void UpdateOutpostParamsList() + { + editorContainer.ClearChildren(); + outpostParamsList.Content.ClearChildren(); + + foreach (OutpostGenerationParams genParams in OutpostGenerationParams.Params) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), outpostParamsList.Content.RectTransform) { MinSize = new Point(0, 20) }, + genParams.Name) + { + Padding = Vector4.Zero, + UserData = genParams + }; + } + } + private void UpdateLevelObjectsList() { editorContainer.ClearChildren(); @@ -273,15 +451,15 @@ namespace Barotrauma Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), commonnessContainer.RectTransform), - TextManager.GetWithVariable("leveleditor.levelobjcommonness", "[leveltype]", selectedParams.Name), textAlignment: Alignment.Center); + TextManager.GetWithVariable("leveleditor.levelobjcommonness", "[leveltype]", selectedParams.Identifier), textAlignment: Alignment.Center); new GUINumberInput(new RectTransform(new Vector2(0.5f, 0.4f), commonnessContainer.RectTransform), GUINumberInput.NumberType.Float) { MinValueFloat = 0, MaxValueFloat = 100, - FloatValue = levelObjectPrefab.GetCommonness(selectedParams.Name), + FloatValue = levelObjectPrefab.GetCommonness(selectedParams.Identifier), OnValueChanged = (numberInput) => { - levelObjectPrefab.OverrideCommonness[selectedParams.Name] = numberInput.FloatValue; + levelObjectPrefab.OverrideCommonness[selectedParams.Identifier] = numberInput.FloatValue; } }; new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), commonnessContainer.RectTransform), style: null); @@ -411,7 +589,7 @@ namespace Barotrauma foreach (GUIComponent levelObjFrame in levelObjectList.Content.Children) { var levelObj = levelObjFrame.UserData as LevelObjectPrefab; - float commonness = levelObj.GetCommonness(selectedParams.Name); + float commonness = levelObj.GetCommonness(selectedParams.Identifier); 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; @@ -428,7 +606,7 @@ namespace Barotrauma { var levelObj1 = c1.GUIComponent.UserData as LevelObjectPrefab; var levelObj2 = c2.GUIComponent.UserData as LevelObjectPrefab; - return Math.Sign(levelObj2.GetCommonness(selectedParams.Name) - levelObj1.GetCommonness(selectedParams.Name)); + return Math.Sign(levelObj2.GetCommonness(selectedParams.Identifier) - levelObj1.GetCommonness(selectedParams.Identifier)); }); } @@ -520,14 +698,15 @@ namespace Barotrauma { foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().Equals(genParams.Name, StringComparison.OrdinalIgnoreCase)) - { - SerializableProperty.SerializeProperties(genParams, subElement, true); - } + string id = element.GetAttributeString("identifier", null) ?? element.Name.ToString(); + if (!id.Equals(genParams.Name, StringComparison.OrdinalIgnoreCase)) { continue; } + SerializableProperty.SerializeProperties(genParams, element, true); } } - else if (element.Name.ToString().Equals(genParams.Name, StringComparison.OrdinalIgnoreCase)) - { + else + { + string id = element.GetAttributeString("identifier", null) ?? element.Name.ToString(); + if (!id.Equals(genParams.Name, StringComparison.OrdinalIgnoreCase)) { continue; } SerializableProperty.SerializeProperties(genParams, element, true); } break; @@ -575,7 +754,8 @@ namespace Barotrauma bool elementFound = false; foreach (XElement element in doc.Root.Elements()) { - if (!element.Name.ToString().Equals(genParams.Name, StringComparison.OrdinalIgnoreCase)) { continue; } + string id = element.GetAttributeString("identifier", null) ?? element.Name.ToString(); + if (!id.Equals(genParams.Name, StringComparison.OrdinalIgnoreCase)) { continue; } SerializableProperty.SerializeProperties(genParams, element, true); elementFound = true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LobbyScreen.cs deleted file mode 100644 index 52b9b2f26..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LobbyScreen.cs +++ /dev/null @@ -1,94 +0,0 @@ -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; - -namespace Barotrauma -{ - class LobbyScreen : Screen - { - private CampaignUI campaignUI; - - private GUIFrame campaignUIContainer; - - private CrewManager CrewManager - { - get { return GameMain.GameSession.CrewManager; } - } - - public CampaignUI CampaignUI - { - get { return campaignUI; } - } - - public string GetMoney() - { - return campaignUI == null ? "" : campaignUI.GetMoney(); - } - - public LobbyScreen() - { - campaignUIContainer = new GUIFrame(new RectTransform(Vector2.One, Frame.RectTransform, Anchor.Center), style: null); - } - - public override void Select() - { - base.Select(); - - CampaignMode campaign = GameMain.GameSession.GameMode as CampaignMode; - if (campaign == null) { return; } - - campaign.Map.SelectLocation(-1); - - campaignUIContainer.ClearChildren(); - campaignUI = new CampaignUI(campaign, campaignUIContainer) - { - StartRound = StartRound, - OnLocationSelected = SelectLocation - }; - campaignUI.UpdateCharacterLists(); - - GameAnalyticsManager.SetCustomDimension01("singleplayer"); - } - - public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) - { - graphics.Clear(Color.Black); - - GUI.DrawBackgroundSprite(spriteBatch, - GameMain.GameSession.Map.CurrentLocation.Type.GetPortrait(GameMain.GameSession.Map.CurrentLocation.PortraitId)); - - spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); - GUI.Draw(Cam, spriteBatch); - spriteBatch.End(); - } - - public void SelectLocation(Location location, LocationConnection locationConnection) - { - } - - private void StartRound() - { - if (GameMain.GameSession.Map.SelectedConnection == null) return; - - GameMain.Instance.ShowLoading(LoadRound()); - } - - private IEnumerable LoadRound() - { - GameMain.GameSession.StartRound(campaignUI.SelectedLevel, - mirrorLevel: GameMain.GameSession.Map.CurrentLocation != GameMain.GameSession.Map.SelectedConnection.Locations[0]); - GameMain.GameScreen.Select(); - - yield return CoroutineStatus.Success; - } - - public bool QuitToMainMenu(GUIButton button, object selection) - { - GameMain.MainMenuScreen.Select(); - return true; - } - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index bccff1f52..c0708a3db 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -54,7 +54,7 @@ namespace Barotrauma #region Creation public MainMenuScreen(GameMain game) { - GameMain.Instance.OnResolutionChanged += () => + GameMain.Instance.ResolutionChanged += () => { if (Selected == this && selectedTab == Tab.Settings) { @@ -688,13 +688,12 @@ namespace Barotrauma if (selectedSub == null) { DebugConsole.NewMessage("Loading a random sub.", Color.White); - var subs = SubmarineInfo.SavedSubmarines.Where(s => !s.HasTag(SubmarineTag.Shuttle) && !s.HasTag(SubmarineTag.HideInMenus)); + var subs = SubmarineInfo.SavedSubmarines.Where(s => s.Type == SubmarineType.Player && !s.HasTag(SubmarineTag.Shuttle) && !s.HasTag(SubmarineTag.HideInMenus)); selectedSub = subs.ElementAt(Rand.Int(subs.Count())); } var gamesession = new GameSession( selectedSub, - "Data/Saves/test.xml", - GameModePreset.List.Find(gm => gm.Identifier == "devsandbox"), + GameModePreset.DevSandbox, missionPrefab: null); //(gamesession.GameMode as SinglePlayerCampaign).GenerateMap(ToolBox.RandomSeed(8)); gamesession.StartRound(fixedSeed ? "abcd" : ToolBox.RandomSeed(8), difficulty: 40); @@ -703,13 +702,6 @@ namespace Barotrauma string[] jobIdentifiers = new string[] { "captain", "engineer", "mechanic", "securityofficer", "medicaldoctor" }; foreach (string job in jobIdentifiers) { - var spawnPoint = WayPoint.GetRandom(SpawnType.Human, null, Submarine.MainSub, useSyncedRand: true); - if (spawnPoint == null) - { - DebugConsole.ThrowError("No spawnpoints found in the selected submarine. Quickstart failed."); - GameMain.MainMenuScreen.Select(); - return; - } var jobPrefab = JobPrefab.Get(job); var variant = Rand.Range(0, jobPrefab.Variants); var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: jobPrefab, variant: variant); @@ -717,12 +709,9 @@ namespace Barotrauma { DebugConsole.ThrowError("Failed to find the job \"" + job + "\"!"); } - - var newCharacter = Character.Create(CharacterPrefab.HumanSpeciesName, spawnPoint.WorldPosition, ToolBox.RandomSeed(8), characterInfo); - newCharacter.GiveJobItems(spawnPoint); - gamesession.CrewManager.AddCharacter(newCharacter); - Character.Controlled = newCharacter; - } + gamesession.CrewManager.AddCharacterInfo(characterInfo); + } + gamesession.CrewManager.InitSinglePlayerRound(); } public void SetEnableModsNotification(bool visible) @@ -870,7 +859,7 @@ namespace Barotrauma } #endif */ - + GameMain.NetLobbyScreen?.Release(); GameMain.NetLobbyScreen = new NetLobbyScreen(); try { @@ -1081,11 +1070,8 @@ namespace Barotrauma selectedSub = new SubmarineInfo(Path.Combine(SaveUtil.TempPath, selectedSub.Name + ".sub")); - GameMain.GameSession = new GameSession(selectedSub, saveName, - GameModePreset.List.Find(g => g.Identifier == "singleplayercampaign")); - (GameMain.GameSession.GameMode as CampaignMode).GenerateMap(mapSeed); - - GameMain.LobbyScreen.Select(); + GameMain.GameSession = new GameSession(selectedSub, saveName, GameModePreset.SinglePlayerCampaign, mapSeed); + ((SinglePlayerCampaign)GameMain.GameSession.GameMode).LoadNewLevel(); } private void LoadGame(string saveFile) @@ -1102,8 +1088,8 @@ namespace Barotrauma return; } - - GameMain.LobbyScreen.Select(); + //TODO + //GameMain.LobbyScreen.Select(); } #region UI Methods @@ -1145,6 +1131,8 @@ namespace Barotrauma int port = NetConfig.DefaultPort; int queryPort = NetConfig.DefaultQueryPort; int maxPlayers = 8; + bool karmaEnabled = true; + string selectedKarmaPreset = ""; PlayStyle selectedPlayStyle = PlayStyle.Casual; if (File.Exists(ServerSettings.SettingsFile)) { @@ -1154,6 +1142,8 @@ namespace Barotrauma port = settingsDoc.Root.GetAttributeInt("port", port); queryPort = settingsDoc.Root.GetAttributeInt("queryport", queryPort); maxPlayers = settingsDoc.Root.GetAttributeInt("maxplayers", maxPlayers); + karmaEnabled = settingsDoc.Root.GetAttributeBool("karmaenabled", true); + selectedKarmaPreset = settingsDoc.Root.GetAttributeString("karmapreset", "default"); string playStyleStr = settingsDoc.Root.GetAttributeString("playstyle", "Casual"); Enum.TryParse(playStyleStr, out selectedPlayStyle); } @@ -1333,10 +1323,12 @@ namespace Barotrauma foreach (string karmaPreset in tempKarmaManager.Presets.Keys) { karmaPresetDD.AddItem(TextManager.Get("KarmaPreset." + karmaPreset), karmaPreset); - if (karmaPreset == "default") { karmaPresetDD.SelectItem(karmaPreset); } + if (karmaPreset == selectedKarmaPreset) { karmaPresetDD.SelectItem(karmaPreset); } } if (karmaPresetDD.SelectedIndex == -1) { karmaPresetDD.Select(0); } + karmaEnabledBox.Selected = karmaEnabled; + tickboxAreaLower.RectTransform.MaxSize = karmaEnabledBox.RectTransform.MaxSize; //spacing diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index ce44807a4..5e6292b9e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -52,6 +52,8 @@ namespace Barotrauma public readonly GUIFrame MissionTypeFrame; public readonly GUIFrame CampaignSetupFrame; + public readonly GUIFrame CampaignFrame; + public readonly GUIButton ContinueCampaignButton, QuitCampaignButton; private readonly GUITickBox[] missionTypeTickBoxes; private readonly GUIListBox missionTypeList; @@ -61,8 +63,8 @@ namespace Barotrauma get; private set; } - private readonly GUIComponent gameModeContainer, campaignContainer; - private readonly GUIButton gameModeViewButton, campaignViewButton, spectateButton; + private readonly GUIComponent gameModeContainer; + private readonly GUIButton spectateButton; private readonly GUILayoutGroup roundControlsHolder; public GUIButton SettingsButton { get; private set; } public static GUIButton JobInfoFrame; @@ -79,11 +81,11 @@ namespace Barotrauma private readonly GUITickBox autoRestartBox; private readonly GUITextBlock autoRestartText; - + private GUIDropDown shuttleList; private GUITickBox shuttleTickBox; - private CampaignUI campaignUI; + private GUIComponent settingsBlocker; private Sprite backgroundSprite; @@ -223,6 +225,12 @@ namespace Barotrauma get { return shuttleList.SelectedData as SubmarineInfo; } } + public CampaignSetupUI CampaignSetupUI; + public List CampaignSubmarines = new List(); + + // Passed onto the gamesession when created + public List ServerOwnedSubmarines = new List(); + public bool UsingShuttle { get { return shuttleTickBox.Selected; } @@ -308,12 +316,6 @@ namespace Barotrauma } } - - public CampaignUI CampaignUI - { - get { return campaignUI; } - } - public NetLobbyScreen() { float panelSpacing = 0.005f; @@ -323,22 +325,6 @@ namespace Barotrauma RelativeSpacing = panelSpacing }; - GameMain.Instance.OnResolutionChanged += () => - { - foreach (GUIComponent c in Frame.GetAllChildren()) - { - if (c.Style != null) - { - c.ApplySizeRestrictions(c.Style); - } - } - - if (innerFrame != null) - { - innerFrame.RectTransform.MaxSize = new Point(int.MaxValue, GameMain.GraphicsHeight - 50); - } - }; - var panelContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), innerFrame.RectTransform, Anchor.Center), isHorizontal: true) { Stretch = true, @@ -385,25 +371,6 @@ namespace Barotrauma RelativeSpacing = 0.025f }; - //gamemode tab buttons ------------------------------------------------------------ - - var gameModeTabButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.03f), panelHolder.RectTransform), isHorizontal: true) - { - RelativeSpacing = 0.01f - }; - gameModeViewButton = new GUIButton(new RectTransform(new Vector2(0.25f, 1.4f), gameModeTabButtonContainer.RectTransform), - TextManager.Get("GameMode"), style: "GUITabButton") - { - Selected = true, - OnClicked = (bt, userData) => { ToggleCampaignView(false); return true; } - }; - campaignViewButton = new GUIButton(new RectTransform(new Vector2(0.25f, 1.4f), gameModeTabButtonContainer.RectTransform), - TextManager.Get("CampaignLabel"), style: "GUITabButton") - { - Enabled = false, - OnClicked = (bt, userData) => { ToggleCampaignView(true); return true; } - }; - //server game panel ------------------------------------------------------------ modeFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), panelHolder.RectTransform)) @@ -417,11 +384,6 @@ namespace Barotrauma Stretch = true }; - campaignContainer = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.9f), modeFrame.RectTransform, Anchor.Center), style: null) - { - Visible = false - }; - var disconnectButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), bottomBarLeft.RectTransform), TextManager.Get("disconnect")) { OnClicked = (bt, userdata) => { GameMain.QuitToMainMenu(save: false, showVerificationPrompt: true); return true; } @@ -462,14 +424,6 @@ namespace Barotrauma Stretch = true }; - GameMain.Instance.OnResolutionChanged += () => - { - if (panelContainer != null && sideBar != null) - { - sideBar.RectTransform.MaxSize = new Point(650, panelContainer.RectTransform.Rect.Height); - } - }; - //player info panel ------------------------------------------------------------ myCharacterFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), sideBar.RectTransform)); @@ -483,9 +437,6 @@ namespace Barotrauma UserData = "spectate" }; - //spacing - new GUIFrame(new RectTransform(new Vector2(1.0f, gameModeTabButtonContainer.RectTransform.RelativeSize.Y), sideBar.RectTransform), style: null); - // Social area GUIFrame logBackground = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), sideBar.RectTransform)); @@ -643,7 +594,7 @@ namespace Barotrauma }; roundControlsHolder = new GUILayoutGroup(new RectTransform(Vector2.One, bottomBarRight.RectTransform), - isHorizontal: true) + isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; @@ -675,11 +626,6 @@ namespace Barotrauma clientHiddenElements.Add(StartButton); bottomBar.RectTransform.MinSize = new Point(0, (int)Math.Max(ReadyToStartBox.RectTransform.MinSize.Y / 0.75f, StartButton.RectTransform.MinSize.Y)); - GameMain.Instance.OnResolutionChanged += () => - { - bottomBar.RectTransform.MinSize = - new Point(0, (int)Math.Max(ReadyToStartBox.RectTransform.MinSize.Y / 0.75f, StartButton.RectTransform.MinSize.Y)); - }; //autorestart ------------------------------------------------------------------ @@ -728,10 +674,6 @@ namespace Barotrauma clientHiddenElements.Add(SettingsButton); lobbyHeader.RectTransform.MinSize = new Point(0, Math.Max(ServerName.Rect.Height, SettingsButton.Rect.Height)); - GameMain.Instance.OnResolutionChanged += () => - { - lobbyHeader.RectTransform.MinSize = new Point(0, Math.Max(ServerName.Rect.Height, SettingsButton.Rect.Height)); - }; GUILayoutGroup lobbyContent = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), infoFrameContent.RectTransform), isHorizontal: true) { @@ -861,10 +803,6 @@ namespace Barotrauma }; shuttleList.ListBox.RectTransform.MinSize = new Point(250, 0); shuttleHolder.RectTransform.MinSize = new Point(0, shuttleList.RectTransform.Children.Max(c => c.MinSize.Y)); - GameMain.Instance.OnResolutionChanged += () => - { - shuttleHolder.RectTransform.MinSize = new Point(0, shuttleList.RectTransform.Children.Max(c => c.MinSize.Y)); - }; subPreviewContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), rightColumn.RectTransform), style: null); subPreviewContainer.RectTransform.SizeChanged += () => @@ -875,84 +813,7 @@ namespace Barotrauma //------------------------------------------------------------------------------------------------------------------ // Gamemode panel //------------------------------------------------------------------------------------------------------------------ - - GUILayoutGroup miscSettingsHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), gameModeContainer.RectTransform), - isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - Stretch = true, - RelativeSpacing = 0.01f - }; - - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), gameModeContainer.RectTransform), style: "HorizontalLine"); - - miscSettingsHolder.RectTransform.SizeChanged += () => - { - miscSettingsHolder.Recalculate(); - foreach (GUIComponent child in miscSettingsHolder.Children) - { - if (child is GUITextBlock textBlock) - { - textBlock.TextScale = 1; - textBlock.AutoScaleHorizontal = true; - textBlock.SetTextPos(); - } - else if (child is GUITickBox tickBox) - { - tickBox.TextBlock.TextScale = 1; - tickBox.TextBlock.AutoScaleHorizontal = true; - tickBox.TextBlock.SetTextPos(); - } - } - }; - - //seed ------------------------------------------------------------------ - - var seedLabel = new GUITextBlock(new RectTransform(Vector2.One, miscSettingsHolder.RectTransform), TextManager.Get("LevelSeed"), font: GUI.SubHeadingFont); - seedLabel.RectTransform.MaxSize = new Point((int)(seedLabel.TextSize.X + 30 * GUI.Scale), int.MaxValue); - SeedBox = new GUITextBox(new RectTransform(new Vector2(0.25f, 1.0f), miscSettingsHolder.RectTransform)); - SeedBox.OnDeselected += (textBox, key) => - { - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.LevelSeed); - }; - clientDisabledElements.Add(SeedBox); - LevelSeed = ToolBox.RandomSeed(8); - - //level difficulty ------------------------------------------------------------------ - - var difficultyLabel = new GUITextBlock(new RectTransform(Vector2.One, miscSettingsHolder.RectTransform), TextManager.Get("LevelDifficulty"), font: GUI.SubHeadingFont) - { - ToolTip = TextManager.Get("leveldifficultyexplanation") - }; - levelDifficultyScrollBar = new GUIScrollBar(new RectTransform(new Vector2(0.25f, 1.0f), miscSettingsHolder.RectTransform), style: "GUISlider", barSize: 0.2f) - { - Step = 0.01f, - Range = new Vector2(0.0f, 100.0f), - ToolTip = TextManager.Get("leveldifficultyexplanation"), - OnReleased = (scrollbar, value) => - { - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, levelDifficulty: scrollbar.BarScrollValue); - return true; - } - }; - difficultyLabel.RectTransform.MaxSize = new Point((int)(difficultyLabel.TextSize.X + 30 * GUI.Scale), int.MaxValue); - var difficultyName = new GUITextBlock(new RectTransform(new Vector2(0.25f, 1.0f), miscSettingsHolder.RectTransform), "") - { - ToolTip = TextManager.Get("leveldifficultyexplanation") - }; - levelDifficultyScrollBar.OnMoved = (scrollbar, value) => - { - if (EventManagerSettings.List.Count == 0) { return true; } - difficultyName.Text = - EventManagerSettings.List[Math.Min((int)Math.Floor(value * EventManagerSettings.List.Count), EventManagerSettings.List.Count - 1)].Name - + " (" + ((int)Math.Round(scrollbar.BarScrollValue)) + " %)"; - difficultyName.TextColor = ToolBox.GradientLerp(scrollbar.BarScroll, GUI.Style.Green, GUI.Style.Orange, GUI.Style.Red); - return true; - }; - - clientDisabledElements.Add(levelDifficultyScrollBar); - - //gamemode ------------------------------------------------------------------ - + GUILayoutGroup gameModeBackground = new GUILayoutGroup(new RectTransform(Vector2.One, gameModeContainer.RectTransform), isHorizontal: true) { Stretch = true, @@ -1005,10 +866,6 @@ namespace Barotrauma style: "GameModeIcon." + mode.Identifier, scaleToFit: true); modeFrame.RectTransform.MinSize = new Point(0, (int)(modeContent.Children.Sum(c => c.Rect.Height + modeContent.AbsoluteSpacing) / modeContent.RectTransform.RelativeSize.Y)); - GameMain.Instance.OnResolutionChanged += () => - { - modeFrame.RectTransform.MinSize = new Point(0, (int)(modeContent.Children.Sum(c => c.Rect.Height + modeContent.AbsoluteSpacing) / modeContent.RectTransform.RelativeSize.Y)); - }; } var gameModeSpecificFrame = new GUIFrame(new RectTransform(new Vector2(0.333f, 1.0f), gameModeBackground.RectTransform), style: null); @@ -1016,6 +873,31 @@ namespace Barotrauma { Visible = false }; + CampaignFrame = new GUIFrame(new RectTransform(Vector2.One, gameModeSpecificFrame.RectTransform), style: null) + { + Visible = false + }; + GUILayoutGroup campaignContent = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.5f), CampaignFrame.RectTransform, Anchor.Center)) + { + RelativeSpacing = 0.05f, + Stretch = true + }; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), campaignContent.RectTransform), + TextManager.Get("gamemode.multiplayercampaign"), font: GUI.SubHeadingFont, textAlignment: Alignment.Center); + ContinueCampaignButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.3f), campaignContent.RectTransform), + TextManager.Get("campaigncontinue"), textAlignment: Alignment.Center) + { + OnClicked = (_, __) => { GameMain.Client?.RequestStartRound(true); return true; } + }; + QuitCampaignButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.3f), campaignContent.RectTransform), + TextManager.Get("pausemenusavequit"), textAlignment: Alignment.Center) + { + OnClicked = (_, __) => + { + GameMain.Client.RequestSelectMode(modeList.Content.GetChildIndex(modeList.Content.GetChildByUserData(GameModePreset.Sandbox))); + return true; + } + }; //mission type ------------------------------------------------------------------ MissionTypeFrame = new GUIFrame(new RectTransform(Vector2.One, gameModeSpecificFrame.RectTransform), style: null); @@ -1062,23 +944,74 @@ namespace Barotrauma index++; } - clientDisabledElements.AddRange(missionTypeTickBoxes); - //traitor probability ------------------------------------------------------------------ + //------------------------------------------------------------------ + // settings panel + //------------------------------------------------------------------ GUILayoutGroup settingsHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.333f, 1.0f), gameModeBackground.RectTransform)) { Stretch = true }; - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.055f), settingsHolder.RectTransform) { MinSize = new Point(0, 25) }, style: null); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), settingsHolder.RectTransform) { MinSize = new Point(0, 25) }, + TextManager.Get("Settings"), font: GUI.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)) { RelativeSpacing = 0.025f }; + //seed ------------------------------------------------------------------ + + var seedLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), TextManager.Get("LevelSeed")); + SeedBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), seedLabel.RectTransform, Anchor.CenterRight)); + SeedBox.OnDeselected += (textBox, key) => + { + GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.LevelSeed); + }; + clientDisabledElements.Add(SeedBox); + LevelSeed = ToolBox.RandomSeed(8); + + //level difficulty ------------------------------------------------------------------ + + var difficultyHolder = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), settingsContent.RectTransform), style: null); + + var difficultyLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), difficultyHolder.RectTransform), TextManager.Get("LevelDifficulty")) + { + ToolTip = TextManager.Get("leveldifficultyexplanation") + }; + + levelDifficultyScrollBar = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.5f), difficultyHolder.RectTransform, Anchor.BottomCenter), style: "GUISlider", barSize: 0.2f) + { + Step = 0.01f, + Range = new Vector2(0.0f, 100.0f), + ToolTip = TextManager.Get("leveldifficultyexplanation"), + OnReleased = (scrollbar, value) => + { + GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, levelDifficulty: scrollbar.BarScrollValue); + return true; + } + }; + var difficultyName = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), difficultyLabel.RectTransform), "", textAlignment: Alignment.CenterRight) + { + ToolTip = TextManager.Get("leveldifficultyexplanation") + }; + levelDifficultyScrollBar.OnMoved = (scrollbar, value) => + { + if (EventManagerSettings.List.Count == 0) { return true; } + difficultyName.Text = + EventManagerSettings.List[Math.Min((int)Math.Floor(value * EventManagerSettings.List.Count), EventManagerSettings.List.Count - 1)].Name + + " (" + ((int)Math.Round(scrollbar.BarScrollValue)) + " %)"; + difficultyName.TextColor = ToolBox.GradientLerp(scrollbar.BarScroll, GUI.Style.Green, GUI.Style.Orange, GUI.Style.Red); + return true; + }; + + clientDisabledElements.Add(levelDifficultyScrollBar); + + //traitor probability ------------------------------------------------------------------ + var traitorsSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), traitorsSettingHolder.RectTransform), TextManager.Get("Traitors"), wrap: true); @@ -1162,18 +1095,21 @@ namespace Barotrauma }; List settingsElements = settingsContent.Children.ToList(); - int spacingElementCount = 0; for (int i = 0; i < settingsElements.Count; i++) { - settingsElements[i].RectTransform.MinSize = new Point(0, Math.Max(settingsElements[i].RectTransform.Children.Max(c => c.Rect.Height), (int)(20 * GUI.Scale))); - if (settingsElements[i] is GUITextBlock) + if (settingsElements[i].CountChildren > 0) { - var spacing = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.03f), settingsContent.RectTransform), style: null); - spacing.RectTransform.RepositionChildInHierarchy(i + spacingElementCount); - spacingElementCount++; + settingsElements[i].RectTransform.MinSize = new Point(0, Math.Max(settingsElements[i].RectTransform.Children.Max(c => c.Rect.Height), (int)(20 * GUI.Scale))); } } + settingsBlocker = new GUIFrame(new RectTransform(Vector2.One, settingsFrame.RectTransform), style: "InnerFrame") + { + Color = Color.Black * 0.5f, + IgnoreLayoutGroups = true, + Visible = false + }; + clientDisabledElements.AddRange(botSpawnModeButtons); } @@ -1181,15 +1117,10 @@ namespace Barotrauma { CoroutineManager.StopCoroutines("WaitForStartRound"); - GUIMessageBox.CloseAll(); if (StartButton != null) { StartButton.Enabled = true; } - if (campaignUI?.StartButton != null) - { - campaignUI.StartButton.Enabled = true; - } GUI.ClearCursorWait(); } @@ -1205,8 +1136,7 @@ namespace Barotrauma } DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 10); - while (Selected == GameMain.NetLobbyScreen && - DateTime.Now < timeOut) + while (Selected == GameMain.NetLobbyScreen && DateTime.Now < timeOut) { msgBox.Header.Text = headerText + new string('.', ((int)Timing.TotalTime % 3 + 1)); yield return CoroutineStatus.Running; @@ -1265,30 +1195,18 @@ namespace Barotrauma clientReadonlyElements.ForEach(c => c.Readonly = true); clientHiddenElements.ForEach(c => c.Visible = false); - UpdatePermissions(); + RefreshEnabledElements(); if (GameMain.Client != null) { ChatManager.RegisterKeys(chatInput, GameMain.Client.ChatBox.ChatManager); spectateButton.Visible = GameMain.Client.GameStarted; - ReadyToStartBox.Parent.Visible = !GameMain.Client.GameStarted; ReadyToStartBox.Selected = false; - if (campaignUI != null) - { - campaignUI.SelectTab(CampaignUI.Tab.Map); - if (campaignUI.StartButton != null) - { - campaignUI.StartButton.Visible = !GameMain.Client.GameStarted && - (GameMain.Client.HasPermission(ClientPermissions.ManageRound) || - GameMain.Client.HasPermission(ClientPermissions.ManageCampaign)); - } - } GameMain.Client.SetReadyToStart(ReadyToStartBox); } else { spectateButton.Visible = false; - ReadyToStartBox.Parent.Visible = false; } SetSpectate(spectateBox.Selected); @@ -1308,7 +1226,7 @@ namespace Barotrauma base.Select(); } - public void UpdatePermissions() + public void RefreshEnabledElements() { ServerName.Readonly = !GameMain.Client.HasPermission(ClientPermissions.ManageSettings); ServerMessage.Readonly = !GameMain.Client.HasPermission(ClientPermissions.ManageSettings); @@ -1329,33 +1247,28 @@ namespace Barotrauma SettingsButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); SettingsButton.OnClicked = GameMain.Client.ServerSettings.ToggleSettingsFrame; - StartButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ManageRound) && !GameMain.Client.GameStarted && !campaignContainer.Visible; + StartButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ManageRound) && !GameMain.Client.GameStarted && !CampaignSetupFrame.Visible && !CampaignFrame.Visible; ServerName.Readonly = !GameMain.Client.HasPermission(ClientPermissions.ManageSettings); ServerMessage.Readonly = !GameMain.Client.HasPermission(ClientPermissions.ManageSettings); shuttleTickBox.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - SubList.Enabled = GameMain.Client.ServerSettings.Voting.AllowSubVoting || GameMain.Client.HasPermission(ClientPermissions.SelectSub); - shuttleList.Enabled = GameMain.Client.HasPermission(ClientPermissions.SelectSub); + SubList.Enabled = !CampaignFrame.Visible && (GameMain.Client.ServerSettings.Voting.AllowSubVoting || GameMain.Client.HasPermission(ClientPermissions.SelectSub)); + shuttleList.Enabled = shuttleTickBox.Enabled = !CampaignFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.SelectSub); ModeList.Enabled = GameMain.Client.ServerSettings.Voting.AllowModeVoting || GameMain.Client.HasPermission(ClientPermissions.SelectMode); LogButtons.Visible = GameMain.Client.HasPermission(ClientPermissions.ServerLog); GameMain.Client.ShowLogButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ServerLog); - - if (campaignUI?.StartButton != null) - { - campaignUI.StartButton.Visible = !GameMain.Client.GameStarted && - (GameMain.Client.HasPermission(ClientPermissions.ManageRound) || - GameMain.Client.HasPermission(ClientPermissions.ManageCampaign)); - } - roundControlsHolder.Children.ForEach(c => c.IgnoreLayoutGroups = !c.Visible); roundControlsHolder.Recalculate(); + + ReadyToStartBox.Parent.Visible = !GameMain.Client.GameStarted && SelectedMode != GameModePreset.MultiPlayerCampaign; + + RefreshGameModeContent(); } public void ShowSpectateButton() { - if (GameMain.Client == null) return; + if (GameMain.Client == null) { return; } spectateButton.Visible = true; spectateButton.Enabled = true; - StartButton.Visible = false; } @@ -1373,7 +1286,7 @@ namespace Barotrauma else if (campaignCharacterInfo != null) { campaignCharacterInfo = null; - UpdatePlayerFrame(campaignCharacterInfo, false); + UpdatePlayerFrame(null, false); } } @@ -1392,7 +1305,7 @@ namespace Barotrauma private void UpdatePlayerFrame(CharacterInfo characterInfo, bool allowEditing, GUIComponent parent) { - if (characterInfo == null) + if (characterInfo == null || CampaignCharacterDiscarded) { characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, GameMain.Client.Name, null); characterInfo.RecreateHead( @@ -1409,7 +1322,7 @@ namespace Barotrauma parent.ClearChildren(); - bool isGameRunning = GameMain.GameSession?.GameMode?.IsRunning ?? false; + bool isGameRunning = GameMain.GameSession?.IsRunning ?? false; infoContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, isGameRunning ? 0.95f : 0.9f), parent.RectTransform, Anchor.BottomCenter), childAnchor: Anchor.TopCenter) { @@ -1538,19 +1451,20 @@ namespace Barotrauma } else { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), infoContainer.RectTransform), characterInfo.Job.Name, textAlignment: Alignment.Center, wrap: true) + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContainer.RectTransform), characterInfo.Job.Name, textAlignment: Alignment.Center, font: GUI.SubHeadingFont, wrap: true) { HoverColor = Color.Transparent, SelectedColor = Color.Transparent }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), infoContainer.RectTransform), TextManager.Get("Skills")); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContainer.RectTransform), TextManager.Get("Skills"), font: GUI.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.08f), infoContainer.RectTransform), - " - " + TextManager.AddPunctuation(':', TextManager.Get("SkillName." + skill.Identifier), ((int)skill.Level).ToString()), - textColor); + 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); } // Spacing @@ -1568,7 +1482,7 @@ namespace Barotrauma { CampaignCharacterDiscarded = true; campaignCharacterInfo = null; - UpdatePlayerFrame(null, true); + UpdatePlayerFrame(null, true, parent); return true; }; confirmation.Buttons[1].OnClicked += confirmation.Close; @@ -1751,7 +1665,6 @@ namespace Barotrauma //make shuttles more dim in the sub list (selecting a shuttle as the main sub is allowed but not recommended) if (subList == this.subList.Content) { - shuttleText.RectTransform.RelativeOffset = new Vector2(0.1f, 0.0f); subTextBlock.TextColor *= 0.5f; foreach (GUIComponent child in frame.Children) { @@ -1759,6 +1672,17 @@ namespace Barotrauma } } } + else + { + var classText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), frame.RectTransform, Anchor.CenterRight), + TextManager.Get($"submarineclass.{sub.SubmarineClass}"), textAlignment: Alignment.CenterRight, font: GUI.SmallFont) + { + UserData = "classtext", + TextColor = subTextBlock.TextColor * 0.8f, + ToolTip = subTextBlock.RawToolTip + }; + } + } public bool VotableClicked(GUIComponent component, object userData) @@ -1807,12 +1731,12 @@ namespace Barotrauma { if (GameMain.Client.HasPermission(ClientPermissions.SelectMode)) { - string presetName = ((GameModePreset)(component.UserData)).Identifier; + string presetName = ((GameModePreset)component.UserData).Identifier; //display a verification prompt when switching away from the campaign if (HighlightedModeIndex == SelectedModeIndex && - (GameMain.NetLobbyScreen.ModeList.SelectedData as GameModePreset)?.Identifier == "multiplayercampaign" && - presetName != "multiplayercampaign") + (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") }); verificationBox.Buttons[0].OnClicked += (btn, userdata) => @@ -1863,7 +1787,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").Sprites[GUIComponent.ComponentState.None].FirstOrDefault().Sprite, scaleToFit: true) + sprite: GUI.Style.GetComponentStyle("GUISoundIcon").GetDefaultSprite(), scaleToFit: true) { UserData = new Pair("soundicon", 0.0f), CanBeFocused = false, @@ -3037,7 +2961,7 @@ namespace Barotrauma if (save) { - if (GameMain.GameSession?.GameMode?.IsRunning ?? false) + if (GameMain.GameSession?.IsRunning ?? false) { TabMenu.PendingChanges = true; CreateChangesPendingText(); @@ -3054,8 +2978,7 @@ namespace Barotrauma { if (modeIndex < 0 || modeIndex >= modeList.Content.CountChildren) { return; } - if (campaignUI != null && - ((GameModePreset)modeList.Content.GetChild(modeIndex).UserData).Identifier != "multiplayercampaign") + if ((GameModePreset)modeList.Content.GetChild(modeIndex).UserData != GameModePreset.MultiPlayerCampaign) { ToggleCampaignMode(false); } @@ -3063,8 +2986,12 @@ namespace Barotrauma if ((HighlightedModeIndex == selectedModeIndex || HighlightedModeIndex < 0) && modeList.SelectedIndex != modeIndex) { modeList.Select(modeIndex, true); } selectedModeIndex = modeIndex; - MissionTypeFrame.Visible = SelectedMode != null && SelectedMode.Identifier == "mission" && HighlightedModeIndex == SelectedModeIndex; - CampaignSetupFrame.Visible = !MissionTypeFrame.Visible && SelectedMode.Identifier == "multiplayercampaign"; + if (SelectedMode != GameModePreset.MultiPlayerCampaign && GameMain.GameSession?.GameMode is CampaignMode && Selected == this) + { + GameMain.GameSession = null; + } + + RefreshGameModeContent(); } public void HighlightMode(int modeIndex) @@ -3072,70 +2999,72 @@ namespace Barotrauma if (modeIndex < 0 || modeIndex >= modeList.Content.CountChildren) { return; } HighlightedModeIndex = modeIndex; - MissionTypeFrame.Visible = SelectedMode != null && SelectedMode.Identifier == "mission" && HighlightedModeIndex == SelectedModeIndex; - CampaignSetupFrame.Visible = SelectedMode != null && SelectedMode.Identifier == "multiplayercampaign"; + RefreshGameModeContent(); } - public void ToggleCampaignView(bool enabled) + private void RefreshGameModeContent() { - campaignContainer.Visible = enabled; - gameModeContainer.Visible = !enabled; + if (GameMain.Client == null) { return; } - campaignViewButton.Selected = enabled; - gameModeViewButton.Selected = !enabled; + autoRestartBox.Parent.Visible = true; + settingsBlocker.Visible = false; + if (SelectedMode == GameModePreset.Mission) + { + MissionTypeFrame.Visible = true; + CampaignFrame.Visible = CampaignSetupFrame.Visible = false; + } + else if (SelectedMode == GameModePreset.MultiPlayerCampaign) + { + MissionTypeFrame.Visible = autoRestartBox.Parent.Visible = false; + + if (GameMain.GameSession?.GameMode is CampaignMode campaign && campaign.Map != null) + { + //campaign running + settingsBlocker.Visible = true; + CampaignFrame.Visible = GameMain.Client.HasPermission(ClientPermissions.ManageCampaign); + ContinueCampaignButton.Enabled = !GameMain.Client.GameStarted && (GameMain.Client.HasPermission(ClientPermissions.ManageCampaign) || GameMain.Client.HasPermission(ClientPermissions.ManageRound)); + QuitCampaignButton.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageCampaign); + CampaignSetupFrame.Visible = false; + } + else + { + CampaignFrame.Visible = false; + CampaignSetupFrame.Visible = GameMain.Client.HasPermission(ClientPermissions.ManageCampaign); + } + } + else + { + MissionTypeFrame.Visible = CampaignFrame.Visible = CampaignSetupFrame.Visible = false; + CampaignFrame.Visible = CampaignSetupFrame.Visible = false; + } + + ReadyToStartBox.Parent.Visible = !GameMain.Client.GameStarted && SelectedMode != GameModePreset.MultiPlayerCampaign; + + StartButton.Visible = + GameMain.Client.HasPermission(ClientPermissions.ManageRound) && + !GameMain.Client.GameStarted && + !CampaignSetupFrame.Visible && + !CampaignFrame.Visible; } public void ToggleCampaignMode(bool enabled) { - ToggleCampaignView(enabled); - if (!enabled) { + //remove campaign character from the panel + if (campaignCharacterInfo != null) { UpdatePlayerFrame(null); } campaignCharacterInfo = null; CampaignCharacterDiscarded = false; - UpdatePlayerFrame(null); - } - - subList.Enabled = !enabled && AllowSubSelection; - shuttleList.Enabled = !enabled && GameMain.Client.HasPermission(ClientPermissions.SelectSub); - StartButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ManageRound) && !GameMain.Client.GameStarted && !enabled; - - if (campaignViewButton != null) { campaignViewButton.Enabled = enabled; } - - if (enabled) - { - if (campaignUI == null || campaignUI.Campaign != GameMain.GameSession.GameMode) - { - campaignContainer.ClearChildren(); - - campaignUI = new CampaignUI(GameMain.GameSession.GameMode as CampaignMode, campaignContainer) - { - StartRound = () => - { - GameMain.Client.RequestStartRound(); - CoroutineManager.StartCoroutine(WaitForStartRound(campaignUI.StartButton), "WaitForStartRound"); - } - }; - - var campaignMenuContainer = new GUIFrame(new RectTransform(new Vector2(0.4f, 1.0f), campaignContainer.RectTransform, Anchor.TopRight), style: null) - { - Color = Color.Black - }; - CampaignUI.SetMenuPanelParent(campaignMenuContainer.RectTransform); - CampaignUI.SetMissionPanelParent(campaignMenuContainer.RectTransform); - GameMain.GameSession.Map.CenterOffset = new Vector2(-campaignContainer.Rect.Width / 5, 0); - } - modeList.Select(2, true); } else { - campaignUI = null; + CampaignFrame.Visible = CampaignSetupFrame.Visible = false; } - - /*if (GameMain.Server != null) + RefreshEnabledElements(); + if (enabled) { - lastUpdateID++; - }*/ + modeList.Select(2, true); + } } public void TryDisplayCampaignSubmarine(SubmarineInfo submarine) @@ -3172,8 +3101,8 @@ namespace Barotrauma { if (!(button.UserData is Pair jobPrefab)) { return false; } - JobInfoFrame = jobPrefab.First.CreateInfoFrame(jobPrefab.Second); - GUIButton closeButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.05f), JobInfoFrame.GetChild(2).GetChild(0).RectTransform, Anchor.BottomRight), + JobInfoFrame = jobPrefab.First.CreateInfoFrame(out GUIComponent buttonContainer); + GUIButton closeButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.05f), buttonContainer.RectTransform, Anchor.BottomRight), TextManager.Get("Close")) { OnClicked = CloseJobInfo @@ -3281,7 +3210,7 @@ namespace Barotrauma if (!GameMain.Config.AreJobPreferencesEqual(jobNamePreferences)) { - if (GameMain.GameSession?.GameMode?.IsRunning ?? false) + if (GameMain.GameSession?.IsRunning ?? false) { TabMenu.PendingChanges = true; CreateChangesPendingText(); @@ -3310,6 +3239,9 @@ namespace Barotrauma public Pair FailedSelectedSub; public Pair FailedSelectedShuttle; + public List> FailedCampaignSubs = new List>(); + public List> FailedOwnedSubs = new List>(); + public bool TrySelectSub(string subName, string md5Hash, GUIListBox subList) { if (GameMain.Client == null) { return false; } @@ -3423,6 +3355,77 @@ namespace Barotrauma return false; } + public bool CheckIfCampaignSubMatches(SubmarineInfo serverSubmarine, string deliveryData) + { + if (GameMain.Client == null) return false; + + //already downloading the selected sub file + if (GameMain.Client.FileReceiver.ActiveTransfers.Any(t => t.FileName == serverSubmarine.Name + ".sub")) + { + return false; + } + + SubmarineInfo purchasableSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == serverSubmarine.Name && s.MD5Hash?.Hash == serverSubmarine.MD5Hash?.Hash); + if (purchasableSub != null) + { + return true; + } + + purchasableSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == serverSubmarine.Name); + + string errorMsg = ""; + if (purchasableSub == null) + { + errorMsg = TextManager.GetWithVariable("SubNotFoundError", "[subname]", serverSubmarine.Name) + " "; + } + else if (purchasableSub.MD5Hash?.Hash == null) + { + errorMsg = TextManager.GetWithVariable("SubLoadError", "[subname]", serverSubmarine.Name) + " "; + /*GUITextBlock textBlock = subList.Content.GetChildByUserData(sub)?.GetChild(); + if (textBlock != null) { textBlock.TextColor = GUI.Style.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.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 string[] { TextManager.Get("Yes"), TextManager.Get("No") }) + { + UserData = "request" + serverSubmarine.Name + }; + requestFileBox.Buttons[0].UserData = new string[] { serverSubmarine.Name, serverSubmarine.MD5Hash.Hash }; + requestFileBox.Buttons[0].OnClicked += requestFileBox.Close; + requestFileBox.Buttons[0].OnClicked += (GUIButton button, object userdata) => + { + string[] fileInfo = (string[])userdata; + + if (deliveryData == "owned") + { + FailedOwnedSubs.Add(new Pair(fileInfo[0], fileInfo[1])); + } + else if (deliveryData == "campaign") + { + FailedCampaignSubs.Add(new Pair(fileInfo[0], fileInfo[1])); + } + + GameMain.Client?.RequestFile(FileTransferType.Submarine, fileInfo[0], fileInfo[1]); + return true; + }; + requestFileBox.Buttons[1].OnClicked += requestFileBox.Close; + + return false; + } + private void CreateSubPreview(SubmarineInfo sub) { subPreviewContainer?.ClearChildren(); @@ -3442,5 +3445,10 @@ namespace Barotrauma } } } + + public void OnRoundEnded() + { + CampaignCharacterDiscarded = false; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs index 5d7eba886..80e3a1c02 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs @@ -88,9 +88,8 @@ namespace Barotrauma public ParticleEditorScreen() { cam = new Camera(); - GameMain.Instance.OnResolutionChanged += CreateUI; + GameMain.Instance.ResolutionChanged += CreateUI; CreateUI(); - } private void CreateUI() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/RoundSummaryScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/RoundSummaryScreen.cs new file mode 100644 index 000000000..4f37330f2 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/RoundSummaryScreen.cs @@ -0,0 +1,55 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.Collections.Generic; + +namespace Barotrauma +{ + class RoundSummaryScreen : Screen + { + private Sprite backgroundSprite; + private RoundSummary roundSummary; + private string loadText; + + private RectTransform prevGuiElementParent; + + public static RoundSummaryScreen Select(Sprite backgroundSprite, RoundSummary roundSummary) + { + var summaryScreen = new RoundSummaryScreen() + { + roundSummary = roundSummary, + backgroundSprite = backgroundSprite, + prevGuiElementParent = roundSummary.Frame.RectTransform.Parent, + loadText = TextManager.Get("campaignstartingpleasewait") + }; + roundSummary.Frame.RectTransform.Parent = summaryScreen.Frame.RectTransform; + summaryScreen.Select(); + summaryScreen.AddToGUIUpdateList(); + return summaryScreen; + } + + public override void Deselect() + { + roundSummary.Frame.RectTransform.Parent = prevGuiElementParent; + } + + public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) + { + spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); + + if (backgroundSprite != null) + { + float scale = Math.Max(GameMain.GraphicsWidth / backgroundSprite.size.X, GameMain.GraphicsHeight / backgroundSprite.size.Y); + backgroundSprite.Draw(spriteBatch, new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) / 2, Color.White, backgroundSprite.size / 2, scale: scale); + } + + 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); + + spriteBatch.End(); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs index 08266e537..666bb8471 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs @@ -64,6 +64,12 @@ namespace Barotrauma GUI.ScreenOverlayColor = to; yield return CoroutineStatus.Success; - } + } + + public virtual void Release() + { + frame.RectTransform.Parent = null; + frame = null; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs index 4f0ab96eb..b6c376970 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs @@ -67,7 +67,7 @@ namespace Barotrauma private List favoriteServers; private List recentServers; - private readonly HashSet activePings = new HashSet(); + private readonly Dictionary activePings = new Dictionary(); private enum ServerListTab { @@ -161,7 +161,7 @@ namespace Barotrauma private const float sidebarWidth = 0.2f; public ServerListScreen() { - GameMain.Instance.OnResolutionChanged += CreateUI; + GameMain.Instance.ResolutionChanged += CreateUI; CreateUI(); } @@ -953,6 +953,7 @@ namespace Barotrauma base.Update(deltaTime); UpdateFriendsList(); + UpdateInfoQueries(); if (PlayerInput.PrimaryMouseButtonClicked()) { @@ -984,7 +985,7 @@ namespace Barotrauma //never show newer versions //(ignore revision number, it doesn't affect compatibility) if (remoteVersion != null && - (remoteVersion.Major > GameMain.Version.Major || remoteVersion.Minor > GameMain.Version.Minor || remoteVersion.Build > GameMain.Version.Build)) + ToolBox.VersionNewerIgnoreRevision(GameMain.Version, remoteVersion)) { child.Visible = false; } @@ -1047,6 +1048,28 @@ namespace Barotrauma serverList.UpdateScrollBarSize(); } + private Queue pendingQueries = new Queue(); + int activeQueries = 0; + private void QueueInfoQuery(ServerInfo info) + { + pendingQueries.Enqueue(info); + } + + private void OnQueryDone(ServerInfo info) + { + activeQueries--; + } + + public void UpdateInfoQueries() + { + while (activeQueries < 25 && pendingQueries.Count > 0) + { + activeQueries++; + var info = pendingQueries.Dequeue(); + info.QueryLiveInfo(UpdateServerInfo, OnQueryDone); + } + } + private void ShowDirectJoinPrompt() { var msgBox = new GUIMessageBox(TextManager.Get("ServerListDirectJoin"), "", @@ -1134,7 +1157,7 @@ namespace Barotrauma SelectedTab = ServerListTab.Favorites; FilterServers(); - serverInfo.QueryLiveInfo(UpdateServerInfo); + QueueInfoQuery(serverInfo); msgBox.Close(); return false; @@ -1276,11 +1299,12 @@ namespace Barotrauma avatarFunc = Steamworks.SteamFriends.GetLargeAvatarAsync; break; } - TaskPool.Add(avatarFunc(friend.Id), (Task task) => + TaskPool.Add($"Get{avatarSize}AvatarAsync", avatarFunc(friend.Id), (task) => { - if (!task.Result.HasValue) { return; } + Steamworks.Data.Image? img = ((Task)task).Result; + if (!img.HasValue) { return; } - var avatarImage = task.Result.Value; + var avatarImage = img.Value; const int desaturatedWeight = 180; @@ -1477,10 +1501,13 @@ namespace Barotrauma CoroutineManager.StopCoroutines("EstimateLobbyPing"); - TaskPool.Add(Steamworks.SteamNetworkingUtils.WaitForPingDataAsync(), (task) => + if (SteamManager.IsInitialized) { - steamPingInfoReady = true; - }); + TaskPool.Add("WaitForPingDataAsync (serverlist)", Steamworks.SteamNetworkingUtils.WaitForPingDataAsync(), (task) => + { + steamPingInfoReady = true; + }); + } friendsListUpdateTime = Timing.TotalTime - 1.0; UpdateFriendsList(); @@ -1526,7 +1553,7 @@ namespace Barotrauma foreach (ServerInfo info in knownServers) { AddToServerList(info); - info.QueryLiveInfo(UpdateServerInfo); + QueueInfoQuery(info); } } } @@ -1823,7 +1850,7 @@ namespace Barotrauma yield return CoroutineStatus.Running; } - Steamworks.Data.PingLocation pingLocation = serverInfo.PingLocation.Value; + Steamworks.Data.NetPingLocation pingLocation = serverInfo.PingLocation.Value; serverInfo.Ping = Steamworks.SteamNetworkingUtils.LocalPingLocation?.EstimatePingTo(pingLocation) ?? -1; serverInfo.PingChecked = true; serverPingText.TextColor = GetPingTextColor(serverInfo.Ping); @@ -1977,25 +2004,25 @@ namespace Barotrauma lock (activePings) { - if (activePings.Contains(serverInfo.IP)) { return; } - activePings.Add(serverInfo.IP); + if (activePings.ContainsKey(serverInfo.IP)) { return; } + activePings.Add(serverInfo.IP, activePings.Any() ? activePings.Values.Max()+1 : 0); } serverInfo.PingChecked = false; serverInfo.Ping = -1; - TaskPool.Add(PingServerAsync(serverInfo?.IP, 1000), + TaskPool.Add($"PingServerAsync ({serverInfo?.IP ?? "NULL"})", PingServerAsync(serverInfo.IP, 1000), new Tuple(serverInfo, serverPingText), (rtt, obj) => { var info = obj.Item1; var text = obj.Item2; - info.Ping = rtt.Result; info.PingChecked = true; + info.Ping = ((Task)rtt).Result; info.PingChecked = true; text.TextColor = GetPingTextColor(info.Ping); text.Text = info.Ping > -1 ? info.Ping.ToString() : "?"; lock (activePings) { - activePings.Remove(serverInfo.IP); + activePings.Remove(info.IP); } }); } @@ -2009,12 +2036,12 @@ namespace Barotrauma public async Task PingServerAsync(string ip, int timeOut) { await Task.Yield(); - int activePingCount = 100; - while (activePingCount > 25) + bool shouldGo = false; + while (!shouldGo) { lock (activePings) { - activePingCount = activePings.Count; + shouldGo = activePings.Count(kvp => kvp.Value < activePings[ip]) < 25; } await Task.Delay(25); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs index 932d9a409..255b7fa58 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs @@ -64,7 +64,7 @@ namespace Barotrauma public SpriteEditorScreen() { cam = new Camera(); - GameMain.Instance.OnResolutionChanged += CreateUI; + GameMain.Instance.ResolutionChanged += CreateUI; CreateUI(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs index 78efe5617..00ca09c65 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs @@ -15,6 +15,7 @@ namespace Barotrauma { private GUIFrame menu; private GUIListBox subscribedItemList, topItemList; + private GUITextBox subscribedItemFilter, topItemFilter; private GUIListBox publishedItemList, myItemList; @@ -66,7 +67,7 @@ namespace Barotrauma public SteamWorkshopScreen() { - GameMain.Instance.OnResolutionChanged += CreateUI; + GameMain.Instance.ResolutionChanged += CreateUI; CreateUI(); Steamworks.SteamUGC.GlobalOnItemInstalled += OnItemInstalled; @@ -135,7 +136,7 @@ namespace Barotrauma } }; - CreateFilterBox(modsContainer, subscribedItemList); + subscribedItemFilter = CreateFilterBox(modsContainer, subscribedItemList); modsPreviewFrame = new GUIFrame(new RectTransform(new Vector2(0.6f, 1.0f), tabs[(int)Tab.Mods].RectTransform, Anchor.TopRight), style: null); @@ -165,7 +166,7 @@ namespace Barotrauma } }; - CreateFilterBox(listContainer, topItemList); + topItemFilter = CreateFilterBox(listContainer, topItemList); new GUIButton(new RectTransform(new Vector2(1.0f, 0.02f), listContainer.RectTransform), TextManager.Get("FindModsButton"), style: "GUIButtonSmall") { @@ -239,7 +240,7 @@ namespace Barotrauma subscribedCoroutine = CoroutineManager.StartCoroutine(PollSubscribedItems()); } - private void CreateFilterBox(GUIComponent parent, GUIListBox listbox) + private GUITextBox CreateFilterBox(GUIComponent parent, GUIListBox listbox) { var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), parent.RectTransform), isHorizontal: true) { @@ -260,6 +261,8 @@ namespace Barotrauma } return true; }; + + return searchBox; } public override void Select() @@ -475,6 +478,18 @@ namespace Barotrauma return; } + string text = string.Empty; + if (listBox == subscribedItemList) + { + text = subscribedItemFilter.Text; + } + else if (listBox == topItemList) + { + text = topItemFilter.Text; + } + + bool visible = string.IsNullOrEmpty(text) ? true : (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) @@ -486,7 +501,8 @@ namespace Barotrauma var itemFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), listBox.Content.RectTransform, minSize: new Point(0, 80)), style: "ListBoxElement") { - UserData = item + UserData = item, + Visible = visible }; if (prevIndex > -1) { @@ -615,7 +631,7 @@ namespace Barotrauma { DebugConsole.NewMessage(errorMsg, Color.Red); titleText.TextColor = Color.Red; - titleText.ToolTip = itemFrame.ToolTip = TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { TextManager.EnsureUTF8(item?.Title), errorMsg }); + titleText.ToolTip = itemFrame.ToolTip = TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { item?.Title, errorMsg }); } } } @@ -630,7 +646,7 @@ namespace Barotrauma { DebugConsole.NewMessage(errorMsg, Color.Red); titleText.TextColor = Color.Red; - titleText.ToolTip = itemFrame.ToolTip = TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { TextManager.EnsureUTF8(item?.Title), errorMsg }); + titleText.ToolTip = itemFrame.ToolTip = TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { item?.Title, errorMsg }); } } } @@ -765,7 +781,7 @@ namespace Barotrauma { if (response.ResponseStatus == ResponseStatus.Completed) { - TaskPool.Add(WritePreviewImageAsync(response, previewImagePath), (task) => { action?.Invoke(); }); + TaskPool.Add("WritePreviewImageAsync", WritePreviewImageAsync(response, previewImagePath), (task) => { action?.Invoke(); }); } } @@ -799,7 +815,7 @@ namespace Barotrauma if (File.Exists(previewImagePath)) { - TaskPool.Add(LoadPreviewImageAsync(item?.PreviewImageUrl, previewImagePath), + TaskPool.Add("LoadPreviewImageAsync", LoadPreviewImageAsync(item?.PreviewImageUrl, previewImagePath), new Tuple(item, listBox), (task, tuple) => { @@ -807,7 +823,7 @@ namespace Barotrauma var previewImage = lb.Content.FindChild(item)?.GetChildByUserData("previewimage") as GUIImage; if (previewImage != null) { - previewImage.Sprite = task.Result; + previewImage.Sprite = ((Task)task).Result; } else { @@ -1449,7 +1465,7 @@ namespace Barotrauma { if (itemEditor == null) { return false; } RemoveItemFromLists(itemEditor.Value.FileId); - TaskPool.Add(Steamworks.SteamUGC.DeleteFileAsync(itemEditor.Value.FileId), + TaskPool.Add("DeleteFileAsync", Steamworks.SteamUGC.DeleteFileAsync(itemEditor.Value.FileId), (t) => { if (t.Status == TaskStatus.Faulted) @@ -1600,7 +1616,7 @@ namespace Barotrauma if (contentFile.Type == ContentType.Executable || contentFile.Type == ContentType.ServerExecutable) { - fileExists |= File.Exists(contentFile.Path + ".dll"); + fileExists |= File.Exists(Path.GetFileNameWithoutExtension(contentFile.Path) + ".dll"); } if (!fileExists) @@ -1640,7 +1656,7 @@ namespace Barotrauma if (contentFile.Type == ContentType.Executable || contentFile.Type == ContentType.ServerExecutable) { - fileExists |= File.Exists(contentFile.Path + ".dll"); + 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) }, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 544e271a0..e0789d9cc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -8,6 +8,7 @@ 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 @@ -294,6 +295,7 @@ namespace Barotrauma }; foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) { + if (sub.Type != SubmarineType.Player) { continue; } linkedSubBox.AddItem(sub.Name, sub); } linkedSubBox.OnSelected += SelectLinkedSub; @@ -715,8 +717,13 @@ namespace Barotrauma GameMain.GameScreen.Select(); - GameSession gameSession = new GameSession(backedUpSubInfo, "", GameModePreset.List.Find(gm => gm.Identifier == "subtest"), null); + GameSession gameSession = new GameSession(backedUpSubInfo, "", GameModePreset.TestMode, null); gameSession.StartRound(null, false); + (gameSession.GameMode as TestGameMode).OnRoundEnd = () => + { + Submarine.Unload(); + GameMain.SubEditorScreen.Select(); + }; return true; } @@ -824,7 +831,8 @@ namespace Barotrauma OnClicked = (btn, userData) => { ItemAssemblyPrefab assemblyPrefab = (ItemAssemblyPrefab) userData; - if (assemblyPrefab != null) { + if (assemblyPrefab != null) + { var msgBox = new GUIMessageBox( TextManager.Get("DeleteDialogLabel"), TextManager.GetWithVariable("DeleteDialogQuestion", "[file]", assemblyPrefab.Name), @@ -873,6 +881,11 @@ namespace Barotrauma new Color(20, 20, 20, 255); UpdateEntityList(); + if (!wasSelectedBefore) + { + OpenEntityMenu(MapEntityCategory.Structure); + wasSelectedBefore = true; + } isAutoSaving = false; if (!wasSelectedBefore) @@ -906,7 +919,6 @@ namespace Barotrauma Submarine.MainSub = new Submarine(subInfo); } - Submarine.MainSub.SetPrevTransform(Submarine.MainSub.Position); Submarine.MainSub.UpdateTransform(interpolate: false); cam.Position = Submarine.MainSub.Position + Submarine.MainSub.HiddenSubPosition; @@ -916,6 +928,7 @@ namespace Barotrauma linkedSubBox.ClearChildren(); foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) { + if (sub.Type != SubmarineType.Player) { continue; } linkedSubBox.AddItem(sub.Name, sub); } @@ -1214,12 +1227,89 @@ namespace Barotrauma nameBox.Flash(); return false; } - var result = SaveSubToFile(nameBox.Text); + + string specialSavePath = ""; + if (Submarine.MainSub.Info.Type != SubmarineType.Player) + { + ContentType contentType = ContentType.Submarine; + switch (Submarine.MainSub.Info.Type) + { + case SubmarineType.OutpostModule: + if (Submarine.MainSub.Info?.OutpostModuleInfo != null) + { + contentType = ContentType.OutpostModule; + } + break; + case SubmarineType.Outpost: + contentType = ContentType.Outpost; + break; + case SubmarineType.Wreck: + contentType = ContentType.Wreck; + break; + } + if (contentType != ContentType.Submarine) + { +#if DEBUG + var existingFiles = ContentPackage.GetFilesOfType(GameMain.VanillaContent.ToEnumerable(), contentType); +#else + var existingFiles = ContentPackage.GetFilesOfType(GameMain.Config.SelectedContentPackages.Where(c => c != GameMain.VanillaContent), contentType); +#endif + specialSavePath = existingFiles.FirstOrDefault(f => + Path.GetFullPath(f.Path) != Path.GetFullPath(SubmarineInfo.SavePath) && ContentPackage.IsModFilePathAllowed(f.Path))?.Path; + if (!string.IsNullOrEmpty(specialSavePath)) + { + specialSavePath = Path.GetDirectoryName(specialSavePath); + } + } + } + else if (Submarine.MainSub.Info.SubmarineClass == SubmarineClass.Undefined && !Submarine.MainSub.Info.HasTag(SubmarineTag.Shuttle)) + { + var msgBox = new GUIMessageBox(TextManager.Get("warning"), TextManager.Get("undefinedsubmarineclasswarning"), new string[] { TextManager.Get("yes"), TextManager.Get("no") }); + + msgBox.Buttons[0].OnClicked = (bt, userdata) => + { + SaveSubToFile(nameBox.Text); + saveFrame = null; + msgBox.Close(); + return true; + }; + msgBox.Buttons[1].OnClicked = (bt, userdata) => + { + msgBox.Close(); + return true; + }; + return true; + } + + 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)) + { + var msgBox = new GUIMessageBox("", TextManager.GetWithVariables("savesubtospecialfolderprompt", + new string[] { "[type]", "[outpostpath]" }, new string[] { TextManager.Get("submarinetype." + Submarine.MainSub.Info.Type), specialSavePath }), + new string[] { 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); saveFrame = null; return result; } - private bool SaveSubToFile(string name) + private bool SaveSubToFile(string name, string specialSavePath = null) { if (string.IsNullOrWhiteSpace(name)) { @@ -1236,7 +1326,37 @@ namespace Barotrauma string savePath = name + ".sub"; string prevSavePath = null; - if (!string.IsNullOrEmpty(Submarine.MainSub?.Info.FilePath) && + string directoryName = Submarine.MainSub?.Info?.FilePath == null ? + SubmarineInfo.SavePath : Path.GetDirectoryName(Submarine.MainSub.Info.FilePath); + if (!string.IsNullOrEmpty(specialSavePath)) + { + directoryName = specialSavePath; + savePath = Path.Combine(directoryName, savePath); + ContentPackage contentPackage = GameMain.Config.SelectedContentPackages.Find(cp => cp.Files.Any(f => Path.GetDirectoryName(f.Path) == 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)) + { + var msgBox = new GUIMessageBox("", TextManager.GetWithVariable("addtocontentpackageprompt", "[packagename]", contentPackage.Name), + new string[] { TextManager.Get("yes"), TextManager.Get("no") }); + msgBox.Buttons[0].OnClicked = (bt, userdata) => + { + contentPackage.AddFile(savePath, ContentType.OutpostModule); + contentPackage.Save(contentPackage.Path); + msgBox.Close(); + return true; + }; + msgBox.Buttons[1].OnClicked = (bt, userdata) => + { + msgBox.Close(); + return true; + }; + } + } + else if (!string.IsNullOrEmpty(Submarine.MainSub?.Info.FilePath) && Submarine.MainSub.Info.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) { prevSavePath = Submarine.MainSub.Info.FilePath.CleanUpPath(); @@ -1251,11 +1371,28 @@ namespace Barotrauma if (contentPackage != null) { Steamworks.Data.PublishedFileId packageId = Steam.SteamManager.GetWorkshopItemIDFromUrl(contentPackage.SteamWorkshopUrl); - Steamworks.Ugc.Item? item = Steamworks.Ugc.Item.GetAsync(packageId).Result; + + 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; - contentPackage.Files.Add(new ContentFile(Path.Combine(prevDir, savePath).CleanUpPath(), ContentType.Submarine)); + string targetPath = Path.Combine(prevDir, savePath).CleanUpPath(); + if (!contentPackage.Files.Any(f => f.Type == ContentType.Submarine && + f.Path.CleanUpPath().Equals(targetPath, StringComparison.InvariantCultureIgnoreCase))) + { + contentPackage.Files.Add(new ContentFile(targetPath, ContentType.Submarine)); + } contentPackage.Save(contentPackage.Path); } } @@ -1311,8 +1448,11 @@ namespace Barotrauma if (prevSavePath != null && prevSavePath != savePath) { SubmarineInfo.RefreshSavedSub(prevSavePath); } linkedSubBox.ClearChildren(); - foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) { linkedSubBox.AddItem(sub.Name, sub); } - + foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) + { + if (sub.Type != SubmarineType.Player) { continue; } + linkedSubBox.AddItem(sub.Name, sub); + } subNameLabel.Text = ToolBox.LimitString(Submarine.MainSub.Info.Name, subNameLabel.Font, subNameLabel.Rect.Width); } @@ -1335,8 +1475,8 @@ namespace Barotrauma }; new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, saveFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); - - var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.4f, 0.5f), saveFrame.RectTransform, Anchor.Center) { MinSize = new Point(750, 400) }); + + 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); @@ -1374,13 +1514,13 @@ namespace Barotrauma submarineNameCharacterCount.Text = nameBox.Text.Length + " / " + submarineNameLimit; - var descriptionHeaderGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.03f), leftColumn.RectTransform), true); + 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); 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), + descriptionBox = new GUITextBox(new RectTransform(Vector2.One, descriptionContainer.Content.RectTransform, Anchor.Center), font: GUI.SmallFont, style: "GUITextBoxNoBorder", wrap: true, textAlignment: Alignment.TopLeft) { Padding = new Vector4(10 * GUI.Scale) @@ -1405,7 +1545,292 @@ namespace Barotrauma descriptionBox.Text = GetSubDescription(); - var crewSizeArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.04f), leftColumn.RectTransform), isHorizontal: true) + var subTypeContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.01f), leftColumn.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true + }; + + new GUITextBlock(new RectTransform(new Vector2(0.4f, 1f), subTypeContainer.RectTransform), TextManager.Get("submarinetype")); + var subTypeDropdown = new GUIDropDown(new RectTransform(new Vector2(0.6f, 1f), subTypeContainer.RectTransform)); + subTypeContainer.RectTransform.MinSize = new Point(0, subTypeContainer.RectTransform.Children.Max(c => c.MinSize.Y)); + subTypeDropdown.AddItem(TextManager.Get("submarinetype.player"), SubmarineType.Player); + subTypeDropdown.AddItem(TextManager.Get("submarinetype.outpostmodule"), SubmarineType.OutpostModule); + subTypeDropdown.AddItem(TextManager.Get("submarinetype.outpost"), SubmarineType.Outpost); + subTypeDropdown.AddItem(TextManager.Get("submarinetype.wreck"), SubmarineType.Wreck); + + //--------------------------------------- + + var outpostSettingsContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), leftColumn.RectTransform)) + { + IgnoreLayoutGroups = true, + CanBeFocused = true, + Visible = false, + Stretch = true + }; + new GUIFrame(new RectTransform(Vector2.One, outpostSettingsContainer.RectTransform), "InnerFrame") + { + IgnoreLayoutGroups = true + }; + + // module flags --------------------- + + 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 (var sub in SubmarineInfo.SavedSubmarines) + { + if (sub.OutpostModuleInfo == null) { continue; } + foreach (string flag in sub.OutpostModuleInfo.ModuleFlags) + { + if (flag == "none") { continue; } + availableFlags.Add(flag); + } + } + + 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) + { + moduleTypeDropDown.AddItem(TextManager.Capitalize(flag), flag); + if (Submarine.MainSub?.Info?.OutpostModuleInfo == null) { continue; } + if (Submarine.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()); + moduleTypeDropDown.Text = ToolBox.LimitString( + Submarine.MainSub.Info.OutpostModuleInfo.ModuleFlags.Any(f => f != "none") ? moduleTypeDropDown.Text : "None", + moduleTypeDropDown.Font, moduleTypeDropDown.Rect.Width); + return true; + }; + outpostModuleGroup.RectTransform.MinSize = new Point(0, outpostModuleGroup.RectTransform.Children.Max(c => c.MinSize.Y)); + + + + + + // module flags --------------------- + + var allowAttachGroup = 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), 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); + 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))) + { + allowAttachDropDown.SelectItem("any"); + } + foreach (string 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)) + { + allowAttachDropDown.SelectItem(flag); + } + } + allowAttachDropDown.OnSelected += (_, __) => + { + if (Submarine.MainSub?.Info?.OutpostModuleInfo == null) { return false; } + Submarine.MainSub.Info.OutpostModuleInfo.SetAllowAttachTo(allowAttachDropDown.SelectedDataMultiple.Cast()); + allowAttachDropDown.Text = ToolBox.LimitString( + Submarine.MainSub.Info.OutpostModuleInfo.ModuleFlags.Any(f => f != "none") ? allowAttachDropDown.Text : "None", + allowAttachDropDown.Font, allowAttachDropDown.Rect.Width); + return true; + }; + allowAttachGroup.RectTransform.MinSize = new Point(0, allowAttachGroup.RectTransform.Children.Max(c => c.MinSize.Y)); + + + + + + + + + + + + + // location types --------------------- + + 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); } + + 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) + { + locationTypeDropDown.AddItem(TextManager.Capitalize(locationType), locationType); + if (Submarine.MainSub?.Info?.OutpostModuleInfo == null) { continue; } + if (Submarine.MainSub.Info.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType)) + { + locationTypeDropDown.SelectItem(locationType); + } + } + if (!Submarine.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); + return true; + }; + locationTypeGroup.RectTransform.MinSize = new Point(0, locationTypeGroup.RectTransform.Children.Max(c => c.MinSize.Y)); + + + // gap positions --------------------- + + var gapPositionGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + + 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); + + Submarine.MainSub.Info?.OutpostModuleInfo?.DetermineGapPositions(Submarine.MainSub); + foreach (var gapPos in Enum.GetValues(typeof(OutpostModuleInfo.GapPosition))) + { + if ((OutpostModuleInfo.GapPosition)gapPos == OutpostModuleInfo.GapPosition.None) { continue; } + gapPositionDropDown.AddItem(TextManager.Capitalize(gapPos.ToString()), gapPos); + if (Submarine.MainSub.Info?.OutpostModuleInfo?.GapPositions.HasFlag((OutpostModuleInfo.GapPosition)gapPos) ?? false) + { + gapPositionDropDown.SelectItem(gapPos); + } + } + + gapPositionDropDown.OnSelected += (_, __) => + { + if (Submarine.MainSub.Info?.OutpostModuleInfo == null) { return false; } + Submarine.MainSub.Info.OutpostModuleInfo.GapPositions = OutpostModuleInfo.GapPosition.None; + if (gapPositionDropDown.SelectedDataMultiple.Any()) + { + List gapPosTexts = new List(); + foreach (OutpostModuleInfo.GapPosition gapPos in gapPositionDropDown.SelectedDataMultiple) + { + Submarine.MainSub.Info.OutpostModuleInfo.GapPositions |= gapPos; + gapPosTexts.Add(TextManager.Capitalize(gapPos.ToString())); + } + gapPositionDropDown.Text = ToolBox.LimitString(string.Join(", ", gapPosTexts), gapPositionDropDown.Font, gapPositionDropDown.Rect.Width); + } + else + { + gapPositionDropDown.Text = ToolBox.LimitString("None", gapPositionDropDown.Font, gapPositionDropDown.Rect.Width); + } + return true; + }; + gapPositionGroup.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) + { + Stretch = true + }; + new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), maxModuleCountGroup.RectTransform), + TextManager.Get("OutPostModuleMaxCount"), textAlignment: Alignment.CenterLeft, wrap: true) + { + ToolTip = TextManager.Get("OutPostModuleMaxCountToolTip") + }; + 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, + MinValueInt = 0, + MaxValueInt = 1000, + OnValueChanged = (numberInput) => + { + Submarine.MainSub.Info.OutpostModuleInfo.MaxCount = numberInput.IntValue; + } + }; + + var commonnessGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), outpostSettingsContainer.RectTransform), isHorizontal: true) + { + Stretch = true + }; + new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), commonnessGroup.RectTransform), + TextManager.Get("subeditor.outpostcommonness"), textAlignment: Alignment.CenterLeft, wrap: true); + new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), commonnessGroup.RectTransform), GUINumberInput.NumberType.Int) + { + FloatValue = Submarine.MainSub?.Info?.OutpostModuleInfo?.Commonness ?? 10, + MinValueFloat = 0, + MaxValueFloat = 100, + OnValueChanged = (numberInput) => + { + Submarine.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)); + + //------------------------------------------------------------------ + + var subSettingsContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), leftColumn.RectTransform)) + { + Stretch = true + }; + new GUIFrame(new RectTransform(Vector2.One, subSettingsContainer.RectTransform), "InnerFrame") + { + IgnoreLayoutGroups = true + }; + + var priceGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true) + { + Stretch = true + }; + 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; + 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), + MinValueInt = basePrice, + MaxValueInt = 999999, + OnValueChanged = (numberInput) => + { + Submarine.MainSub.Info.Price = numberInput.IntValue; + } + }; + + if (!Submarine.MainSub.Info.HasTag(SubmarineTag.Shuttle)) + { + var classGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true) + { + Stretch = true + }; + new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), classGroup.RectTransform), + TextManager.Get("submarineclass"), textAlignment: Alignment.CenterLeft, wrap: true); + GUIDropDown classDropDown = new GUIDropDown(new RectTransform(new Vector2(0.4f, 1.0f), classGroup.RectTransform)); + classDropDown.RectTransform.MinSize = new Point(0, subTypeContainer.RectTransform.Children.Max(c => c.MinSize.Y)); + classDropDown.AddItem(TextManager.Get("submarineclass.undefined"), SubmarineClass.Undefined); + classDropDown.AddItem(TextManager.Get("submarineclass.scout"), SubmarineClass.Scout); + classDropDown.AddItem(TextManager.Get("submarineclass.attack"), SubmarineClass.Attack); + classDropDown.AddItem(TextManager.Get("submarineclass.transport"), SubmarineClass.Transport); + classDropDown.AddItem(TextManager.Get("submarineclass.deepdiver"), SubmarineClass.DeepDiver); + classDropDown.OnSelected += (selected, userdata) => + { + SubmarineClass submarineClass = (SubmarineClass)userdata; + Submarine.MainSub.Info.SubmarineClass = submarineClass; + return true; + }; + + classDropDown.SelectItem(Submarine.MainSub.Info.SubmarineClass); + } + + var crewSizeArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true) { Stretch = true, AbsoluteSpacing = 5 @@ -1413,13 +1838,13 @@ 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); - var crewSizeMin = new GUINumberInput(new RectTransform(new Vector2(0.1f, 1.0f), crewSizeArea.RectTransform), GUINumberInput.NumberType.Int) + var crewSizeMin = new GUINumberInput(new RectTransform(new Vector2(0.17f, 1.0f), crewSizeArea.RectTransform), GUINumberInput.NumberType.Int, relativeButtonAreaWidth: 0.25f) { MinValueInt = 1, MaxValueInt = 128 }; - new GUITextBlock(new RectTransform(new Vector2(0.1f, 1.0f), crewSizeArea.RectTransform), "-", textAlignment: Alignment.Center); - var crewSizeMax = new GUINumberInput(new RectTransform(new Vector2(0.1f, 1.0f), crewSizeArea.RectTransform), GUINumberInput.NumberType.Int) + new GUITextBlock(new RectTransform(new Vector2(0.06f, 1.0f), crewSizeArea.RectTransform), "-", textAlignment: Alignment.Center); + var crewSizeMax = new GUINumberInput(new RectTransform(new Vector2(0.17f, 1.0f), crewSizeArea.RectTransform), GUINumberInput.NumberType.Int, relativeButtonAreaWidth: 0.25f) { MinValueInt = 1, MaxValueInt = 128 @@ -1439,7 +1864,7 @@ namespace Barotrauma Submarine.MainSub.Info.RecommendedCrewSizeMax = crewSizeMax.IntValue; }; - var crewExpArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.04f), leftColumn.RectTransform), isHorizontal: true) + var crewExpArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true) { Stretch = true, AbsoluteSpacing = 5 @@ -1484,9 +1909,27 @@ namespace Barotrauma crewExperienceLevels[0] : Submarine.MainSub.Info.RecommendedCrewExperience; experienceText.Text = TextManager.Get((string)experienceText.UserData); } - + + subTypeDropdown.OnSelected += (selected, userdata) => + { + SubmarineType type = (SubmarineType)userdata; + Submarine.MainSub.Info.Type = type; + if (type == SubmarineType.OutpostModule) + { + Submarine.MainSub.Info.OutpostModuleInfo ??= new OutpostModuleInfo(Submarine.MainSub.Info); + } + outpostSettingsContainer.Visible = type == SubmarineType.OutpostModule; + outpostSettingsContainer.IgnoreLayoutGroups = !outpostSettingsContainer.Visible; + + subSettingsContainer.Visible = type == SubmarineType.Player; + subSettingsContainer.IgnoreLayoutGroups = !subSettingsContainer.Visible; + return true; + }; + subTypeDropdown.SelectItem(Submarine.MainSub.Info.Type); + 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); var previewImageHolder = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), rightColumn.RectTransform), style: null) { Color = Color.Black, CanBeFocused = false }; @@ -1633,6 +2076,12 @@ namespace Barotrauma }; paddedSaveFrame.Recalculate(); leftColumn.Recalculate(); + + subSettingsContainer.RectTransform.MinSize = outpostSettingsContainer.RectTransform.MinSize = + new Point(0, Math.Max(subSettingsContainer.Rect.Height, outpostSettingsContainer.Rect.Height)); + subSettingsContainer.Recalculate(); + outpostSettingsContainer.Recalculate(); + descriptionBox.Text = Submarine.MainSub == null ? "" : Submarine.MainSub.Info.Description; submarineDescriptionCharacterCount.Text = descriptionBox.Text.Length + " / " + submarineDescriptionLimit; @@ -1843,8 +2292,23 @@ namespace Barotrauma searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; searchBox.OnTextChanged += (textBox, text) => { FilterSubs(subList, text); return true; }; - foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) + List sortedSubs = new List(SubmarineInfo.SavedSubmarines); + sortedSubs.Sort((s1, s2) => { return s1.Type.CompareTo(s2.Type) * 100 + s1.Name.CompareTo(s2.Name); }); + + SubmarineInfo prevSub = null; + + foreach (SubmarineInfo sub in sortedSubs) { + if (prevSub == null || prevSub.Type != sub.Type) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), subList.Content.RectTransform) { MinSize = new Point(0, 35) }, + TextManager.Get("SubmarineType." + sub.Type), font: GUI.LargeFont, textAlignment: Alignment.Center, style: "ListBoxElement") + { + CanBeFocused = false + }; + prevSub = sub; + } + GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), subList.Content.RectTransform) { MinSize = new Point(0, 30) }, ToolBox.LimitString(sub.Name, GUI.Font, subList.Rect.Width - 80)) { @@ -1861,6 +2325,15 @@ namespace Barotrauma ToolTip = textBlock.RawToolTip }; } + 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) + { + TextColor = textBlock.TextColor * 0.8f, + ToolTip = textBlock.RawToolTip + }; + } } var deleteButton = new GUIButton(new RectTransform(Vector2.One, deleteButtonHolder.RectTransform, Anchor.TopCenter), @@ -1914,7 +2387,7 @@ namespace Barotrauma { foreach (GUIComponent child in subList.Content.Children) { - if (!(child.UserData is SubmarineInfo sub)) { return; } + if (!(child.UserData is SubmarineInfo sub)) { continue; } child.Visible = string.IsNullOrEmpty(filter) || sub.Name.ToLower().Contains(filter.ToLower()); } } @@ -2474,6 +2947,8 @@ namespace Barotrauma private void CloseItem() { if (dummyCharacter == null) { return; } + //nothing to close -> return + if (DraggedItemPrefab == null && dummyCharacter?.SelectedConstruction == null && OpenedItem == null) { return; } DraggedItemPrefab = null; dummyCharacter.SelectedConstruction = null; OpenedItem?.Drop(dummyCharacter); @@ -3694,6 +4169,10 @@ namespace Barotrauma private void CreateImage(int width, int height, System.IO.Stream stream) { MapEntity.SelectedList.Clear(); + foreach (MapEntity me in MapEntity.mapEntityList) + { + me.IsHighlighted = false; + } var prevScissorRect = GameMain.Instance.GraphicsDevice.ScissorRectangle; @@ -3707,24 +4186,14 @@ namespace Barotrauma Matrix.CreateScale(new Vector3(scale, scale, 1)) * viewMatrix; - /*Sprite backgroundSprite = LevelGenerationParams.LevelParams.Find(l => l.BackgroundTopSprite != null).BackgroundTopSprite;*/ - using (RenderTarget2D rt = new RenderTarget2D( GameMain.Instance.GraphicsDevice, width, height, false, SurfaceFormat.Color, DepthFormat.None)) using (SpriteBatch spriteBatch = new SpriteBatch(GameMain.Instance.GraphicsDevice)) { GameMain.Instance.GraphicsDevice.SetRenderTarget(rt); - GameMain.Instance.GraphicsDevice.Clear(new Color(8, 13, 19)); - /*if (backgroundSprite != null) - { - spriteBatch.Begin(); - backgroundSprite.DrawTiled(spriteBatch, Vector2.Zero, new Vector2(width, height), color: new Color(0.025f, 0.075f, 0.131f, 1.0f)); - spriteBatch.End(); - }*/ - spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, null, null, null, null, transform); Submarine.Draw(spriteBatch); Submarine.DrawFront(spriteBatch); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index 31c7fbd53..8a2082e58 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -292,7 +292,7 @@ namespace Barotrauma public void AddCustomContent(GUIComponent component, int childIndex) { component.RectTransform.Parent = layoutGroup.RectTransform; - component.RectTransform.RepositionChildInHierarchy(childIndex); + component.RectTransform.RepositionChildInHierarchy(Math.Min(childIndex, layoutGroup.CountChildren - 1)); layoutGroup.Recalculate(); Recalculate(); } @@ -309,7 +309,21 @@ namespace Barotrauma string propertyTag = (entity.GetType().Name + "." + property.PropertyInfo.Name).ToLowerInvariant(); string fallbackTag = property.PropertyInfo.Name.ToLowerInvariant(); - string displayName = TextManager.Get($"sp.{propertyTag}.name", true, $"sp.{fallbackTag}.name"); + string displayName = + TextManager.Get($"{propertyTag}", true, useEnglishAsFallBack: false) ?? + TextManager.Get($"sp.{propertyTag}.name", true, useEnglishAsFallBack: false); + if (string.IsNullOrEmpty(displayName)) + { + Editable editable = property.GetAttribute(); + if (editable != null && !string.IsNullOrEmpty(editable.FallBackTextTag)) + { + displayName = TextManager.Get(editable.FallBackTextTag, true); + } + else + { + displayName = TextManager.Get(fallbackTag, true); + } + } if (displayName == null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs index 140867f5c..01a06e4ec 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs @@ -16,67 +16,7 @@ namespace Barotrauma.Sounds private static List playbackAmplitude; private const int AMPLITUDE_SAMPLE_COUNT = 4410; //100ms in a 44100hz file - public OggSound(SoundManager owner, string filename, bool stream, XElement xElement) : base(owner, filename, stream, true, xElement) - { - filename = filename.CleanUpPath(); - if (!ToolBox.IsProperFilenameCase(filename)) - { - DebugConsole.ThrowError("Sound file \"" + filename + "\" has incorrect case!"); - } - - reader = new VorbisReader(filename); - - ALFormat = reader.Channels == 1 ? Al.FormatMono16 : Al.FormatStereo16; - SampleRate = reader.SampleRate; - - if (!stream) - { - int bufferSize = (int)reader.TotalSamples * reader.Channels; - - float[] floatBuffer = new float[bufferSize]; - short[] shortBuffer = new short[bufferSize]; - - int readSamples = reader.ReadSamples(floatBuffer, 0, bufferSize); - - playbackAmplitude = new List(); - for (int i=0;i= bufferSize) { break; } - maxAmplitude = Math.Max(maxAmplitude, Math.Abs(floatBuffer[j])); - } - playbackAmplitude.Add(maxAmplitude); - } - - CastBuffer(floatBuffer, shortBuffer, readSamples); - - Al.BufferData(ALBuffer, ALFormat, shortBuffer, - readSamples * sizeof(short), SampleRate); - - int alError = Al.GetError(); - if (alError != Al.NoError) - { - throw new Exception("Failed to set buffer data for non-streamed audio! "+Al.GetErrorString(alError)); - } - - MuffleBuffer(floatBuffer, SampleRate, reader.Channels); - - CastBuffer(floatBuffer, shortBuffer, readSamples); - - Al.BufferData(ALMuffledBuffer, ALFormat, shortBuffer, - readSamples * sizeof(short), SampleRate); - - alError = Al.GetError(); - if (alError != Al.NoError) - { - throw new Exception("Failed to set buffer data for non-streamed audio! " + Al.GetErrorString(alError)); - } - - reader.Dispose(); - } - } + public OggSound(SoundManager owner, string filename, bool stream, XElement xElement) : base(owner, filename, stream, true, xElement) { } public override float GetAmplitudeAtPlaybackPos(int playbackPos) { @@ -114,6 +54,64 @@ namespace Barotrauma.Sounds filter.Process(buffer); } + public override void InitializeALBuffers() + { + base.InitializeALBuffers(); + + reader ??= new VorbisReader(Filename); + + ALFormat = reader.Channels == 1 ? Al.FormatMono16 : Al.FormatStereo16; + SampleRate = reader.SampleRate; + + if (!Stream) + { + int bufferSize = (int)reader.TotalSamples * reader.Channels; + + float[] floatBuffer = new float[bufferSize]; + short[] shortBuffer = new short[bufferSize]; + + int readSamples = reader.ReadSamples(floatBuffer, 0, bufferSize); + + playbackAmplitude = new List(); + for (int i = 0; i < bufferSize; i += reader.Channels * AMPLITUDE_SAMPLE_COUNT) + { + float maxAmplitude = 0.0f; + for (int j = i; j < i + reader.Channels * AMPLITUDE_SAMPLE_COUNT; j++) + { + if (j >= bufferSize) { break; } + maxAmplitude = Math.Max(maxAmplitude, Math.Abs(floatBuffer[j])); + } + playbackAmplitude.Add(maxAmplitude); + } + + CastBuffer(floatBuffer, shortBuffer, readSamples); + + Al.BufferData(ALBuffer, ALFormat, shortBuffer, + readSamples * sizeof(short), SampleRate); + + int alError = Al.GetError(); + if (alError != Al.NoError) + { + throw new Exception("Failed to set buffer data for non-streamed audio! " + Al.GetErrorString(alError)); + } + + MuffleBuffer(floatBuffer, SampleRate, reader.Channels); + + CastBuffer(floatBuffer, shortBuffer, readSamples); + + Al.BufferData(ALMuffledBuffer, ALFormat, shortBuffer, + readSamples * sizeof(short), SampleRate); + + alError = Al.GetError(); + if (alError != Al.NoError) + { + throw new Exception("Failed to set buffer data for non-streamed audio! " + Al.GetErrorString(alError)); + } + + reader.Dispose(); reader = null; + } + } + public override void Dispose() { if (Stream) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/OpenAL/Alc.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OpenAL/Alc.cs index 195731d78..785c510b4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/OpenAL/Alc.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OpenAL/Alc.cs @@ -43,11 +43,42 @@ namespace OpenAL #elif LINUX public const string OpenAlDll = "libopenal.so.1"; #elif WINDOWS -#if X86 - public const string OpenAlDll = "soft_oal_x86.dll"; -#elif X64 public const string OpenAlDll = "soft_oal_x64.dll"; #endif + + public delegate void ErrorReasonCallback(string str); + +#if WINDOWS + [DllImport(OpenAlDll, CallingConvention = CallingConvention.Cdecl, EntryPoint = "alcSetErrorReasonCallback")] + private static extern void SetErrorReasonCallback(IntPtr callback); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void ErrorReasonCallbackInternal(IntPtr cstr); + + private static ErrorReasonCallbackInternal CurrentErrorReasonCallback; + private static IntPtr CurrentErrorReasonCallbackPtr; + + public static void SetErrorReasonCallback(ErrorReasonCallback callback) + { + CurrentErrorReasonCallback = (IntPtr cstr) => + { + int strLen = 0; + while (Marshal.ReadByte(cstr, strLen) != '\0') { strLen++; } + byte[] bytes = new byte[strLen]; + Marshal.Copy(cstr, bytes, 0, strLen); + string csStr = Encoding.UTF8.GetString(bytes); + + callback?.Invoke(csStr); + }; + + CurrentErrorReasonCallbackPtr = Marshal.GetFunctionPointerForDelegate(CurrentErrorReasonCallback); + SetErrorReasonCallback(CurrentErrorReasonCallbackPtr); + } +#else + public static void SetErrorReasonCallback(ErrorReasonCallback callback) + { + //FIXME: not implemented on macOS and Linux + } #endif #region Enum @@ -77,10 +108,11 @@ namespace OpenAL public const int CaptureDeviceSpecifier = 0x310; public const int CaptureDefaultDeviceSpecifier = 0x311; public const int EnumCaptureSamples = 0x312; + public const int EnumConnected = 0x313; - #endregion +#endregion - #region Context Management Functions +#region Context Management Functions [DllImport(OpenAlDll, CallingConvention = CallingConvention.Cdecl, EntryPoint = "alcCreateContext")] private static extern IntPtr _CreateContext(IntPtr device, IntPtr attrlist); @@ -112,7 +144,22 @@ namespace OpenAL public static extern IntPtr GetContextsDevice(IntPtr context); [DllImport(OpenAlDll, CallingConvention = CallingConvention.Cdecl, EntryPoint = "alcOpenDevice")] - public static extern IntPtr OpenDevice(string deviceName); + private static extern IntPtr OpenDevice(IntPtr deviceName); + + public static IntPtr OpenDevice(string deviceName) + { + if (deviceName == null) + { + return OpenDevice(IntPtr.Zero); + } + + byte[] devicenameBytes = Encoding.UTF8.GetBytes(deviceName + "\0"); + GCHandle devicenameHandle = GCHandle.Alloc(devicenameBytes, GCHandleType.Pinned); + IntPtr retVal = OpenDevice(devicenameHandle.AddrOfPinnedObject()); + devicenameHandle.Free(); + + return retVal; + } [DllImport(OpenAlDll, CallingConvention = CallingConvention.Cdecl, EntryPoint = "alcCloseDevice")] public static extern bool CloseDevice(IntPtr device); @@ -150,9 +197,9 @@ namespace OpenAL [DllImport(OpenAlDll, CallingConvention = CallingConvention.Cdecl, EntryPoint = "alcGetEnumValue")] public static extern int GetEnumValue(IntPtr device, string enumname); - #endregion +#endregion - #region Query Functions +#region Query Functions [DllImport(OpenAlDll, CallingConvention = CallingConvention.Cdecl, EntryPoint = "alcGetString")] private static extern IntPtr _GetString(IntPtr device, int param); @@ -171,6 +218,7 @@ namespace OpenAL { List retVal = new List(); IntPtr strPtr = _GetString(device, param); + if (strPtr == IntPtr.Zero) { return retVal; } int strStart = 0; int strEnd = 0; byte currChar = Marshal.ReadByte(strPtr, strEnd); @@ -208,9 +256,9 @@ namespace OpenAL data = dataArr[0]; } - #endregion +#endregion - #region Capture Functions +#region Capture Functions [DllImport(OpenAlDll, CallingConvention = CallingConvention.Cdecl, EntryPoint = "alcCaptureOpenDevice")] private static extern IntPtr CaptureOpenDevice(IntPtr devicename, uint frequency, int format, int buffersize); @@ -236,6 +284,6 @@ namespace OpenAL [DllImport(OpenAlDll, CallingConvention = CallingConvention.Cdecl, EntryPoint = "alcCaptureSamples")] public static extern void CaptureSamples(IntPtr device, IntPtr buffer, int samples); - #endregion +#endregion } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs index 26080fec0..72f509c2e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs @@ -98,37 +98,8 @@ namespace Barotrauma.Sounds BaseGain = 1.0f; BaseNear = 100.0f; BaseFar = 200.0f; - - if (!stream) - { - Al.GenBuffer(out alBuffer); - int alError = Al.GetError(); - if (alError != Al.NoError) - { - throw new Exception("Failed to create OpenAL buffer for non-streamed sound: " + Al.GetErrorString(alError)); - } - if (!Al.IsBuffer(alBuffer)) - { - throw new Exception("Generated OpenAL buffer is invalid!"); - } - - Al.GenBuffer(out alMuffledBuffer); - alError = Al.GetError(); - if (alError != Al.NoError) - { - throw new Exception("Failed to create OpenAL buffer for non-streamed sound: " + Al.GetErrorString(alError)); - } - - if (!Al.IsBuffer(alMuffledBuffer)) - { - throw new Exception("Generated OpenAL buffer is invalid!"); - } - } - else - { - alBuffer = 0; - } + InitializeALBuffers(); } public override string ToString() @@ -191,10 +162,42 @@ namespace Barotrauma.Sounds public abstract float GetAmplitudeAtPlaybackPos(int playbackPos); - public virtual void Dispose() + public virtual void InitializeALBuffers() { - if (disposed) { return; } + if (!Stream) + { + Al.GenBuffer(out alBuffer); + int alError = Al.GetError(); + if (alError != Al.NoError) + { + throw new Exception("Failed to create OpenAL buffer for non-streamed sound: " + Al.GetErrorString(alError)); + } + if (!Al.IsBuffer(alBuffer)) + { + throw new Exception("Generated OpenAL buffer is invalid!"); + } + + Al.GenBuffer(out alMuffledBuffer); + alError = Al.GetError(); + if (alError != Al.NoError) + { + throw new Exception("Failed to create OpenAL buffer for non-streamed sound: " + Al.GetErrorString(alError)); + } + + if (!Al.IsBuffer(alMuffledBuffer)) + { + throw new Exception("Generated OpenAL buffer is invalid!"); + } + } + else + { + alBuffer = 0; + } + } + + public virtual void DeleteALBuffers() + { Owner.KillChannels(this); if (alBuffer != 0) { @@ -202,7 +205,7 @@ namespace Barotrauma.Sounds { throw new Exception("Buffer to delete is invalid!"); } - + Al.DeleteBuffer(alBuffer); alBuffer = 0; int alError = Al.GetError(); @@ -226,6 +229,13 @@ namespace Barotrauma.Sounds throw new Exception("Failed to delete OpenAL buffer for non-streamed sound: " + Al.GetErrorString(alError)); } } + } + + public virtual void Dispose() + { + if (disposed) { return; } + + DeleteALBuffers(); Owner.RemoveSound(this); disposed = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs index 9c64fa486..568ab0619 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs @@ -65,6 +65,7 @@ namespace Barotrauma.Sounds public void Dispose() { + if (ALSources == null) { return; } for (int i = 0; i < ALSources.Length; i++) { Al.DeleteSource(ALSources[i]); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs index 3c669361d..255a8e072 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs @@ -19,8 +19,8 @@ namespace Barotrauma.Sounds private set; } - private readonly IntPtr alcDevice; - private readonly IntPtr alcContext; + private IntPtr alcDevice; + private IntPtr alcContext; public enum SourcePoolIndex { @@ -33,6 +33,10 @@ namespace Barotrauma.Sounds private readonly SoundChannel[][] playingChannels = new SoundChannel[2][]; private readonly object threadDeathMutex = new object(); + public bool CanDetectDisconnect { get; private set; } + + public bool Disconnected { get; private set; } + private Thread streamingThread; private Vector3 listenerPosition; @@ -197,18 +201,55 @@ namespace Barotrauma.Sounds streamingThread = null; categoryModifiers = null; - int alcError = Alc.NoError; + sourcePools = new SoundSourcePool[2]; + playingChannels[(int)SourcePoolIndex.Default] = new SoundChannel[SOURCE_COUNT]; + playingChannels[(int)SourcePoolIndex.Voice] = new SoundChannel[16]; + + string deviceName = GameMain.Config.AudioOutputDevice; + + if (string.IsNullOrEmpty(deviceName)) + { + deviceName = Alc.GetString((IntPtr)null, Alc.DefaultDeviceSpecifier); + } + +#if (!OSX) + var audioDeviceNames = Alc.GetStringList((IntPtr)null, Alc.AllDevicesSpecifier); + if (audioDeviceNames.Any() && !audioDeviceNames.Any(n => n.Equals(deviceName, StringComparison.OrdinalIgnoreCase))) + { + deviceName = audioDeviceNames[0]; + } +#endif + GameMain.Config.AudioOutputDevice = deviceName; + + InitializeAlcDevice(deviceName); + + ListenerPosition = Vector3.Zero; + ListenerTargetVector = new Vector3(0.0f, 0.0f, 1.0f); + ListenerUpVector = new Vector3(0.0f, -1.0f, 0.0f); + + CompressionDynamicRangeGain = 1.0f; + } + + public bool InitializeAlcDevice(string deviceName) + { + ReleaseResources(true); - string deviceName = Alc.GetString(IntPtr.Zero, Alc.DefaultDeviceSpecifier); DebugConsole.NewMessage($"Attempting to open ALC device \"{deviceName}\""); alcDevice = IntPtr.Zero; + int alcError = Al.NoError; for (int i = 0; i < 3; i++) { alcDevice = Alc.OpenDevice(deviceName); if (alcDevice == IntPtr.Zero) { - DebugConsole.NewMessage($"ALC device initialization attempt #{i + 1} failed: device is null"); + alcError = Alc.GetError(IntPtr.Zero); + DebugConsole.NewMessage($"ALC device initialization attempt #{i + 1} failed: device is null (error code {Alc.GetErrorString(alcError)})"); + if (!string.IsNullOrEmpty(deviceName)) + { + deviceName = null; + DebugConsole.NewMessage($"Switching to default device..."); + } } else { @@ -223,29 +264,44 @@ namespace Barotrauma.Sounds } alcDevice = IntPtr.Zero; } + else + { + break; + } } } if (alcDevice == IntPtr.Zero) { DebugConsole.ThrowError("ALC device creation failed too many times!"); Disabled = true; - return; + return false; } + CanDetectDisconnect = Alc.IsExtensionPresent(alcDevice, "ALC_EXT_disconnect"); + alcError = Alc.GetError(alcDevice); + if (alcError != Alc.NoError) + { + DebugConsole.ThrowError("Error determining if disconnect can be detected: " + alcError.ToString() + ". Disabling audio playback..."); + Disabled = true; + return false; + } + + Disconnected = false; + int[] alcContextAttrs = new int[] { }; alcContext = Alc.CreateContext(alcDevice, alcContextAttrs); if (alcContext == null) { DebugConsole.ThrowError("Failed to create an ALC context! (error code: " + Alc.GetError(alcDevice).ToString() + "). Disabling audio playback..."); Disabled = true; - return; + return false; } if (!Alc.MakeContextCurrent(alcContext)) { DebugConsole.ThrowError("Failed to assign the current ALC context! (error code: " + Alc.GetError(alcDevice).ToString() + "). Disabling audio playback..."); Disabled = true; - return; + return false; } alcError = Alc.GetError(alcDevice); @@ -253,31 +309,27 @@ namespace Barotrauma.Sounds { DebugConsole.ThrowError("Error after assigning ALC context: " + Alc.GetErrorString(alcError) + ". Disabling audio playback..."); Disabled = true; - return; + return false; } - sourcePools = new SoundSourcePool[2]; - sourcePools[(int)SourcePoolIndex.Default] = new SoundSourcePool(SOURCE_COUNT); - playingChannels[(int)SourcePoolIndex.Default] = new SoundChannel[SOURCE_COUNT]; - - sourcePools[(int)SourcePoolIndex.Voice] = new SoundSourcePool(16); - playingChannels[(int)SourcePoolIndex.Voice] = new SoundChannel[16]; - Al.DistanceModel(Al.LinearDistanceClamped); - + int alError = Al.GetError(); if (alError != Al.NoError) { DebugConsole.ThrowError("Error setting distance model: " + Al.GetErrorString(alError) + ". Disabling audio playback..."); Disabled = true; - return; + return false; } - ListenerPosition = Vector3.Zero; - ListenerTargetVector = new Vector3(0.0f, 0.0f, 1.0f); - ListenerUpVector = new Vector3(0.0f, -1.0f, 0.0f); + sourcePools[(int)SourcePoolIndex.Default] = new SoundSourcePool(SOURCE_COUNT); + sourcePools[(int)SourcePoolIndex.Voice] = new SoundSourcePool(16); - CompressionDynamicRangeGain = 1.0f; + ReloadSounds(); + + Disabled = false; + + return true; } public Sound LoadSound(string filename, bool stream = false) @@ -551,7 +603,25 @@ namespace Barotrauma.Sounds public void Update() { - if (Disabled) { return; } + if (Disconnected || Disabled) { return; } + + if (CanDetectDisconnect) + { + Alc.GetInteger(alcDevice, Alc.EnumConnected, out int isConnected); + int alcError = Alc.GetError(alcDevice); + if (alcError != Alc.NoError) + { + throw new Exception("Failed to determine if device is connected: " + alcError.ToString()); + } + + if (isConnected == 0) + { + DebugConsole.ThrowError("Playback device has been disconnected. You can select another available device in the settings."); + GameMain.Config.AudioOutputDevice = ""; + Disconnected = true; + return; + } + } if (GameMain.Client != null && GameMain.Config.VoipAttenuationEnabled) { @@ -681,10 +751,16 @@ namespace Barotrauma.Sounds } } - public void Dispose() + private void ReloadSounds() { - if (Disabled) { return; } + for (int i = loadedSounds.Count - 1; i >= 0; i--) + { + loadedSounds[i].InitializeALBuffers(); + } + } + private void ReleaseResources(bool keepSounds) + { for (int i = 0; i < playingChannels.Length; i++) { lock (playingChannels[i]) @@ -699,10 +775,24 @@ namespace Barotrauma.Sounds streamingThread?.Join(); for (int i = loadedSounds.Count - 1; i >= 0; i--) { - loadedSounds[i].Dispose(); + if (keepSounds) + { + loadedSounds[i].DeleteALBuffers(); + } + else + { + loadedSounds[i].Dispose(); + } } sourcePools[(int)SourcePoolIndex.Default]?.Dispose(); sourcePools[(int)SourcePoolIndex.Voice]?.Dispose(); + } + + public void Dispose() + { + if (Disabled) { return; } + + ReleaseResources(false); if (!Alc.MakeContextCurrent(IntPtr.Zero)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index ba6395a32..3f6993393 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -809,7 +809,8 @@ namespace Barotrauma Screen.Selected == GameMain.ParticleEditorScreen || Screen.Selected == GameMain.SpriteEditorScreen || Screen.Selected == GameMain.SubEditorScreen || - (Screen.Selected == GameMain.GameScreen && GameMain.GameSession?.GameMode is SubTestMode)) + Screen.Selected == GameMain.EventEditorScreen || + (Screen.Selected == GameMain.GameScreen && GameMain.GameSession?.GameMode is TestGameMode)) { return "editor"; } @@ -882,7 +883,7 @@ namespace Barotrauma if (GameMain.GameSession != null) { - if (Submarine.Loaded != null && Level.Loaded != null && Submarine.MainSub.AtEndPosition) + if (Submarine.Loaded != null && Level.Loaded != null && Submarine.MainSub != null && Submarine.MainSub.AtEndPosition) { return "levelend"; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs index a64df8a01..eefd8a241 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs @@ -133,10 +133,10 @@ namespace Barotrauma }); return t; } - file = Path.GetFullPath(file); + string fullPath = Path.GetFullPath(file); foreach (Sprite s in LoadedSprites) { - if (s.FullPath == file && s.texture != null && !s.texture.IsDisposed) + if (s.FullPath == fullPath && s.texture != null && !s.texture.IsDisposed) { reusedSprite = s; return s.texture; @@ -145,7 +145,12 @@ namespace Barotrauma if (File.Exists(file)) { - ToolBox.IsProperFilenameCase(file); + if (!ToolBox.IsProperFilenameCase(file)) + { +#if DEBUG + DebugConsole.ThrowError("Texture file \"" + file + "\" has incorrect case!"); +#endif + } return TextureLoader.FromFile(file, compress); } else @@ -194,11 +199,11 @@ namespace Barotrauma } public void DrawTiled(SpriteBatch spriteBatch, Vector2 position, Vector2 targetSize, - Rectangle? rect = null, Color? color = null, Point? startOffset = null, Vector2? textureScale = null, float? depth = null) + Color? color = null, Vector2? startOffset = null, Vector2? textureScale = null, float? depth = null) { if (Texture == null) { return; } //Init optional values - Vector2 drawOffset = startOffset.HasValue ? new Vector2(startOffset.Value.X, startOffset.Value.Y) : Vector2.Zero; + Vector2 drawOffset = startOffset.HasValue ? startOffset.Value : Vector2.Zero; Vector2 scale = textureScale ?? Vector2.One; Color drawColor = color ?? Color.White; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionResult.cs b/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionResult.cs new file mode 100644 index 000000000..8e2c5e73d --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionResult.cs @@ -0,0 +1,22 @@ +using Barotrauma.Networking; +using System; + +namespace Barotrauma +{ + partial class TraitorMissionResult + { + public TraitorMissionResult(IReadMessage inc) + { + MissionIdentifier = inc.ReadString(); + EndMessage = inc.ReadString(); + Success = inc.ReadBoolean(); + byte characterCount = inc.ReadByte(); + for (int i = 0; i < characterCount; i++) + { + UInt16 characterID = inc.ReadUInt16(); + var character = Entity.FindEntityByID(characterID) as Character; + if (character != null) { Characters.Add(character); } + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Upgrades/UpgradePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Upgrades/UpgradePrefab.cs new file mode 100644 index 000000000..7f2243b45 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Upgrades/UpgradePrefab.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Barotrauma +{ + partial class UpgradePrefab + { + public readonly List DecorativeSprites = new List(); + public Sprite Sprite { get; private set; } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs index f98cdc0dc..d4f357c04 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs @@ -140,7 +140,6 @@ namespace Barotrauma split[1] = split[2]; split[2] = string.Empty; } - split[1] = split[1].Replace("\"", ""); // Replaces quotation marks around data that are added when exporting via excel xmlContent.Add($"<{split[0]}>{split[1]}"); } else if (split[0].Contains(".") && !split[0].Any(char.IsUpper)) // An empty field diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/Quad.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/Quad.cs index b373c9e7e..d14a9a885 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/Quad.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/Quad.cs @@ -27,7 +27,7 @@ namespace Barotrauma basicEffect = new BasicEffect(graphics) { TextureEnabled = true }; - GameMain.Instance.OnResolutionChanged += () => + GameMain.Instance.ResolutionChanged += () => { InitVertexData(); }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs index a921f129e..2b7b56072 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs @@ -212,6 +212,11 @@ namespace Barotrauma } } + if (((width & 0x03) != 0) || ((height & 0x03) != 0)) + { + DebugConsole.AddWarning($"Cannot compress a texture because the dimensions are not a multiple of 4 (path: {path ?? "null"}, size: {width}x{height})"); + } + Texture2D tex = null; CrossThread.RequestExecutionOnMainThread(() => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs index c04d65ed9..e7b4f6d47 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs @@ -9,6 +9,52 @@ namespace Barotrauma { public static partial class ToolBox { + /// + /// Checks if point is inside of a polygon + /// + /// + /// + /// Additional check to see if the point is within the bounding box before doing more complex math + /// + /// Note that the bounding box check can be more expensive than the vertex calculations in some cases. + /// Reference + /// + /// + public static bool PointIntersectsWithPolygon(Vector2 point, Vector2[] verts, bool checkBoundingBox = true) + { + var (x, y) = point; + + if (checkBoundingBox) + { + float minX = verts[0].X; + float maxX = verts[0].X; + float minY = verts[0].Y; + float maxY = verts[0].Y; + + foreach (var (vertX, vertY) in verts) + { + minX = Math.Min(vertX, minX); + maxX = Math.Max(vertX, maxX); + minY = Math.Min(vertY, minY); + maxY = Math.Max(vertY, maxY); + } + + if (x < minX || x > maxX || y < minY || y > maxY ) { return false; } + } + + bool isInside = false; + + for (int i = 0, j = verts.Length - 1; i < verts.Length; j = i++ ) + { + if (verts[i].Y > y != verts[j].Y > y && x < (verts[j].X - verts[i].X) * (y - verts[i].Y) / (verts[j].Y - verts[i].Y) + verts[i].X ) + { + isInside = !isInside; + } + } + + return isInside; + } + // Convert an RGB value into an HLS value. public static Vector3 RgbToHLS(this Color color) { @@ -251,5 +297,16 @@ namespace Barotrauma UInt64.TryParse(args[1], out lobbyId); } } + + public static bool VersionNewerIgnoreRevision(Version a, Version b) + { + if (b.Major > a.Major) { return true; } + if (b.Major < a.Major) { return false; } + if (b.Minor > a.Minor) { return true; } + if (b.Minor < a.Minor) { return false; } + if (b.Build > a.Build) { return true; } + if (b.Build < a.Build) { return false; } + return false; + } } } diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 4860f7350..fed3f8f6c 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.9.10.0 + 0.10.4.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma @@ -95,7 +95,7 @@ - + @@ -105,7 +105,7 @@ - + diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index ebad2035d..0e9f2494e 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.9.10.0 + 0.10.4.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma @@ -96,7 +96,7 @@ - + diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 2f9b13cd3..7d5e7d711 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.9.10.0 + 0.10.4.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/soft_oal_x64.dll b/Barotrauma/BarotraumaClient/soft_oal_x64.dll index 2013cc5e84d826a346b192c3f78c7b602c1946a8..3963d4d89416dbe973d2012fc031a178e587a4d7 100644 GIT binary patch delta 851388 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 z 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