From cf8f0de6594d0833aaece0ec37feea0f382ac75c Mon Sep 17 00:00:00 2001 From: Markus Isberg Date: Mon, 2 Oct 2023 16:43:54 +0300 Subject: [PATCH] Unstable 1.1.14.0 --- .../BarotraumaClient/ClientSource/Camera.cs | 4 +- .../ClientSource/Characters/AI/AITarget.cs | 4 +- .../Characters/AI/HumanAIController.cs | 15 +- .../Characters/Animation/Ragdoll.cs | 1 + .../ClientSource/Characters/Character.cs | 30 +- .../ClientSource/Characters/CharacterHUD.cs | 2 +- .../ClientSource/Characters/CharacterInfo.cs | 17 +- .../Characters/CharacterNetworking.cs | 37 +- .../Characters/Health/CharacterHealth.cs | 20 +- .../Health/HealingCooldownClient.cs | 2 - .../ClientSource/Characters/Limb.cs | 62 +- .../CircuitBox/CircuitBoxComponent.cs | 114 +++ .../CircuitBox/CircuitBoxConnection.cs | 127 +++ .../CircuitBox/CircuitBoxLabel.cs | 22 + .../CircuitBoxMouseDragSnapshotHandler.cs | 233 ++++++ .../ClientSource/CircuitBox/CircuitBoxNode.cs | 88 ++ .../ClientSource/CircuitBox/CircuitBoxUI.cs | 722 +++++++++++++++++ .../ClientSource/CircuitBox/CircuitBoxWire.cs | 11 + .../CircuitBox/CircuitBoxWireRenderer.cs | 271 +++++++ .../ClientSource/DebugConsole.cs | 120 ++- .../Events/EventActions/ConversationAction.cs | 19 +- .../Events/EventActions/EventLogAction.cs | 11 + .../EventActions/EventObjectiveAction.cs | 66 ++ .../Events/EventActions/MessageBoxAction.cs | 2 +- .../EventActions/TutorialSegmentAction.cs | 43 - .../ClientSource/Events/EventLog.cs | 61 ++ .../ClientSource/Events/EventManager.cs | 49 +- .../Events/Missions/MineralMission.cs | 28 +- .../ClientSource/Events/Missions/Mission.cs | 61 +- .../Events/Missions/NestMission.cs | 18 +- .../ClientSource/Fonts/ScalableFont.cs | 4 +- .../ClientSource/GUI/ChatBox.cs | 13 +- .../ClientSource/GUI/CrewManagement.cs | 4 +- .../ClientSource/GUI/FileSelection.cs | 4 +- .../BarotraumaClient/ClientSource/GUI/GUI.cs | 33 +- .../ClientSource/GUI/GUIComponent.cs | 2 +- .../ClientSource/GUI/GUIMessage.cs | 6 +- .../ClientSource/GUI/GUIMessageBox.cs | 47 ++ .../ClientSource/GUI/GUIPrefab.cs | 2 +- .../ClientSource/GUI/GUIScissorComponent.cs | 25 +- .../ClientSource/GUI/GUIStyle.cs | 14 +- .../ClientSource/GUI/GUITickBox.cs | 39 +- .../ClientSource/GUI/HUDLayoutSettings.cs | 5 +- .../ClientSource/GUI/LoadingScreen.cs | 86 +- .../ClientSource/GUI/MedicalClinicUI.cs | 2 +- .../ClientSource/GUI/RectTransform.cs | 2 + .../ClientSource/GUI/ShapeExtensions.cs | 26 +- .../ClientSource/GUI/TabMenu.cs | 188 +---- .../ClientSource/GUI/UISprite.cs | 3 + .../GameAnalytics/GameAnalyticsManager.cs | 8 +- .../BarotraumaClient/ClientSource/GameMain.cs | 69 +- .../ClientSource/GameSession/CrewManager.cs | 81 +- .../GameSession/GameModes/CampaignMode.cs | 35 +- .../GameModes/MultiPlayerCampaign.cs | 6 +- .../GameModes/SinglePlayerCampaign.cs | 15 +- .../GameModes/Tutorials/Tutorial.cs | 4 +- .../ClientSource/GameSession/GameSession.cs | 40 +- .../ClientSource/GameSession/HintManager.cs | 49 +- .../GameSession/ObjectiveManager.cs | 91 ++- .../ClientSource/GameSession/ReadyCheck.cs | 72 +- .../ClientSource/GameSession/RoundSummary.cs | 419 ++++++---- .../ClientSource/Items/CharacterInventory.cs | 102 ++- .../ClientSource/Items/Components/Door.cs | 6 +- .../Components/EntitySpawnerComponent.cs | 2 +- .../Items/Components/Holdable/Holdable.cs | 14 +- .../Items/Components/Holdable/IdCard.cs | 52 +- .../Items/Components/Holdable/RangedWeapon.cs | 3 +- .../Items/Components/Holdable/Sprayer.cs | 6 +- .../Items/Components/ItemComponent.cs | 48 +- .../Items/Components/ItemContainer.cs | 181 ++++- .../Items/Components/ItemLabel.cs | 2 +- .../ClientSource/Items/Components/Ladder.cs | 5 +- .../Items/Components/LightComponent.cs | 2 +- .../Components/Machines/Deconstructor.cs | 2 +- .../Items/Components/Machines/Engine.cs | 6 +- .../Items/Components/Machines/Fabricator.cs | 120 ++- .../Items/Components/Machines/MiniMap.cs | 46 +- .../Items/Components/Machines/Pump.cs | 2 +- .../Items/Components/Machines/Reactor.cs | 2 +- .../Items/Components/Machines/Sonar.cs | 70 +- .../Items/Components/Machines/Steering.cs | 30 +- .../ClientSource/Items/Components/Planter.cs | 2 +- .../Items/Components/Power/PowerContainer.cs | 4 +- .../Items/Components/Power/PowerTransfer.cs | 21 +- .../Items/Components/Projectile.cs | 133 +-- .../Items/Components/RemoteController.cs | 2 +- .../Items/Components/RepairTool.cs | 2 +- .../Items/Components/Repairable.cs | 16 +- .../ClientSource/Items/Components/Rope.cs | 54 +- .../Items/Components/Signal/CircuitBox.cs | 499 ++++++++++++ .../Items/Components/Signal/Connection.cs | 235 +++--- .../Components/Signal/ConnectionPanel.cs | 26 +- .../Components/Signal/CustomInterface.cs | 9 +- .../Items/Components/Signal/MotionSensor.cs | 2 +- .../Items/Components/Signal/Terminal.cs | 6 +- .../Items/Components/Signal/WifiComponent.cs | 4 +- .../Items/Components/Signal/Wire.cs | 16 +- .../ClientSource/Items/Components/Turret.cs | 105 +-- .../ClientSource/Items/DockingPort.cs | 14 +- .../ClientSource/Items/Inventory.cs | 141 ++-- .../ClientSource/Items/Item.cs | 112 ++- .../ClientSource/Items/ItemInventory.cs | 9 +- .../ClientSource/Map/Explosion.cs | 97 ++- .../Levels/LevelObjects/LevelObjectManager.cs | 4 +- .../ClientSource/Map/Lights/ConvexHull.cs | 2 +- .../ClientSource/Map/Lights/LightManager.cs | 18 +- .../ClientSource/Map/Lights/LightSource.cs | 108 ++- .../ClientSource/Map/LinkedSubmarine.cs | 2 +- .../ClientSource/Map/MapEntity.cs | 124 +-- .../ClientSource/Map/Structure.cs | 44 +- .../ClientSource/Map/Submarine.cs | 40 +- .../ClientSource/Map/SubmarinePreview.cs | 90 ++- .../Networking/FileTransfer/FileReceiver.cs | 2 +- .../ClientSource/Networking/GameClient.cs | 99 +-- .../Networking/Primitives/Peers/ClientPeer.cs | 10 +- .../Primitives/Peers/LidgrenClientPeer.cs | 15 +- .../Primitives/Peers/SteamP2PClientPeer.cs | 6 +- .../ClientSource/Networking/RespawnManager.cs | 12 +- .../Networking/ServerList/ServerInfo.cs | 11 +- .../ClientSource/Networking/ServerLog.cs | 4 +- .../ClientSource/Networking/ServerSettings.cs | 34 +- .../ClientSource/Networking/Voting.cs | 4 + .../ClientSource/Particles/Particle.cs | 10 +- .../ClientSource/Particles/ParticleEmitter.cs | 28 +- .../ClientSource/Particles/ParticleManager.cs | 30 +- .../ClientSource/Particles/ParticlePrefab.cs | 2 +- .../ClientSource/Physics/PhysicsBody.cs | 4 +- .../BarotraumaClient/ClientSource/Program.cs | 2 +- .../CampaignSetupUI/CampaignSetupUI.cs | 4 +- .../MultiPlayerCampaignSetupUI.cs | 4 +- .../SinglePlayerCampaignSetupUI.cs | 31 +- .../CharacterEditor/CharacterEditorScreen.cs | 16 +- .../Screens/EventEditor/EditorNode.cs | 66 +- ...ection.cs => EventEditorNodeConnection.cs} | 52 +- .../Screens/EventEditor/EventEditorScreen.cs | 22 +- .../ClientSource/Screens/GameScreen.cs | 4 +- .../ClientSource/Screens/LevelEditorScreen.cs | 48 ++ .../ClientSource/Screens/MainMenuScreen.cs | 13 +- .../ClientSource/Screens/ModDownloadScreen.cs | 2 +- .../ClientSource/Screens/NetLobbyScreen.cs | 291 ++++--- .../ServerListScreen/ServerListScreen.cs | 4 +- .../ClientSource/Screens/SubEditorScreen.cs | 190 +++-- .../ClientSource/Screens/TestScreen.cs | 57 +- .../Serialization/SerializableEntityEditor.cs | 11 +- .../ClientSource/Settings/SettingsMenu.cs | 10 +- .../ClientSource/Sounds/OggSound.cs | 190 +++-- .../ClientSource/Sounds/Sound.cs | 22 +- .../ClientSource/Sounds/SoundBuffer.cs | 2 +- .../ClientSource/Sounds/SoundChannel.cs | 86 +- .../ClientSource/Sounds/SoundFilters.cs | 196 ++--- .../ClientSource/Sounds/SoundManager.cs | 4 +- .../ClientSource/Sounds/SoundPlayer.cs | 38 +- .../ClientSource/Sprite/Sprite.cs | 44 + .../ClientSource/Steam/Lobby.cs | 3 +- .../ClientSource/Steam/SteamManager.cs | 2 +- .../Steam/WorkshopMenu/Mutable/PublishTab.cs | 16 +- .../ClientSource/Steam/WorkshopMenu/UiUtil.cs | 2 +- .../ClientSource/Traitors/TraitorManager.cs | 22 + .../Traitors/TraitorMissionPrefab.cs | 33 - .../Traitors/TraitorMissionResult.cs | 22 - .../ClientSource/Utils/SpreadsheetExport.cs | 3 +- .../ClientSource/Utils/TextureLoader.cs | 193 ++--- .../ClientSource/Utils/WikiImage.cs | 8 +- .../Content/Effects/wearableclip.xnb | Bin 2416 -> 2416 bytes .../Content/Effects/wearableclip_opengl.xnb | Bin 2380 -> 2380 bytes .../BarotraumaClient/LinuxClient.csproj | 3 +- Barotrauma/BarotraumaClient/MacClient.csproj | 12 +- .../BarotraumaClient/Shaders/wearableclip.fx | 100 +-- .../Shaders/wearableclip_opengl.fx | 100 +-- .../BarotraumaClient/WindowsClient.csproj | 3 +- .../BarotraumaServer/LinuxServer.csproj | 3 +- Barotrauma/BarotraumaServer/MacServer.csproj | 9 +- .../Characters/CharacterNetworking.cs | 29 +- .../CircuitBox/CircuitBoxConnection.cs | 16 + .../ServerSource/DebugConsole.cs | 93 ++- .../Events/EventActions/ConversationAction.cs | 2 +- .../Events/EventActions/EventLogAction.cs | 55 ++ .../EventActions/EventObjectiveAction.cs | 36 + .../ServerSource/Events/EventLog.cs | 22 + .../ServerSource/Events/EventManager.cs | 25 +- .../ServerSource/Events/Missions/Mission.cs | 96 +++ .../Events/Missions/NestMission.cs | 2 - .../BarotraumaServer/ServerSource/GameMain.cs | 1 + .../GameModes/CharacterCampaignData.cs | 9 +- .../GameModes/MultiPlayerCampaign.cs | 10 +- .../ServerSource/Items/CharacterInventory.cs | 12 + .../Items/Components/Holdable/Holdable.cs | 1 + .../Items/Components/Projectile.cs | 18 +- .../Items/Components/Repairable.cs | 3 +- .../Items/Components/Signal/CircuitBox.cs | 320 ++++++++ .../Components/Signal/ConnectionPanel.cs | 3 +- .../Items/Components/Signal/Terminal.cs | 12 +- .../ServerSource/Items/Inventory.cs | 63 +- .../ServerSource/Items/Item.cs | 56 +- .../ServerSource/Items/ItemEventData.cs | 19 + .../ServerSource/Items/ItemInventory.cs | 13 + .../BarotraumaServer/ServerSource/Map/Hull.cs | 1 + .../ServerSource/Networking/Client.cs | 2 - .../Networking/FileTransfer/ModSender.cs | 2 +- .../ServerSource/Networking/GameServer.cs | 115 +-- .../ServerSource/Networking/KarmaManager.cs | 42 +- .../Peers/Server/LidgrenServerPeer.cs | 3 +- .../ServerSource/Networking/RespawnManager.cs | 19 +- .../ServerSource/Networking/ServerSettings.cs | 16 +- .../ServerSource/Networking/Voting.cs | 14 + .../ServerSource/Steam/SteamManager.cs | 5 +- .../ServerSource/Traitors/Goals/Goal.cs | 64 -- .../Traitors/Goals/GoalDestroyItemsWithTag.cs | 107 --- .../Goals/GoalEntityTransformation.cs | 162 ---- .../Traitors/Goals/GoalFindItem.cs | 239 ------ .../Traitors/Goals/GoalFloodPercentOfSub.cs | 46 -- .../Traitors/Goals/GoalInjectTarget.cs | 84 -- .../Goals/GoalKeepTransformedAlive.cs | 73 -- .../Traitors/Goals/GoalKillTarget.cs | 163 ---- .../Goals/GoalReachDistanceFromSub.cs | 56 -- .../Traitors/Goals/GoalReplaceInventory.cs | 70 -- .../Traitors/Goals/GoalSabotageItems.cs | 62 -- .../Traitors/Goals/GoalUnwiring.cs | 96 --- .../Traitors/Goals/GoalWaitForTraitors.cs | 35 - .../Traitors/Goals/HumanoidGoal.cs | 17 - .../Goals/Modifiers/GoalHasDuration.cs | 62 -- .../Goals/Modifiers/GoalHasTimeLimit.cs | 50 -- .../Goals/Modifiers/GoalIsOptional.cs | 36 - .../Traitors/Goals/Modifiers/Modifier.cs | 80 -- .../ServerSource/Traitors/Objective.cs | 194 ----- .../ServerSource/Traitors/Traitor.cs | 43 - .../ServerSource/Traitors/TraitorManager.cs | 604 ++++++++++---- .../ServerSource/Traitors/TraitorMission.cs | 394 --------- .../Traitors/TraitorMissionPrefab.cs | 664 --------------- .../Traitors/TraitorMissionResult.cs | 30 - .../BarotraumaServer/WindowsServer.csproj | 3 +- .../Characters/AI/AIController.cs | 8 +- .../Characters/AI/EnemyAIController.cs | 72 +- .../Characters/AI/HumanAIController.cs | 92 ++- .../Characters/AI/IndoorsSteeringManager.cs | 28 +- .../Characters/AI/NPCConversation.cs | 2 +- .../Characters/AI/Objectives/AIObjective.cs | 42 +- .../Objectives/AIObjectiveChargeBatteries.cs | 1 + .../AI/Objectives/AIObjectiveCleanupItem.cs | 17 +- .../AI/Objectives/AIObjectiveCleanupItems.cs | 14 +- .../AI/Objectives/AIObjectiveCombat.cs | 354 ++++---- .../AI/Objectives/AIObjectiveContainItem.cs | 8 +- .../AI/Objectives/AIObjectiveDecontainItem.cs | 11 +- .../Objectives/AIObjectiveEscapeHandcuffs.cs | 2 +- .../Objectives/AIObjectiveExtinguishFire.cs | 11 +- .../Objectives/AIObjectiveFindDivingGear.cs | 43 +- .../AI/Objectives/AIObjectiveFindSafety.cs | 27 +- .../AI/Objectives/AIObjectiveFixLeak.cs | 36 +- .../AI/Objectives/AIObjectiveFixLeaks.cs | 3 +- .../AI/Objectives/AIObjectiveGetItem.cs | 50 +- .../AI/Objectives/AIObjectiveGetItems.cs | 88 +- .../AI/Objectives/AIObjectiveGoTo.cs | 31 +- .../AI/Objectives/AIObjectiveIdle.cs | 4 +- .../AI/Objectives/AIObjectiveLoadItem.cs | 7 +- .../AI/Objectives/AIObjectiveLoop.cs | 59 +- .../AI/Objectives/AIObjectiveManager.cs | 2 +- .../AI/Objectives/AIObjectiveOperateItem.cs | 6 +- .../AI/Objectives/AIObjectivePrepare.cs | 9 +- .../AI/Objectives/AIObjectivePumpWater.cs | 3 +- .../AI/Objectives/AIObjectiveRepairItem.cs | 84 +- .../AI/Objectives/AIObjectiveRepairItems.cs | 2 +- .../AI/Objectives/AIObjectiveRescue.cs | 133 +-- .../AI/Objectives/AIObjectiveRescueAll.cs | 100 +-- .../AI/Objectives/AIObjectiveReturn.cs | 11 +- .../SharedSource/Characters/AI/Order.cs | 7 + .../SharedSource/Characters/AI/PetBehavior.cs | 97 ++- .../ShipIssueWorkerOperateWeapons.cs | 2 +- .../Characters/AI/ShipCommandManager.cs | 6 +- .../Characters/AI/Wreck/WreckAI.cs | 9 +- .../Characters/AI/Wreck/WreckAIConfig.cs | 2 +- .../Characters/Animation/AnimController.cs | 80 +- .../Animation/HumanoidAnimController.cs | 29 +- .../Characters/Animation/Ragdoll.cs | 53 +- .../SharedSource/Characters/Attack.cs | 23 +- .../SharedSource/Characters/Character.cs | 411 ++++++---- .../Characters/CharacterEventData.cs | 10 +- .../SharedSource/Characters/CharacterInfo.cs | 32 +- .../Characters/CharacterPrefab.cs | 2 + .../Health/Afflictions/AfflictionPrefab.cs | 56 +- .../Characters/Health/CharacterHealth.cs | 34 +- .../SharedSource/Characters/Jobs/Job.cs | 10 +- .../SharedSource/Characters/Limb.cs | 93 ++- .../Characters/Params/CharacterParams.cs | 23 +- .../Characters/Params/EditableParams.cs | 13 +- .../Params/Ragdoll/RagdollParams.cs | 22 +- .../AbilityConditionAttackData.cs | 4 +- .../AbilityConditionMission.cs | 6 +- .../AbilityConditionAllyHasTalent.cs | 22 + .../AbilityConditionHasItem.cs | 8 +- .../AbilityConditionHasStatusTag.cs | 8 +- .../AbilityConditionHasTalent.cs | 3 +- .../AbilityConditionHoldingItem.cs | 4 +- .../CharacterAbilityGiveItemStatToTags.cs | 5 +- .../CharacterAbilityGivePermanentStat.cs | 4 +- .../CharacterAbilityByTheBook.cs | 4 +- .../CharacterAbilityRegenerateLoot.cs | 35 +- .../CharacterAbilityTandemFire.cs | 6 +- .../Characters/Talents/TalentTree.cs | 11 +- .../CircuitBox/CircuitBoxComponent.cs | 84 ++ .../CircuitBox/CircuitBoxConnection.cs | 121 +++ .../CircuitBox/CircuitBoxCursor.cs | 109 +++ .../CircuitBox/CircuitBoxInputOutputNode.cs | 38 + .../CircuitBox/CircuitBoxNetStructs.cs | 163 ++++ .../SharedSource/CircuitBox/CircuitBoxNode.cs | 132 +++ .../CircuitBox/CircuitBoxSelectable.cs | 43 + .../CircuitBox/CircuitBoxSizes.cs | 17 + .../SharedSource/CircuitBox/CircuitBoxWire.cs | 186 +++++ .../CircuitBox/ICircuitBoxIdentifiable.cs | 28 + .../CircuitBox/ItemSlotIndexPair.cs | 44 + .../ContentFile/ContentFile.cs | 16 + .../ContentFile/RandomEventsFile.cs | 12 +- .../ContentFile/SubmarineFile.cs | 4 + .../ContentFile/TraitorMissionsFile.cs | 26 - .../ContentPackage/ContentPackage.cs | 4 +- .../ContentPackageManager.cs | 6 + .../ContentManagement/ContentPath.cs | 24 +- .../ContentManagement/ContentXElement.cs | 3 +- .../SharedSource/CoroutineManager.cs | 188 ++--- .../SharedSource/DebugConsole.cs | 81 +- .../SharedSource/Events/ArtifactEvent.cs | 13 +- .../SharedSource/Events/Event.cs | 5 + .../EventActions/CheckAfflictionAction.cs | 12 +- .../EventActions/CheckConditionalAction.cs | 2 +- .../Events/EventActions/CheckDataAction.cs | 27 +- .../Events/EventActions/CheckItemAction.cs | 135 +++- .../CheckTraitorEventStateAction.cs | 35 + .../EventActions/CheckTraitorVoteAction.cs | 40 + .../EventActions/CheckVisibilityAction.cs | 60 ++ .../Events/EventActions/CombatAction.cs | 7 +- .../Events/EventActions/ConversationAction.cs | 42 +- .../Events/EventActions/CountTargetsAction.cs | 120 +++ .../Events/EventActions/EventAction.cs | 10 +- .../Events/EventActions/EventLogAction.cs | 94 +++ ...gmentAction.cs => EventObjectiveAction.cs} | 21 +- .../Events/EventActions/GiveExpAction.cs | 49 ++ .../Events/EventActions/GiveSkillExpAction.cs | 4 +- .../SharedSource/Events/EventActions/GoTo.cs | 2 - .../EventActions/NPCChangeTeamAction.cs | 2 +- .../Events/EventActions/NPCFollowAction.cs | 7 +- .../EventActions/NPCOperateItemAction.cs | 1 + .../Events/EventActions/NPCWaitAction.cs | 1 + .../Events/EventActions/OnRoundEndAction.cs | 42 + .../SetTraitorEventStateAction.cs | 48 ++ .../Events/EventActions/SpawnAction.cs | 166 +++- .../Events/EventActions/TagAction.cs | 169 +++- .../Events/EventActions/TriggerAction.cs | 47 +- .../EventActions/TutorialHighlightAction.cs | 8 +- .../WaitForItemFabricatedAction.cs | 75 ++ .../EventActions/WaitForItemUsedAction.cs | 153 ++++ .../SharedSource/Events/EventLog.cs | 65 ++ .../SharedSource/Events/EventManager.cs | 125 +-- .../SharedSource/Events/EventPrefab.cs | 26 +- .../SharedSource/Events/EventSet.cs | 6 +- .../Missions/AbandonedOutpostMission.cs | 6 +- .../Events/Missions/BeaconMission.cs | 7 +- .../Events/Missions/CargoMission.cs | 6 + .../Events/Missions/EndMission.cs | 15 +- .../Events/Missions/EscortMission.cs | 12 +- .../Events/Missions/MineralMission.cs | 2 +- .../SharedSource/Events/Missions/Mission.cs | 95 +-- .../Events/Missions/MissionPrefab.cs | 32 +- .../Events/Missions/MonsterMission.cs | 2 +- .../Events/Missions/NestMission.cs | 49 +- .../Events/Missions/PirateMission.cs | 2 +- .../Events/Missions/SalvageMission.cs | 22 +- .../Events/Missions/ScanMission.cs | 36 +- .../SharedSource/Events/MonsterEvent.cs | 99 ++- .../SharedSource/Events/ScriptedEvent.cs | 177 +++- .../Extensions/ColorExtensions.cs | 30 + .../Extensions/IEnumerableExtensions.cs | 22 +- .../Extensions/StringExtensions.cs | 4 +- .../GameAnalytics/GameAnalyticsConsent.cs | 198 +++-- .../GameSession/AutoItemPlacer.cs | 19 +- .../SharedSource/GameSession/CargoManager.cs | 12 +- .../SharedSource/GameSession/CrewManager.cs | 9 +- .../SharedSource/GameSession/Data/Factions.cs | 3 + .../GameSession/GameModes/CampaignMode.cs | 36 +- .../GameModes/MultiPlayerCampaign.cs | 3 + .../SharedSource/GameSession/GameSession.cs | 74 +- .../SharedSource/Items/CharacterInventory.cs | 23 +- .../Items/Components/DockingPort.cs | 4 +- .../SharedSource/Items/Components/Door.cs | 23 +- .../Items/Components/ElectricalDischarger.cs | 87 +- .../SharedSource/Items/Components/Growable.cs | 8 +- .../Items/Components/Holdable/Holdable.cs | 57 +- .../Items/Components/Holdable/IdCard.cs | 18 +- .../Components/Holdable/LevelResource.cs | 2 +- .../Items/Components/Holdable/MeleeWeapon.cs | 9 +- .../Items/Components/Holdable/Pickable.cs | 48 +- .../Items/Components/Holdable/RepairTool.cs | 59 +- .../Items/Components/ItemComponent.cs | 48 +- .../Items/Components/ItemContainer.cs | 152 +++- .../Items/Components/Machines/Controller.cs | 73 +- .../Components/Machines/Deconstructor.cs | 2 +- .../Items/Components/Machines/Engine.cs | 12 +- .../Items/Components/Machines/Fabricator.cs | 187 +++-- .../Items/Components/Machines/MiniMap.cs | 2 +- .../Items/Components/Machines/Pump.cs | 2 +- .../Items/Components/Machines/Reactor.cs | 11 +- .../Items/Components/Machines/Steering.cs | 6 +- .../Items/Components/Power/PowerContainer.cs | 39 +- .../Items/Components/Power/PowerTransfer.cs | 2 +- .../Items/Components/Power/Powered.cs | 28 +- .../Items/Components/Projectile.cs | 69 +- .../Items/Components/RemoteController.cs | 3 +- .../Items/Components/Repairable.cs | 58 +- .../SharedSource/Items/Components/Rope.cs | 37 +- .../BooleanOperatorComponent.cs | 2 +- .../Items/Components/Signal/CircuitBox.cs | 684 ++++++++++++++++ .../Items/Components/Signal/Connection.cs | 54 +- .../Components/Signal/ConnectionPanel.cs | 18 +- .../Components/Signal/CustomInterface.cs | 7 +- .../Components/Signal/EqualsComponent.cs | 2 +- .../Items/Components/Signal/LightComponent.cs | 16 + .../Items/Components/Signal/MotionSensor.cs | 75 +- .../Components/Signal/RegExFindComponent.cs | 13 +- .../Items/Components/Signal/RelayComponent.cs | 4 +- .../Items/Components/Signal/Terminal.cs | 24 +- .../Items/Components/Signal/WaterDetector.cs | 7 +- .../Items/Components/Signal/WifiComponent.cs | 35 +- .../Items/Components/Signal/Wire.cs | 36 +- .../SharedSource/Items/Components/Turret.cs | 169 ++-- .../SharedSource/Items/Components/Wearable.cs | 59 +- .../SharedSource/Items/Inventory.cs | 198 +++-- .../SharedSource/Items/Item.cs | 403 ++++++++-- .../SharedSource/Items/ItemEventData.cs | 22 +- .../SharedSource/Items/ItemInventory.cs | 16 +- .../SharedSource/Items/ItemPrefab.cs | 131 ++- .../SharedSource/Items/RelatedItem.cs | 41 +- .../SharedSource/Items/StartItemSet.cs | 6 +- .../SharedSource/Map/Explosion.cs | 67 +- .../BarotraumaShared/SharedSource/Map/Gap.cs | 8 +- .../BarotraumaShared/SharedSource/Map/Hull.cs | 74 +- .../SharedSource/Map/Levels/Level.cs | 228 ++++-- .../SharedSource/Map/Levels/LevelData.cs | 2 +- .../Map/Levels/LevelGenerationParams.cs | 12 +- .../Levels/LevelObjects/LevelObjectManager.cs | 19 +- .../Levels/LevelObjects/LevelObjectPrefab.cs | 7 + .../Map/Levels/Ruins/RuinGenerationParams.cs | 3 + .../Map/Levels/Ruins/RuinGenerator.cs | 2 +- .../SharedSource/Map/LinkedSubmarine.cs | 3 +- .../SharedSource/Map/Map/Location.cs | 9 +- .../SharedSource/Map/Map/LocationType.cs | 3 + .../SharedSource/Map/Map/Map.cs | 55 +- .../SharedSource/Map/Map/Radiation.cs | 16 +- .../SharedSource/Map/MapEntity.cs | 96 ++- .../SharedSource/Map/MapEntityPrefab.cs | 24 +- .../Map/Outposts/OutpostGenerationParams.cs | 6 + .../Map/Outposts/OutpostGenerator.cs | 194 +++-- .../SharedSource/Map/Structure.cs | 60 +- .../SharedSource/Map/Submarine.cs | 192 +++-- .../SharedSource/Map/SubmarineBody.cs | 26 +- .../SharedSource/Map/WayPoint.cs | 2 +- .../SharedSource/Networking/ChatMessage.cs | 8 +- .../SharedSource/Networking/Client.cs | 8 +- .../SharedSource/Networking/EntitySpawner.cs | 93 ++- .../SharedSource/Networking/NetworkMember.cs | 15 +- .../Primitives/NetworkExtensions.cs | 2 +- .../SharedSource/Networking/RespawnManager.cs | 5 +- .../SharedSource/Networking/ServerLog.cs | 3 + .../SharedSource/Networking/ServerSettings.cs | 73 +- .../SharedSource/Networking/Voting.cs | 8 +- .../SharedSource/Physics/PhysicsBody.cs | 18 +- .../SharedSource/Screens/GameScreen.cs | 17 +- .../SharedSource/Screens/NetLobbyScreen.cs | 43 +- .../Serialization/SerializableProperty.cs | 60 +- .../Serialization/XMLExtensions.cs | 36 +- .../SharedSource/Settings/GameSettings.cs | 8 +- .../SharedSource/Sprite/ConditionalSprite.cs | 3 +- .../SharedSource/Sprite/Sprite.cs | 54 +- .../StatusEffects/PropertyConditional.cs | 746 ++++++++++------- .../StatusEffects/StatusEffect.cs | 600 ++++++++------ .../SharedSource/Steam/AuthTicket.cs | 63 +- .../SharedSource/Steam/Workshop.cs | 11 +- .../SharedSource/SteamAchievementManager.cs | 2 +- .../BarotraumaShared/SharedSource/Tags.cs | 102 +++ .../SharedSource/Text/TextManager.cs | 34 +- .../SharedSource/Traitors/TraitorEvent.cs | 126 +++ .../Traitors/TraitorEventPrefab.cs | 350 ++++++++ .../SharedSource/Traitors/TraitorManager.cs | 49 ++ .../Traitors/TraitorMissionResult.cs | 15 - .../SharedSource/Utils/CrossThread.cs | 2 +- .../SharedSource/Utils/Md5Hash.cs | 11 + .../SharedSource/Utils/Option/Option.cs | 13 + .../SharedSource/Utils/SaveUtil.cs | 318 ++++---- .../SharedSource/Utils/ToolBox.cs | 71 +- Barotrauma/BarotraumaShared/changelog.txt | 289 ++++++- Barotrauma/BarotraumaShared/hintmanager.xml | 15 +- .../BarotraumaShared/libsteam_api.dylib | Bin 446352 -> 0 bytes .../BarotraumaShared/libsteam_api64.dylib | Bin 446352 -> 0 bytes Barotrauma/BarotraumaShared/libsteam_api64.so | Bin 398662 -> 0 bytes .../Callbacks/CallResult.cs | 5 +- .../Classes/AuthTicketForWebApi.cs | 38 + .../Facepunch.Steamworks.Posix.csproj | 11 +- .../Facepunch.Steamworks.Win64.csproj | 16 +- .../Generated/CustomEnums.cs | 52 +- .../Generated/Interfaces/ISteamAppList.cs | 8 +- .../Generated/Interfaces/ISteamApps.cs | 41 +- .../Generated/Interfaces/ISteamClient.cs | 2 +- .../Generated/Interfaces/ISteamController.cs | 8 +- .../Generated/Interfaces/ISteamFriends.cs | 69 +- .../Generated/Interfaces/ISteamGameSearch.cs | 5 +- .../Generated/Interfaces/ISteamGameServer.cs | 132 ++- .../Interfaces/ISteamGameServerStats.cs | 2 +- .../Generated/Interfaces/ISteamHTMLSurface.cs | 2 +- .../Generated/Interfaces/ISteamHTTP.cs | 2 +- .../Generated/Interfaces/ISteamInput.cs | 167 +++- .../Generated/Interfaces/ISteamInventory.cs | 56 +- .../Generated/Interfaces/ISteamMatchmaking.cs | 8 +- .../ISteamMatchmakingPingResponse.cs | 2 +- .../ISteamMatchmakingPlayersResponse.cs | 2 +- .../ISteamMatchmakingRulesResponse.cs | 2 +- .../ISteamMatchmakingServerListResponse.cs | 2 +- .../Interfaces/ISteamMatchmakingServers.cs | 2 +- .../Generated/Interfaces/ISteamMusic.cs | 2 +- .../Generated/Interfaces/ISteamMusicRemote.cs | 2 +- .../Generated/Interfaces/ISteamNetworking.cs | 2 +- ...teamNetworkingConnectionCustomSignaling.cs | 41 - ...eamNetworkingCustomSignalingRecvContext.cs | 40 - .../Interfaces/ISteamNetworkingFakeUDPPort.cs | 61 ++ .../Interfaces/ISteamNetworkingMessages.cs | 96 +++ .../Interfaces/ISteamNetworkingSockets.cs | 157 +++- .../Interfaces/ISteamNetworkingUtils.cs | 167 +++- .../Interfaces/ISteamParentalSettings.cs | 2 +- .../Generated/Interfaces/ISteamParties.cs | 8 +- .../Generated/Interfaces/ISteamRemotePlay.cs | 2 +- .../Interfaces/ISteamRemoteStorage.cs | 54 +- .../Generated/Interfaces/ISteamScreenshots.cs | 2 +- .../Generated/Interfaces/ISteamTV.cs | 97 --- .../Generated/Interfaces/ISteamUGC.cs | 159 +++- .../Generated/Interfaces/ISteamUser.cs | 56 +- .../Generated/Interfaces/ISteamUserStats.cs | 41 +- .../Generated/Interfaces/ISteamUtils.cs | 90 ++- .../Generated/Interfaces/ISteamVideo.cs | 5 +- .../Generated/SteamCallbacks.cs | 375 ++++++--- .../Generated/SteamConstants.cs | 38 +- .../Generated/SteamEnums.cs | 755 +++++++++++++----- .../Generated/SteamStructFunctions.cs | 51 ++ .../Generated/SteamStructs.cs | 72 +- .../Generated/SteamTypes.cs | 192 ----- .../Networking/BroadcastBufferManager.cs | 282 +++++++ .../Networking/Connection.cs | 65 +- .../Networking/ConnectionLaneStatus.cs | 34 + .../Networking/ConnectionManager.cs | 207 ++++- .../Networking/ConnectionStatus.cs | 77 ++ .../Networking/Delegates.cs | 28 + .../Networking/ISocketManager.cs | 2 +- .../Networking/NetAddress.cs | 12 + .../Networking/NetDebugFunc.cs | 9 - .../Networking/NetKeyValue.cs | 2 +- .../Facepunch.Steamworks/Networking/NetMsg.cs | 9 +- .../Networking/SocketManager.cs | 45 +- Libraries/Facepunch.Steamworks/SteamApps.cs | 109 ++- Libraries/Facepunch.Steamworks/SteamClient.cs | 42 +- .../Facepunch.Steamworks/SteamFriends.cs | 136 +++- Libraries/Facepunch.Steamworks/SteamInput.cs | 56 +- .../Facepunch.Steamworks/SteamInventory.cs | 20 +- .../Facepunch.Steamworks/SteamMatchmaking.cs | 41 +- .../SteamMatchmakingServers.cs | 9 +- Libraries/Facepunch.Steamworks/SteamMusic.cs | 27 +- .../Facepunch.Steamworks/SteamNetworking.cs | 35 +- .../SteamNetworkingSockets.cs | 158 +++- .../SteamNetworkingUtils.cs | 186 ++++- .../Facepunch.Steamworks/SteamParental.cs | 8 +- .../Facepunch.Steamworks/SteamParties.cs | 19 +- .../Facepunch.Steamworks/SteamRemotePlay.cs | 17 +- .../SteamRemoteStorage.cs | 40 +- .../Facepunch.Steamworks/SteamScreenshots.cs | 26 +- Libraries/Facepunch.Steamworks/SteamServer.cs | 70 +- .../Facepunch.Steamworks/SteamServerStats.cs | 49 +- Libraries/Facepunch.Steamworks/SteamUgc.cs | 111 ++- Libraries/Facepunch.Steamworks/SteamUser.cs | 95 ++- .../Facepunch.Steamworks/SteamUserStats.cs | 42 +- Libraries/Facepunch.Steamworks/SteamUtils.cs | 102 ++- Libraries/Facepunch.Steamworks/SteamVideo.cs | 19 +- .../Structs/Achievement.cs | 29 +- .../Facepunch.Steamworks/Structs/AppId.cs | 5 +- .../Structs/DlcInformation.cs | 16 +- .../Structs/DownloadProgress.cs | 21 +- .../Structs/FileDetails.cs | 8 +- .../Facepunch.Steamworks/Structs/Friend.cs | 2 +- .../Facepunch.Steamworks/Structs/Image.cs | 20 +- .../Facepunch.Steamworks/Structs/Lobby.cs | 88 +- .../Structs/PartyBeacon.cs | 12 +- .../Structs/Screenshot.cs | 10 +- .../Facepunch.Steamworks/Structs/Server.cs | 2 +- .../Structs/ServerInit.cs | 13 +- .../Facepunch.Steamworks/Structs/Stat.cs | 2 +- .../Facepunch.Steamworks/Structs/SteamId.cs | 5 +- .../Structs/UgcAdditionalPreview.cs | 22 + .../Structs/UgcAdditionalPreview.cs.meta | 11 + .../Facepunch.Steamworks/Structs/UgcEditor.cs | 23 +- .../Facepunch.Steamworks/Structs/UgcItem.cs | 25 +- .../Facepunch.Steamworks/Structs/UgcQuery.cs | 2 + .../Structs/UgcResultPage.cs | 34 +- .../Facepunch.Steamworks/Utility/Helpers.cs | 39 +- .../Utility/SteamInterface.cs | 14 +- .../Facepunch.Steamworks/libsteam_api64.dylib | Bin 0 -> 610496 bytes .../Facepunch.Steamworks/libsteam_api64.so | Bin 0 -> 391056 bytes .../Facepunch.Steamworks/steam_api64.dll | Bin 262944 -> 298856 bytes .../Dynamics/Contacts/Contact.cs | 6 +- Libraries/Lidgren.Network/NetPeer.Internal.cs | 404 +++++----- .../NetPeer.LatencySimulation.cs | 40 +- Libraries/Lidgren.Network/NetPeer.cs | 21 +- .../Lidgren.Network/NetPeerConfiguration.cs | 53 +- Libraries/Lidgren.Network/NetUtility.cs | 82 +- 606 files changed, 21906 insertions(+), 11456 deletions(-) create mode 100644 Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxComponent.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxConnection.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxLabel.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxMouseDragSnapshotHandler.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxNode.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxUI.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxWire.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxWireRenderer.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/EventLogAction.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/EventObjectiveAction.cs delete mode 100644 Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialSegmentAction.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Events/EventLog.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs rename Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/{NodeConnection.cs => EventEditorNodeConnection.cs} (86%) create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorManager.cs delete mode 100644 Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionPrefab.cs delete mode 100644 Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionResult.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/CircuitBox/CircuitBoxConnection.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventLogAction.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventObjectiveAction.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Events/EventLog.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Items/CharacterInventory.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Items/ItemEventData.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Items/ItemInventory.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Goal.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalDestroyItemsWithTag.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalEntityTransformation.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFindItem.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFloodPercentOfSub.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalInjectTarget.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKeepTransformedAlive.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKillTarget.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalReachDistanceFromSub.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalReplaceInventory.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalSabotageItems.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalUnwiring.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalWaitForTraitors.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/HumanoidGoal.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalHasDuration.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalHasTimeLimit.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalIsOptional.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/Modifier.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Objective.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMissionPrefab.cs delete mode 100644 Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMissionResult.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAllyHasTalent.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxComponent.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxConnection.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxCursor.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxInputOutputNode.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNetStructs.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNode.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxSelectable.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxSizes.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxWire.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/CircuitBox/ICircuitBoxIdentifiable.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/CircuitBox/ItemSlotIndexPair.cs delete mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TraitorMissionsFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorEventStateAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorVoteAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckVisibilityAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CountTargetsAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventLogAction.cs rename Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/{TutorialSegmentAction.cs => EventObjectiveAction.cs} (62%) create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveExpAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/OnRoundEndAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetTraitorEventStateAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemFabricatedAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemUsedAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventLog.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Tags.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEvent.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEventPrefab.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorManager.cs delete mode 100644 Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorMissionResult.cs delete mode 100644 Barotrauma/BarotraumaShared/libsteam_api.dylib delete mode 100644 Barotrauma/BarotraumaShared/libsteam_api64.dylib delete mode 100644 Barotrauma/BarotraumaShared/libsteam_api64.so create mode 100644 Libraries/Facepunch.Steamworks/Classes/AuthTicketForWebApi.cs delete mode 100644 Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingConnectionCustomSignaling.cs delete mode 100644 Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingCustomSignalingRecvContext.cs create mode 100644 Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingFakeUDPPort.cs create mode 100644 Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingMessages.cs delete mode 100644 Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamTV.cs create mode 100644 Libraries/Facepunch.Steamworks/Networking/BroadcastBufferManager.cs create mode 100644 Libraries/Facepunch.Steamworks/Networking/ConnectionLaneStatus.cs create mode 100644 Libraries/Facepunch.Steamworks/Networking/ConnectionStatus.cs create mode 100644 Libraries/Facepunch.Steamworks/Networking/Delegates.cs delete mode 100644 Libraries/Facepunch.Steamworks/Networking/NetDebugFunc.cs create mode 100644 Libraries/Facepunch.Steamworks/Structs/UgcAdditionalPreview.cs create mode 100644 Libraries/Facepunch.Steamworks/Structs/UgcAdditionalPreview.cs.meta create mode 100644 Libraries/Facepunch.Steamworks/libsteam_api64.dylib create mode 100644 Libraries/Facepunch.Steamworks/libsteam_api64.so diff --git a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs index 187b87f7e..272aace14 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs @@ -255,7 +255,7 @@ namespace Barotrauma /// public bool Freeze { get; set; } - public void MoveCamera(float deltaTime, bool allowMove = true, bool allowZoom = true, bool? followSub = null) + public void MoveCamera(float deltaTime, bool allowMove = true, bool allowZoom = true, bool allowInput = true, bool? followSub = null) { prevPosition = position; prevZoom = zoom; @@ -268,7 +268,7 @@ namespace Barotrauma Vector2 moveInput = Vector2.Zero; if (allowMove && !Freeze) { - if (GUI.KeyboardDispatcher.Subscriber == null) + if (GUI.KeyboardDispatcher.Subscriber == null && allowInput) { if (PlayerInput.KeyDown(Keys.LeftShift)) { moveSpeed *= 2.0f; } if (PlayerInput.KeyDown(Keys.LeftControl)) { moveSpeed *= 0.5f; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/AITarget.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/AITarget.cs index 51bf3a451..dd4b2855e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/AITarget.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/AITarget.cs @@ -50,9 +50,9 @@ namespace Barotrauma } else if (Entity is Item i) { - if (i.Submarine != null && i.GetComponent() == null) + if (i.Submarine != null && i.Container != null) { - // Don't show items that are inside the submarine, because monsters shouldn't target them when they are inside and the monsters are outside. + // Don't show contained items that are inside the submarine, because they shouldn't attract monsters. return; } color = Color.CadetBlue; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs index 3c2985c11..672fffbb7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs @@ -82,15 +82,20 @@ namespace Barotrauma { var previousNode = path.Nodes[i - 1]; var currentNode = path.Nodes[i]; + bool isPathActive = !path.Finished && !path.IsAtEndNode; + Color pathColor = isPathActive ? Color.Blue * 0.5f : Color.Gray; GUI.DrawLine(spriteBatch, new Vector2(currentNode.DrawPosition.X, -currentNode.DrawPosition.Y), new Vector2(previousNode.DrawPosition.X, -previousNode.DrawPosition.Y), - Color.Blue * 0.5f, 0, 3); + pathColor, 0, 3); - GUIStyle.SmallFont.DrawString(spriteBatch, - currentNode.ID.ToString(), - new Vector2(currentNode.DrawPosition.X - 10, -currentNode.DrawPosition.Y - 30), - Color.Blue); + if (isPathActive) + { + GUIStyle.SmallFont.DrawString(spriteBatch, + currentNode.ID.ToString(), + new Vector2(currentNode.DrawPosition.X - 10, -currentNode.DrawPosition.Y - 30), + Color.Blue); + } } if (path.CurrentNode != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs index 6f3eda991..c09d92817 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs @@ -469,6 +469,7 @@ namespace Barotrauma float gibParticleAmount = MathHelper.Clamp(limb.Mass / character.AnimController.Mass, 0.1f, 1.0f); foreach (ParticleEmitter emitter in character.GibEmitters) { + if (emitter?.Prefab == null) { continue; } if (inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Air) { continue; } if (!inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Water) { continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 5c54b9ba2..115a95e77 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -23,8 +23,6 @@ namespace Barotrauma protected float hudInfoTimer = 1.0f; protected bool hudInfoVisible = false; - private float pressureParticleTimer; - private float findFocusedTimer; protected float lastRecvPositionUpdateTime; @@ -299,7 +297,9 @@ namespace Barotrauma keys[i].SetState(); } - if (CharacterInventory.IsMouseOnInventory && CharacterHUD.ShouldDrawInventory(this)) + if (CharacterInventory.IsMouseOnInventory && + !keys[(int)InputType.Aim].Held && + CharacterHUD.ShouldDrawInventory(this)) { ResetInputIfPrimaryMouse(InputType.Use); ResetInputIfPrimaryMouse(InputType.Shoot); @@ -340,13 +340,6 @@ namespace Barotrauma cam.Zoom = MathHelper.Lerp(cam.Zoom, cam.DefaultZoom + (Math.Max(pressure, 10) / 150.0f) * Rand.Range(0.9f, 1.1f), zoomInEffectStrength); - - pressureParticleTimer += pressure * deltaTime; - if (pressureParticleTimer > 10.0f) - { - GameMain.ParticleManager.CreateParticle(Params.BleedParticleWater, WorldPosition + Rand.Vector(5.0f), Rand.Vector(10.0f)); - pressureParticleTimer = 0.0f; - } } } @@ -722,7 +715,8 @@ namespace Barotrauma break; default: var petBehavior = enemyAI.PetBehavior; - if (petBehavior != null && petBehavior.Happiness < petBehavior.MaxHappiness * 0.25f) + if (petBehavior != null && + (petBehavior.Happiness < petBehavior.UnhappyThreshold || petBehavior.Hunger > petBehavior.HungryThreshold)) { PlaySound(CharacterSound.SoundType.Unhappy); } @@ -859,21 +853,11 @@ namespace Barotrauma if (Controlled.AnimController.Stairs != null) { + //consider the bottom of the stairs the "floor of the room the controlled character is in" yPos = Controlled.AnimController.Stairs.SimPosition.Y - Controlled.AnimController.Stairs.RectHeight * 0.5f; } - foreach (var ladder in Ladder.List) - { - if (CanInteractWith(ladder.Item) && Controlled.CanInteractWith(ladder.Item)) - { - float xPos = ladder.Item.SimPosition.X; - if (Math.Abs(xPos - SimPosition.X) < 3.0) - { - yPos = ladder.Item.SimPosition.Y - ladder.Item.RectHeight * 0.5f; - } - break; - } - } + //don't show the HUD texts if the character is below the floor of the room the controlled character is in if (AnimController.FloorY < yPos) { return; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index 83c669f6e..5f0c45241 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -99,7 +99,7 @@ namespace Barotrauma public override bool Interrupted => Character.Removed || !Character.Enabled; public override Color Color => - Character.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.PoisonType) > 0 || Character.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.ParalysisType) > 0 ? + Character.CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.PoisonType) > 0 || Character.CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.ParalysisType) > 0 ? GUIStyle.HealthBarColorPoisoned : GUIStyle.Red; public override string NumberToDisplay => string.Empty; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index 47d87e1ca..dce0ec9cd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -19,7 +19,6 @@ namespace Barotrauma public bool LastControlled; public int CrewListIndex { get; set; } = -1; - #warning TODO: Refactor private Sprite disguisedPortrait; private List disguisedAttachmentSprites; private Vector2? disguisedSheetIndex; @@ -609,7 +608,6 @@ namespace Barotrauma CharacterInfo = info; parentComponent = parent; HasIcon = hasIcon; - RecreateFrameContents(); } @@ -848,6 +846,12 @@ namespace Barotrauma var info = CharacterInfo; + if (info.HeadSprite == null) + { + DebugConsole.ThrowError($"Head Selection: the head sprite is null! Failed to open the head selection."); + return false; + } + float characterHeightWidthRatio = info.HeadSprite.size.Y / info.HeadSprite.size.X; HeadSelectionList ??= new GUIListBox( new RectTransform( @@ -885,8 +889,13 @@ namespace Barotrauma GUILayoutGroup row = null; int itemsInRow = 0; - ContentXElement headElement = info.Ragdoll.MainElement.Elements().FirstOrDefault(e => + ContentXElement headElement = info.Ragdoll.MainElement?.Elements().FirstOrDefault(e => e.GetAttributeString("type", "").Equals("head", StringComparison.OrdinalIgnoreCase)); + if (headElement == null) + { + DebugConsole.ThrowError($"Head Selection: the head element is null in {info.ragdoll.FileName}! Failed to open the head selection."); + return false; + } ContentXElement headSpriteElement = headElement.GetChildElement("sprite"); ContentPath spritePathWithTags = headSpriteElement.GetAttributeContentPath("texture"); @@ -963,7 +972,7 @@ namespace Barotrauma private bool SwitchAttachment(GUIScrollBar scrollBar, WearableType type) { var info = CharacterInfo; - int index = (int)scrollBar.BarScrollValue; + int index = (int)Math.Round(scrollBar.BarScrollValue); switch (type) { case WearableType.Beard: diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 8cd302262..de19d3561 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -201,9 +201,9 @@ namespace Barotrauma keys[(int)InputType.Use].Held = useInput; keys[(int)InputType.Use].SetState(false, useInput); + bool crouching = msg.ReadBoolean(); if (AnimController is HumanoidAnimController) { - bool crouching = msg.ReadBoolean(); keys[(int)InputType.Crouch].Held = crouching; keys[(int)InputType.Crouch].SetState(false, crouching); } @@ -269,7 +269,34 @@ namespace Barotrauma if (readStatus) { ReadStatus(msg); - AIController?.ClientRead(msg); + bool isEnemyAi = msg.ReadBoolean(); + if (isEnemyAi) + { + byte aiState = msg.ReadByte(); + if (AIController is EnemyAIController enemyAi) + { + enemyAi.State = (AIState)aiState; + } + else + { + DebugConsole.AddWarning($"Received enemy AI data for a character with no {nameof(EnemyAIController)}. Ignoring..."); + } + bool isPet = msg.ReadBoolean(); + if (isPet) + { + byte happiness = msg.ReadByte(); + byte hunger = msg.ReadByte(); + if ((AIController as EnemyAIController)?.PetBehavior is PetBehavior petBehavior) + { + petBehavior.Happiness = (float)happiness / byte.MaxValue * petBehavior.MaxHappiness; + petBehavior.Hunger = (float)hunger / byte.MaxValue * petBehavior.MaxHunger; + } + else + { + DebugConsole.AddWarning($"Received pet AI data for a character with no {nameof(PetBehavior)}. Ignoring..."); + } + } + } } msg.ReadPadBits(); @@ -323,7 +350,7 @@ namespace Barotrauma } else { - Inventory.ClientEventRead(msg, sendingTime); + Inventory.ClientEventRead(msg); } break; case EventType.Control: @@ -379,7 +406,7 @@ namespace Barotrauma float targetY = msg.ReadSingle(); Vector2 targetSimPos = new Vector2(targetX, targetY); //255 = entity already removed, no need to do anything - if (attackLimbIndex == 255 || Removed) { break; } + if (attackLimbIndex == 255 || targetEntityID == NullEntityID || Removed) { break; } if (attackLimbIndex >= AnimController.Limbs.Length) { //it's possible to get these errors when mid-round syncing, as the client may not @@ -639,7 +666,7 @@ namespace Barotrauma } else { - DebugConsole.ThrowError("Could not set order \"" + orderPrefab.Identifier + "\" for character \"" + character.Name + "\" because required target entity was not found."); + DebugConsole.AddSafeError("Could not set order \"" + orderPrefab.Identifier + "\" for character \"" + character.Name + "\" because required target entity was not found."); } } else diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 602518293..76919ce12 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -257,7 +257,8 @@ namespace Barotrauma { for (int i = 0; i < character.Inventory.Capacity; i++) { - if (character.Inventory.SlotTypes[i] != InvSlotType.HealthInterface || Character.Controlled != Character) { continue; } + if (character.Inventory.SlotTypes[i] != InvSlotType.HealthInterface) { continue; } + if (character.Inventory.HideSlot(i)) { continue; } //don't draw the item if it's being dragged out of the slot bool drawItem = !Inventory.DraggingItems.Any() || !Character.Inventory.GetItemsAt(i).All(it => Inventory.DraggingItems.Contains(it)) || character.Inventory.visualSlots[i].MouseOn(); @@ -479,6 +480,17 @@ namespace Barotrauma inventoryScale = Inventory.UIScale; uiScale = GUI.Scale; + showHiddenAfflictionsButton.RectTransform.NonScaledSize = new Point(afflictionIconContainer.Rect.Height); + //remove affliction icons so we recreate and resize them + for (int i = afflictionIconContainer.CountChildren - 1; i >= 0; i--) + { + var child = afflictionIconContainer.GetChild(i); + if (child.UserData is AfflictionPrefab) + { + afflictionIconContainer.RemoveChild(child); + } + } + healthBarHolder.RectTransform.AbsoluteOffset = HUDLayoutSettings.HealthBarArea.Location; healthBarHolder.RectTransform.NonScaledSize = HUDLayoutSettings.HealthBarArea.Size; healthBarHolder.RectTransform.RelativeOffset = Vector2.Zero; @@ -496,6 +508,8 @@ namespace Barotrauma } healthWindow.RectTransform.RecalculateChildren(false); + + Character.Inventory?.RefreshSlotPositions(); } public void UpdateClientSpecific(float deltaTime) @@ -579,7 +593,7 @@ namespace Barotrauma bool inWater = Character.AnimController.InWater; var drawTarget = inWater ? Particles.ParticlePrefab.DrawTargetType.Water : Particles.ParticlePrefab.DrawTargetType.Air; - var emitter = Character.BloodEmitters.FirstOrDefault(e => e.Prefab.ParticlePrefab.DrawTarget == drawTarget || e.Prefab.ParticlePrefab.DrawTarget == Particles.ParticlePrefab.DrawTargetType.Both); + var emitter = Character.BloodEmitters.FirstOrDefault(e => e.Prefab.ParticlePrefab?.DrawTarget == drawTarget || e.Prefab.ParticlePrefab?.DrawTarget == Particles.ParticlePrefab.DrawTargetType.Both); float particleMinScale = emitter?.Prefab.Properties.ScaleMin ?? 0.5f; float particleMaxScale = emitter?.Prefab.Properties.ScaleMax ?? 1; float severity = Math.Min(affliction.Strength / affliction.Prefab.MaxStrength * Character.Params.BleedParticleMultiplier, 1); @@ -2129,7 +2143,7 @@ namespace Barotrauma { var affliction = kvp.Key; float burnStrength = affliction.Strength / Math.Min(affliction.Prefab.MaxStrength, 100) * affliction.Prefab.BurnOverlayAlpha; - if (kvp.Value == limbHealths[limb.HealthIndex]) + if (kvp.Value == limbHealths[limb.HealthIndex] || !affliction.Prefab.LimbSpecific) { limb.BurnOverlayStrength += burnStrength; limb.DamageOverlayStrength += affliction.Strength / Math.Min(affliction.Prefab.MaxStrength, 100) * affliction.Prefab.DamageOverlayAlpha; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/HealingCooldownClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/HealingCooldownClient.cs index bcc7d4083..49623c6a3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/HealingCooldownClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/HealingCooldownClient.cs @@ -12,8 +12,6 @@ namespace Barotrauma private static DateTimeOffset OnCooldownUntil = DateTimeOffset.MinValue; private const float CooldownDuration = 0.5f; - public static readonly Identifier MedicalItemTag = new Identifier("medical"); - public static void PutOnCooldown() { OnCooldownUntil = DateTimeOffset.UtcNow.AddSeconds(CooldownDuration); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index 61729cd15..f6f7a7331 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -432,20 +432,20 @@ namespace Barotrauma { if (Sprite != null) { - Sprite.Remove(); var source = Sprite.SourceElement; + Sprite.Remove(); Sprite = new Sprite(source, file: GetSpritePath(source, Params.normalSpriteParams, ref _texturePath)); } if (_deformSprite != null) { - _deformSprite.Remove(); var source = _deformSprite.Sprite.SourceElement; + _deformSprite.Remove(); _deformSprite = new DeformableSprite(source, filePath: GetSpritePath(source, Params.deformSpriteParams, ref _texturePath)); } if (DamagedSprite != null) { - DamagedSprite.Remove(); var source = DamagedSprite.SourceElement; + DamagedSprite.Remove(); DamagedSprite = new Sprite(source, file: GetSpritePath(source, Params.damagedSpriteParams, ref _damagedTexturePath)); } for (int i = 0; i < ConditionalSprites.Count; i++) @@ -458,8 +458,8 @@ namespace Barotrauma for (int i = 0; i < DecorativeSprites.Count; i++) { var decorativeSprite = DecorativeSprites[i]; - decorativeSprite.Remove(); var source = decorativeSprite.Sprite.SourceElement; + decorativeSprite.Remove(); DecorativeSprites[i] = new DecorativeSprite(source, file: GetSpritePath(source, Params.decorativeSpriteParams[i], ref _texturePath)); } } @@ -478,9 +478,18 @@ namespace Barotrauma { if (spriteParams != null) { - ContentPath texturePath = - character.Params.VariantFile?.Root?.GetAttributeContentPath("texture", character.Prefab.ContentPackage) - ?? ContentPath.FromRaw(spriteParams.Element.ContentPackage ?? character.Prefab.ContentPackage, spriteParams.GetTexturePath()); + //1. check if the variant file redefines the texture + ContentPath texturePath = character.Params.VariantFile?.Root?.GetAttributeContentPath("texture", character.Prefab.ContentPackage); + //2. check if the base prefab defines the texture + if (texturePath.IsNullOrEmpty() && !character.Prefab.VariantOf.IsEmpty) + { + RagdollParams parentRagdollParams = character.IsHumanoid ? + RagdollParams.GetRagdollParams(character.Prefab.VariantOf) : + RagdollParams.GetRagdollParams(character.Prefab.VariantOf); + texturePath = parentRagdollParams.OriginalElement?.GetAttributeContentPath("texture"); + } + //3. "default case", get the texture from this character's XML + texturePath ??= ContentPath.FromRaw(spriteParams.Element.ContentPackage ?? character.Prefab.ContentPackage, spriteParams.GetTexturePath()); path = GetSpritePath(texturePath); } else @@ -592,6 +601,7 @@ namespace Barotrauma { foreach (ParticleEmitter emitter in character.DamageEmitters) { + if (emitter?.Prefab == null) { continue; } if (InWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Air) { continue; } if (!InWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Water) { continue; } ParticlePrefab overrideParticle = null; @@ -614,6 +624,7 @@ namespace Barotrauma foreach (ParticleEmitter emitter in character.BloodEmitters) { + if (emitter?.Prefab == null) { continue; } if (InWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Air) { continue; } if (!InWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Water) { continue; } emitter.Emit(1.0f, WorldPosition, character.CurrentHull, sizeMultiplier: bloodParticleSize, amountMultiplier: bloodParticleAmount); @@ -727,7 +738,7 @@ namespace Barotrauma } } - float herpesStrength = character.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.SpaceHerpesType); + float herpesStrength = character.CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.SpaceHerpesType); bool hideLimb = Hide || OtherWearables.Any(w => w.HideLimb) || @@ -890,6 +901,28 @@ namespace Barotrauma foreach (WearableSprite wearable in WearingItems) { if (onlyDrawable != null && onlyDrawable != wearable && wearable.CanBeHiddenByOtherWearables) { continue; } + if (wearable.CanBeHiddenByItem.Any()) + { + bool hiddenByOtherItem = false; + foreach (var otherWearable in WearingItems) + { + if (otherWearable == wearable) { continue; } + if (wearable.CanBeHiddenByItem.Contains(otherWearable.WearableComponent.Item.Prefab.Identifier)) + { + hiddenByOtherItem = true; + break; + } + foreach (Identifier tag in wearable.CanBeHiddenByItem) + { + if (otherWearable.WearableComponent.Item.HasTag(tag)) + { + hiddenByOtherItem = true; + break; + } + } + } + if (hiddenByOtherItem) { continue; } + } DrawWearable(wearable, depthStep, spriteBatch, blankColor, alpha: color.A / 255f, spriteEffect); //if there are multiple sprites on this limb, make the successive ones be drawn in front depthStep += step; @@ -1260,12 +1293,11 @@ namespace Barotrauma private void DrawWearable(WearableSprite wearable, float depthStep, SpriteBatch spriteBatch, Color color, float alpha, SpriteEffects spriteEffect) { - var (finalColor, origin, rotation, scale, depth) - = CalculateDrawParameters(wearable, depthStep, color, alpha); - + if (wearable.Sprite?.Texture == null) { return; } + var (finalColor, origin, rotation, scale, depth) = CalculateDrawParameters(wearable, depthStep, color, alpha); var prevEffect = spriteBatch.GetCurrentEffect(); var alphaClipper = WearingItems.Find(w => w.AlphaClipOtherWearables); - bool shouldApplyAlphaClip = alphaClipper != null && wearable != alphaClipper; + bool shouldApplyAlphaClip = alphaClipper?.Sprite?.Texture != null && wearable != alphaClipper; if (shouldApplyAlphaClip) { ApplyAlphaClip(spriteBatch, wearable, alphaClipper, spriteEffect); @@ -1308,13 +1340,13 @@ namespace Barotrauma OtherWearables.ForEach(w => w.Sprite.Remove()); OtherWearables.Clear(); - HuskSprite?.Sprite.Remove(); + HuskSprite?.Sprite?.Remove(); HuskSprite = null; - HairWithHatSprite?.Sprite.Remove(); + HairWithHatSprite?.Sprite?.Remove(); HairWithHatSprite = null; - HerpesSprite?.Sprite.Remove(); + HerpesSprite?.Sprite?.Remove(); HerpesSprite = null; TintMask?.Remove(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxComponent.cs new file mode 100644 index 000000000..014ccad7b --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxComponent.cs @@ -0,0 +1,114 @@ +#nullable enable + +using System; +using System.Linq; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace Barotrauma +{ + internal partial class CircuitBoxComponent + { + public static Option EditingHUD = Option.None; + + private Sprite Sprite => Item.Prefab.InventoryIcon ?? Item.Prefab.Sprite; + + private CircuitBoxLabel? label; + private CircuitBoxLabel Label + { + get + { + if (label is { } l) + { + return l; + } + + var name = TextManager.Get($"circuitboxnode.{Item.Prefab.Identifier}").Fallback($"[FALLBACK] {Item.Name}"); + label = new CircuitBoxLabel(name, GUIStyle.LargeFont); + return label.Value; + } + } + + public void UpdateEditing(RectTransform parent) + { + if (EditingHUD.TryUnwrap(out var editor)) + { + if (editor.UserData == this) { return; } + RemoveEditingHUD(); + } + EditingHUD = Option.Some(CreateEditingHUD(parent)); + } + + public static void RemoveEditingHUD() + { + if (!EditingHUD.TryUnwrap(out var editor)) { return; } + + editor.RectTransform.Parent = null; + EditingHUD = Option.None; + } + + public GUIComponent CreateEditingHUD(RectTransform parent) + { + GUIFrame frame = new(new RectTransform(new Vector2(0.4f, 0.3f), parent, Anchor.TopRight)) + { + UserData = this + }; + + GUIListBox listBox = new(new RectTransform(ToolBox.PaddingSizeParentRelative(frame.RectTransform, 0.8f), frame.RectTransform, Anchor.Center)) + { + KeepSpaceForScrollBar = true, + AutoHideScrollBar = false, + CanTakeKeyBoardFocus = false + }; + + bool isEditor = Screen.Selected is { IsEditor: true }; + + GUILayoutGroup titleHolder = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.3f), listBox.Content.RectTransform)); + new GUITextBlock(new RectTransform(Vector2.One, titleHolder.RectTransform), Item.Name, font: GUIStyle.LargeFont) + { + TextColor = Color.White, + Color = Color.Black + }; + int fieldCount = 0; + + foreach (ItemComponent ic in Item.Components) + { + if (ic is Holdable) { continue; } + if (!ic.AllowInGameEditing) { continue; } + if (SerializableProperty.GetProperties(ic).Count == 0 && + !SerializableProperty.GetProperties(ic).Any(p => p.GetAttribute().IsEditable(ic))) + { + continue; + } + + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), listBox.Content.RectTransform), style: "HorizontalLine"); + + var componentEditor = new SerializableEntityEditor(listBox.Content.RectTransform, ic, inGame: !isEditor, showName: false, titleFont: GUIStyle.SubHeadingFont); + fieldCount += componentEditor.Fields.Count; + + ic.CreateEditingHUD(componentEditor); + componentEditor.Recalculate(); + } + + if (fieldCount == 0) + { + frame.Visible = false; + } + + return frame; + } + + public override void DrawHeader(SpriteBatch spriteBatch, RectangleF drawRect, Color color) + { + // scale to topRect height + Vector2 scale = new(drawRect.Height / MathF.Min(Sprite.size.X, Sprite.size.Y)), + spritePosition = new(drawRect.Left, drawRect.Top); + + float spriteWidth = Sprite.size.X * scale.X; + + Sprite.Draw(spriteBatch, spritePosition, Color.White, Vector2.Zero, 0f, scale); + GUI.DrawString(spriteBatch, new Vector2(spritePosition.X + spriteWidth + CircuitBoxSizes.NodeHeaderTextPadding, drawRect.Center.Y - Label.Size.Y / 2f), Label.Value, GUIStyle.TextColorNormal, font: GUIStyle.LargeFont); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxConnection.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxConnection.cs new file mode 100644 index 000000000..7fea1d1f1 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxConnection.cs @@ -0,0 +1,127 @@ +#nullable enable + +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace Barotrauma +{ + internal abstract partial class CircuitBoxConnection + { + public string Name => Label.Value.Value; + public CircuitBoxLabel Label { get; private set; } + + private Sprite? knobSprite, + screwSprite, + connectorSprite; + + private static int padding => GUI.IntScale(8); + + private Option tooltip = Option.None; + + private partial void InitProjSpecific(CircuitBox circuitBox) + { + Label = new CircuitBoxLabel(Connection.Name, GUIStyle.SubHeadingFont); + knobSprite = circuitBox.ConnectionSprite; + screwSprite = circuitBox.ConnectionScrewSprite; + connectorSprite = circuitBox.WireConnectorSprite; + Length = Rect.Width + padding + Label.Size.X; + } + + public void Draw(SpriteBatch spriteBatch, Vector2 drawPos, Vector2 parentPos, Color color) + { + if (CircuitBox.UI is not { } circuitBoxUi) { return; } + var drawRect = CircuitBoxNode.OverrideRectLocation(Rect, drawPos, parentPos); + + Vector2 cursorPos = circuitBoxUi.GetCursorPosition(); + cursorPos.Y = -cursorPos.Y; + + bool isMouseOver = drawRect.Contains(cursorPos); + + float xPos; + if (IsOutput) + { + xPos = drawRect.Left - padding - Label.Size.X; + } + else + { + xPos = drawRect.Right + padding; + } + + Vector2 stringPos = new Vector2(xPos, drawRect.Center.Y - Label.Size.Y / 2f); + GUI.DrawString(spriteBatch, stringPos, Label.Value, GUIStyle.TextColorNormal, font: Label.Font); + + if (knobSprite is null) + { + CircuitBoxUI.DrawRectangleWithBorder(spriteBatch, drawRect, GUIStyle.Blue * 0.3f, GUIStyle.Blue); + } + else + { + float scale = drawRect.Height / knobSprite.size.Y; + knobSprite?.Draw(spriteBatch, drawRect.Center, color, 0f, scale); + } + + bool isScrewed = this switch + { + CircuitBoxOutputConnection output => output.ExternallyConnectedFrom.Count > 0, + CircuitBoxInputConnection input => input.ExternallyConnectedTo.Count > 0, + _ => Connection.Wires.Count > 0 || + Connection.CircuitBoxConnections.Count > 0 || + ExternallyConnectedFrom.Count > 0 + }; + + if (isMouseOver) + { + var glowSprite = GUIStyle.UIGlowCircular.Value.Sprite; + float glowScale = 40f / glowSprite.size.X; + if (isScrewed) + { + glowScale *= 1.2f; + } + glowSprite.Draw(spriteBatch, position, GUIStyle.Yellow, glowSprite.size / 2, scale: glowScale); + } + + tooltip = Option.None; + if (ConnectionPanel.ShouldDebugDrawWiring) + { + Connection.DrawConnectionDebugInfo(spriteBatch, Connection, drawRect.Center, isScrewed ? 1.1f : 0.9f, out var tooltipText); + + if (isMouseOver && !tooltipText.IsNullOrEmpty()) + { + tooltip = Option.Some(tooltipText); + } + } + + if (!isScrewed) { return; } + + if (screwSprite is not null) + { + float screwScale = drawRect.Height / screwSprite.size.Y; + screwSprite.Draw(spriteBatch, drawRect.Center, color, 0f, screwScale); + } + + if (connectorSprite is not null) + { + float screwScale = drawRect.Height / connectorSprite.size.Y * 2f; + Vector2 pos = drawRect.Center; + + connectorSprite.Draw(spriteBatch, + pos: pos, + color: Color.White, + origin: connectorSprite.Origin, + rotate: MathHelper.Pi / (IsOutput ? -2f : 2f), + scale: screwScale, + spriteEffect: SpriteEffects.None); + } + } + + public void DrawHUD(SpriteBatch spriteBatch, Camera camera) + { + if (!tooltip.TryUnwrap(out var text)) { return; } + + var drawPos = camera.WorldToScreen(new Vector2(Rect.Right, -Rect.Bottom)); + + GUIComponent.DrawToolTip(spriteBatch, text, drawPos); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxLabel.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxLabel.cs new file mode 100644 index 000000000..cd9013e64 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxLabel.cs @@ -0,0 +1,22 @@ +#nullable enable + +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + internal readonly struct CircuitBoxLabel + { + public LocalizedString Value { get; } + + public Vector2 Size { get; } + + public GUIFont Font { get; } + + public CircuitBoxLabel(LocalizedString value, GUIFont font) + { + Value = value; + Font = font; + Size = font.MeasureString(font.ForceUpperCase ? value.Value.ToUpperInvariant() : value.Value); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxMouseDragSnapshotHandler.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxMouseDragSnapshotHandler.cs new file mode 100644 index 000000000..71d74c142 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxMouseDragSnapshotHandler.cs @@ -0,0 +1,233 @@ +#nullable enable + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + /// + /// This class handles a couple things: + /// - Figuring out which components should be moved when dragging a certain part of the UI. + /// - Finding components, connectors and wires under cursor. + /// - Determines whether the user is dragging something. + /// + internal sealed class CircuitBoxMouseDragSnapshotHandler + { + public IEnumerable Nodes => circuitBoxUi.CircuitBox.Components.Union(circuitBoxUi.CircuitBox.InputOutputNodes); + + private IReadOnlyList Wires => circuitBoxUi.CircuitBox.Wires; + + // List of all connections in the circuit box + private ImmutableArray connections = ImmutableArray.Empty; + + // Nodes that were under cursor when dragging started + private ImmutableHashSet lastNodesUnderCursor = ImmutableHashSet.Empty, + // Nodes that were selected when dragging started + lastSelectedComponents = ImmutableHashSet.Empty, + // Nodes that should be moved when dragging + moveAffectedComponents = ImmutableHashSet.Empty; + + public ImmutableHashSet GetLastComponentsUnderCursor() => lastNodesUnderCursor; + public ImmutableHashSet GetMoveAffectedComponents() => moveAffectedComponents; + + public Option LastConnectorUnderCursor = Option.None; + public Option LastWireUnderCursor = Option.None; + + /// + /// If the user is currently dragging a node + /// + public bool IsDragging { get; private set; } + + /// + /// If the user is currently dragging a wire + /// + public bool IsWiring { get; private set; } + + private Vector2 startClick = Vector2.Zero; + private readonly CircuitBoxUI circuitBoxUi; + + /// + /// How far the user has to drag the mouse while holding down the button before dragging starts + /// + private const float dragTreshold = 16f; + + public CircuitBoxMouseDragSnapshotHandler(CircuitBoxUI ui) + { + circuitBoxUi = ui; + } + + /// + /// Called when the user holds down the mouse button + /// + public void StartDragging() + { + Vector2 cursorPos = circuitBoxUi.GetCursorPosition(); + SnapshotNodesUnderCursor(cursorPos); + SnapshotSelectedNodes(); + SnapshotMoveAffectedNodes(); + startClick = cursorPos; + } + + /// + /// Finds all connections and gathers them into a single list for easier iteration. + /// + public void UpdateConnections() + { + var builder = ImmutableArray.CreateBuilder(); + + builder.AddRange(circuitBoxUi.CircuitBox.Inputs); + builder.AddRange(circuitBoxUi.CircuitBox.Outputs); + + foreach (var node in Nodes) + { + builder.AddRange(node.Connectors); + } + + connections = builder.ToImmutable(); + } + + /// + /// Finds a possible connector under the cursor. + /// + public Option FindConnectorUnderCursor(Vector2 cursorPos) + { + foreach (var connection in connections) + { + if (connection.Contains(cursorPos)) + { + return Option.Some(connection); + } + } + + return Option.None; + } + + /// + /// Finds a possible wire under the cursor. + /// + public Option FindWireUnderCursor(Vector2 cursorPos) + { + foreach (CircuitBoxWire wire in Wires) + { + if (wire is { IsSelected: true, IsSelectedByMe: false }) { continue; } + if (wire.Renderer.Contains(cursorPos)) + { + return Option.Some(wire); + } + } + + return Option.None; + } + + /// + /// Find all nodes that are currently under the cursor that are not selected by someone else. + /// + public ImmutableHashSet FindNodesUnderCursor(Vector2 cursorPos) + { + var builder = ImmutableHashSet.CreateBuilder(); + foreach (var node in Nodes) + { + if (node is { IsSelected: true, IsSelectedByMe: false }) { continue; } + if (node.Rect.Contains(cursorPos)) + { + builder.Add(node); + } + } + + return builder.ToImmutable(); + } + + /// + /// Finds and stores all nodes, connectors and wires that are under the cursor when dragging starts. + /// + private void SnapshotNodesUnderCursor(Vector2 cursorPos) + { + lastNodesUnderCursor = FindNodesUnderCursor(cursorPos); + LastConnectorUnderCursor = FindConnectorUnderCursor(cursorPos); + LastWireUnderCursor = FindWireUnderCursor(cursorPos); + } + + /// + /// Stores all nodes that are currently selected when dragging starts. + /// There's no real way to change your selection while dragging so this is kinda pointless + /// but we snapshot it anyway just in case. + /// + private void SnapshotSelectedNodes() + { + lastSelectedComponents = Nodes.Where(static n => n is { IsSelected: true, IsSelectedByMe: true }).ToImmutableHashSet(); + } + + /// + /// Stores all nodes that should be moved when dragging starts. + /// + private void SnapshotMoveAffectedNodes() + { + bool moveSelection = lastNodesUnderCursor.Any(node => lastSelectedComponents.Contains(node)); + + /* + * If the user is dragging a selection, we should move all selected nodes (true). + * + * But for convenience, if the user is dragging a single node that is not part of the selection, + * we should move that node only instead and leave the selection alone. (false) + */ + moveAffectedComponents = moveSelection switch + { + true => lastSelectedComponents, + false => circuitBoxUi.GetTopmostNode(lastNodesUnderCursor) switch + { + null => ImmutableHashSet.Empty, + var node => ImmutableHashSet.Create(node) + } + }; + } + + public Vector2 GetDragAmount(Vector2 mousePos) => mousePos - startClick; + + /// + /// Called when the user releases the mouse button + /// + public void EndDragging() + { + startClick = Vector2.Zero; + IsDragging = false; + IsWiring = false; + lastNodesUnderCursor = ImmutableHashSet.Empty; + } + + public void UpdateDrag(Vector2 cursorPos) + { + // if there are no connectors under cursor, we can't be wiring anything + if (LastConnectorUnderCursor.IsNone()) + { + IsWiring = false; + } + + // if there are no nodes under cursor, we can't be dragging anything + if (lastNodesUnderCursor.IsEmpty) + { + IsDragging = false; + } + + // startClick is set to zero when the user releases the mouse button, so we should be neither dragging nor wiring in this state + if (startClick == Vector2.Zero) + { + IsDragging = false; + IsWiring = false; + return; + } + + bool isDragTresholdExceeded = Vector2.DistanceSquared(startClick, cursorPos) > dragTreshold * dragTreshold; + + if (LastConnectorUnderCursor.IsNone()) + { + IsDragging |= isDragTresholdExceeded; + } + else + { + IsWiring |= isDragTresholdExceeded; + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxNode.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxNode.cs new file mode 100644 index 000000000..b470f97f9 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxNode.cs @@ -0,0 +1,88 @@ +#nullable enable + +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace Barotrauma +{ + internal partial class CircuitBoxNode + { + private RectangleF DrawRect, + TopDrawRect; + + private void UpdateDrawRects() + { + var drawRect = new RectangleF(Position - Size / 2f, Size); + drawRect.Y = -drawRect.Y; + drawRect.Y -= drawRect.Height; + DrawRect = drawRect; + + TopDrawRect = new RectangleF(drawRect.X, drawRect.Y - (CircuitBoxSizes.NodeHeaderHeight - 1), drawRect.Width, CircuitBoxSizes.NodeHeaderHeight); + } + + public void OnUICreated() + { + Size = CalculateSize(Connectors); + UpdatePositions(); + } + + public void DrawBackground(SpriteBatch spriteBatch, RectangleF drawRect, RectangleF topDrawRect, Color color) + { + CircuitBox.NodeFrameSprite?.Draw(spriteBatch, drawRect, color); + CircuitBox.NodeTopSprite?.Draw(spriteBatch, topDrawRect, color); + } + + public void Draw(SpriteBatch spriteBatch, Vector2 drawPos, Color color) + { + RectangleF drawRect = OverrideRectLocation(DrawRect, drawPos, Position), + topDrawRect = OverrideRectLocation(TopDrawRect, drawPos, Position); + + DrawBackground(spriteBatch, drawRect, topDrawRect, color); + DrawHeader(spriteBatch, topDrawRect, color); + + DrawConnectors(spriteBatch, drawPos); + } + + public void DrawHUD(SpriteBatch spriteBatch, Camera camera) + { + foreach (var c in Connectors) + { + c.DrawHUD(spriteBatch, camera); + } + } + + public virtual void DrawHeader(SpriteBatch spriteBatch, RectangleF rect, Color color) { } + + public void DrawConnectors(SpriteBatch spriteBatch, Vector2 drawPos) + { + var color = Color.White * Opacity; + foreach (var c in Connectors) + { + c.Draw(spriteBatch, drawPos, Position, color); + } + } + + public void DrawSelection(SpriteBatch spriteBatch, Color color) + { + int pad = GUI.IntScale(8); + + var rect = Rect; + rect.Y = -rect.Y; + rect.Y -= rect.Height; + + rect.Inflate(pad, pad); + + GUI.DrawFilledRectangle(spriteBatch, rect, color * Opacity); + } + + /// + /// Sets the location of the rectangle to a specific position, keeping origin intact. + /// + public static RectangleF OverrideRectLocation(RectangleF rect, Vector2 overridePos, Vector2 originalPos) + { + rect.Location -= new Vector2(originalPos.X, -originalPos.Y); + rect.Location += new Vector2(overridePos.X, -overridePos.Y); + return rect; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxUI.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxUI.cs new file mode 100644 index 000000000..a195b3276 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxUI.cs @@ -0,0 +1,722 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; + +namespace Barotrauma +{ + internal sealed class CircuitBoxUI + { + private readonly Camera camera; + private static readonly Vector2 gridSize = new Vector2(128f); + public readonly CircuitBox CircuitBox; + private bool componentMenuOpen; + private float componentMenuOpenState; + + private GUICustomComponent? circuitComponent; + private GUIFrame? componentMenu; + private GUIButton? toggleMenuButton; + private GUIFrame? selectedWireFrame; + private GUIListBox? componentList; + private GUITextBlock? inventoryIndicatorText; + private readonly Sprite cursorSprite = GUIStyle.CursorSprite[CursorState.Default]; + + private Option selection = Option.None; + private string searchTerm = string.Empty; + + public static Option DraggedWire = Option.None; + + public readonly CircuitBoxMouseDragSnapshotHandler MouseSnapshotHandler; + + public List VirtualWires = new(); + + public CircuitBoxUI(CircuitBox box) + { + camera = new Camera + { + MinZoom = 0.25f, + MaxZoom = 2f + }; + + CircuitBox = box; + MouseSnapshotHandler = new CircuitBoxMouseDragSnapshotHandler(this); + } + +#region UI + + public void CreateGUI(GUIFrame parent) + { + GUIFrame paddedFrame = new GUIFrame(new RectTransform(new Vector2(0.97f, 0.95f), parent.RectTransform, Anchor.Center), style: null); + circuitComponent = new GUICustomComponent(new RectTransform(Vector2.One, paddedFrame.RectTransform), onDraw: (spriteBatch, component) => + { + Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle; + spriteBatch.End(); + spriteBatch.GraphicsDevice.ScissorRectangle = component.Rect; + + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable, transformMatrix: camera.Transform); + DrawCircuits(spriteBatch); + spriteBatch.End(); + + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); + DrawHUD(spriteBatch); + spriteBatch.End(); + + spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect; + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); + }); + + GUIScissorComponent menuContainer = new GUIScissorComponent(new RectTransform(Vector2.One, paddedFrame.RectTransform, anchor: Anchor.Center)) + { + CanBeFocused = false + }; + + componentMenuOpen = true; + componentMenu = new GUIFrame(new RectTransform(new Vector2(1f, 0.4f), menuContainer.Content.RectTransform, Anchor.BottomRight)); + toggleMenuButton = new GUIButton(new RectTransform(new Point(300, 30), GUI.Canvas) { MinSize = new Point(0, 15) }, style: "UIToggleButtonVertical") + { + OnClicked = (btn, userdata) => + { + componentMenuOpen = !componentMenuOpen; + foreach (GUIComponent child in btn.Children) + { + child.SpriteEffects = componentMenuOpen ? SpriteEffects.None : SpriteEffects.FlipVertically; + } + + return true; + } + }; + + GUILayoutGroup menuLayout = new GUILayoutGroup(new RectTransform(Vector2.One, componentMenu.RectTransform), childAnchor: Anchor.TopCenter) { RelativeSpacing = 0.02f }; + GUILayoutGroup headerLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.2f), menuLayout.RectTransform), isHorizontal: true); + + GUILayoutGroup labelLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.33f, 1f), headerLayout.RectTransform), isHorizontal: true); + + GUILayoutGroup searchBarLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.33f, 1f), headerLayout.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true); + GUITextBlock searchBarLabel = new GUITextBlock(new RectTransform(new Vector2(0.15f, 1f), searchBarLayout.RectTransform), "Filter"); + GUITextBox searchbar = new GUITextBox(new RectTransform(new Vector2(0.85f, 1f), searchBarLayout.RectTransform), string.Empty, createClearButton: true); + + new GUIFrame(new RectTransform(new Vector2(0.5f, 0.01f), menuLayout.RectTransform), style: "HorizontalLine"); + + componentList = new GUIListBox(new RectTransform(new Vector2(0.95f, 0.65f), menuLayout.RectTransform)) + { + PlaySoundOnSelect = true, + UseGridLayout = true, + OnSelected = (_, o) => + { + if (o is not ItemPrefab prefab) { return false; } + + CircuitBox.HeldComponent = Option.Some(prefab); + return true; + } + }; + + GUILayoutGroup inventoryLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.33f, 1f), headerLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.Center); + GUILayoutGroup indicatorLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.2f, 1f), inventoryLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + GUIImage indicatorIcon = new GUIImage(new RectTransform(new Vector2(0.5f, 0.8f), indicatorLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "CircuitIndicatorIcon"); + inventoryIndicatorText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), indicatorLayout.RectTransform), GetInventoryText(), font: GUIStyle.SubHeadingFont); + + int gapSize = GUI.IntScale(8); + selectedWireFrame = SubEditorScreen.CreateWiringPanel(Point.Zero, SelectWire); + selectedWireFrame.RectTransform.AbsoluteOffset = new Point(parent.Rect.X - (selectedWireFrame.Rect.Width + gapSize), parent.Rect.Y); + + foreach (ItemPrefab prefab in ItemPrefab.Prefabs.OrderBy(static p => p.Name)) + { + if (!prefab.Tags.Contains("circuitboxcomponent")) { continue; } + + CreateComponentElement(prefab, componentList.Content.RectTransform); + } + + searchbar.OnTextChanged += (tb, s) => + { + searchTerm = s; + UpdateComponentList(); + return true; + }; + int buttonHeight = (int)(GUIStyle.ItemFrameMargin.Y * 0.4f); + var settingsIcon = new GUIButton(new RectTransform(new Point(buttonHeight), parent.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(buttonHeight / 4), MinSize = new Point(buttonHeight) }, + style: "GUIButtonSettings") + { + OnClicked = (btn, userdata) => + { + GUIContextMenu.CreateContextMenu( + new ContextMenuOption("circuitboxsetting.resetview", isEnabled: true, onSelected: ResetCamera) + { + Tooltip = TextManager.Get("circuitboxsettingdescription.resetview") + }, + new ContextMenuOption("circuitboxsetting.find", isEnabled: true, + new ContextMenuOption("circuitboxsetting.focusinput", isEnabled: true, onSelected: () => FindInputOuput(CircuitBoxInputOutputNode.Type.Input)) + { + Tooltip = TextManager.Get("circuitboxsettingdescription.focusinput") + }, + new ContextMenuOption("circuitboxsetting.focusoutput", isEnabled: true, onSelected: () => FindInputOuput(CircuitBoxInputOutputNode.Type.Output)) + { + Tooltip = TextManager.Get("circuitboxsettingdescription.focusoutput") + }, + new ContextMenuOption("circuitboxsetting.focuscircuits", isEnabled: CircuitBox.Components.Any(), onSelected: FindCircuit) + { + Tooltip = TextManager.Get("circuitboxsettingdescription.focuscircuits") + })); + + + void ResetCamera() + { + // Vector2.One because Vector2.Zero means no value + camera.TargetPos = Vector2.One; + } + + void FindInputOuput(CircuitBoxInputOutputNode.Type type) + { + var input = CircuitBox.InputOutputNodes.FirstOrDefault(n => n.NodeType == type); + if (input is null) { return; } + + camera.TargetPos = input.Position; + } + + void FindCircuit() + { + var closestComponent = CircuitBox.Components.MinBy(c => Vector2.DistanceSquared(c.Position, camera.Position)); + if (closestComponent is null) { return; } + + camera.TargetPos = closestComponent.Position; + } + return true; + } + }; + + MouseSnapshotHandler.UpdateConnections(); + + // update scales of everything + foreach (var node in CircuitBox.Components) { node.OnUICreated(); } + foreach (var node in CircuitBox.InputOutputNodes) { node.OnUICreated(); } + foreach (var wire in CircuitBox.Wires) { wire.Update(); } + } + + private string GetInventoryText() + => CircuitBox.ComponentContainer is { } container + ? $"{container.Inventory.AllItems.Count()}/{container.Capacity}" + : "0/0"; + + public void UpdateComponentList() + { + if (inventoryIndicatorText is { } text) + { + text.Text = GetInventoryText(); + } + + if (componentList is null) { return; } + var playerInventory = CircuitBox.GetSortedCircuitBoxSortedItemsFromPlayer(Character.Controlled); + + foreach (GUIComponent child in componentList.Content.Children) + { + if (child.UserData is not ItemPrefab prefab) { continue; } + + child.Enabled = !CircuitBox.IsFull && (!CircuitBox.IsInGame() || CircuitBox.GetApplicableResourcePlayerHas(prefab, playerInventory).IsSome()); + + if (child.GetChild()?.GetChild() is { } image) + { + image.Enabled = child.Enabled; + } + + child.ToolTip = child.Enabled + ? prefab.Description + : RichString.Rich(TextManager.GetWithVariable(new Identifier("CircuitBoxUIComponentNotAvailable"), new Identifier("[item]"), prefab.Name)); + + if (string.IsNullOrWhiteSpace(searchTerm)) + { + child.Visible = true; + continue; + } + + child.Visible = prefab.Name.Contains(searchTerm, StringComparison.OrdinalIgnoreCase); + } + } + + private static bool SelectWire(GUIComponent component, object obj) + { + if (obj is not ItemPrefab prefab) { return false; } + + CircuitBoxWire.SelectedWirePrefab = prefab; + return true; + } + + private static void CreateComponentElement(ItemPrefab prefab, RectTransform parent) + { + GUIFrame itemFrame = new GUIFrame(new RectTransform(new Vector2(0.1f, 0.9f), parent) { MinSize = new Point(0, 50) }, style: "GUITextBox") + { + UserData = prefab + }; + + itemFrame.RectTransform.MinSize = new Point(0, itemFrame.Rect.Width); + itemFrame.RectTransform.MaxSize = new Point(int.MaxValue, itemFrame.Rect.Width); + itemFrame.ToolTip = prefab.Name; + + GUILayoutGroup paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.8f), itemFrame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) + { + Stretch = true, + RelativeSpacing = 0.03f, + CanBeFocused = false + }; + + Sprite icon; + Color iconColor; + + if (prefab.InventoryIcon != null) + { + icon = prefab.InventoryIcon; + iconColor = prefab.InventoryIconColor; + } + else + { + icon = prefab.Sprite; + iconColor = prefab.SpriteColor; + } + + GUIImage? img = null; + if (icon != null) + { + img = new GUIImage(new RectTransform(new Vector2(1.0f, 0.8f), paddedFrame.RectTransform, Anchor.TopCenter), icon) + { + CanBeFocused = false, + LoadAsynchronously = true, + DisabledColor = Color.DarkGray * 0.8f, + Color = iconColor + }; + } + + GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform, Anchor.BottomCenter), + text: prefab.Name, textAlignment: Alignment.Center, font: GUIStyle.SmallFont) + { + CanBeFocused = false + }; + + textBlock.Text = ToolBox.LimitString(textBlock.Text, textBlock.Font, textBlock.Rect.Width); + paddedFrame.Recalculate(); + + if (img != null) + { + img.Scale = Math.Min(Math.Min(img.Rect.Width / img.Sprite.size.X, img.Rect.Height / img.Sprite.size.Y), 1.5f); + img.RectTransform.NonScaledSize = new Point((int)(img.Sprite.size.X * img.Scale), img.Rect.Height); + } + } + +#endregion + + private void DrawHUD(SpriteBatch spriteBatch) + { + float scale = GUI.Scale / 1.5f; + Vector2 offset = new Vector2(20, 40) * scale; + + foreach (var (character, cursor) in CircuitBox.ActiveCursors) + { + if (!cursor.IsActive) { continue; } + + Vector2 cursorWorldPos = camera.WorldToScreen(cursor.DrawPosition); + + if (cursor.Info.DragStart.TryUnwrap(out Vector2 dragStart)) + { + DrawSelection(spriteBatch, dragStart, cursor.DrawPosition, cursor.Color); + } + + if (cursor.HeldPrefab.TryUnwrap(out ItemPrefab? otherHeldPrefab)) + { + otherHeldPrefab.Sprite.Draw(spriteBatch, cursorWorldPos); + } + + cursorSprite?.Draw(spriteBatch, cursorWorldPos, cursor.Color, 0f, scale); + GUI.DrawString(spriteBatch, cursorWorldPos + offset, character.Name, cursor.Color, Color.Black, GUI.IntScale(4), GUIStyle.SmallFont); + } + + if (selection.TryUnwrap(out RectangleF rect)) + { + Vector2 pos1 = rect.Location; + Vector2 pos2 = new Vector2(rect.Location.X + rect.Size.X, rect.Location.Y + rect.Size.Y); + DrawSelection(spriteBatch, pos1, pos2, GUIStyle.Blue); + } + + if (CircuitBox.HeldComponent.TryUnwrap(out ItemPrefab? component)) + { + component.Sprite.Draw(spriteBatch, PlayerInput.MousePosition); + } + + foreach (var c in CircuitBox.Components) + { + c.DrawHUD(spriteBatch, camera); + } + + foreach (var n in CircuitBox.InputOutputNodes) + { + n.DrawHUD(spriteBatch, camera); + } + } + + private void DrawSelection(SpriteBatch spriteBatch, Vector2 pos1, Vector2 pos2, Color color) + { + Vector2 location = camera.WorldToScreen(pos1); + location.Y = -location.Y; + Vector2 location2 = camera.WorldToScreen(pos2); + location2.Y = -location2.Y; + MapEntity.DrawSelectionRect(spriteBatch, location, new Vector2(-(location.X - location2.X), location.Y - location2.Y), color); + } + + private const float lineBaseWidth = 1f; + private static float lineWidth; + + public static void DrawRectangleWithBorder(SpriteBatch spriteBatch, RectangleF rect, Color fillColor, Color borderColor) + { + Vector2 topRight = new Vector2(rect.Right, rect.Top), + topLeft = new Vector2(rect.Left, rect.Top), + bottomRight = new Vector2(rect.Right, rect.Bottom), + bottomLeft = new Vector2(rect.Left, rect.Bottom); + + Vector2 offset = new Vector2(0f, lineWidth / 2f); + + GUI.DrawFilledRectangle(spriteBatch, rect, fillColor); + + spriteBatch.DrawLine(topRight, topLeft, borderColor, thickness: lineWidth); + spriteBatch.DrawLine(topLeft - offset, bottomLeft + offset, borderColor, thickness: lineWidth); + spriteBatch.DrawLine(bottomLeft, bottomRight, borderColor, thickness: lineWidth); + spriteBatch.DrawLine(bottomRight + offset, topRight - offset, borderColor, thickness: lineWidth); + } + + private void DrawCircuits(SpriteBatch spriteBatch) + { + camera.UpdateTransform(interpolate: true, updateListener: false); + SubEditorScreen.DrawOutOfBoundsArea(spriteBatch, camera, CircuitBoxSizes.PlayableAreaSize, GUIStyle.Red * 0.33f); + SubEditorScreen.DrawGrid(spriteBatch, camera, gridSize.X, gridSize.Y, zoomTreshold: false); + lineWidth = lineBaseWidth / camera.Zoom; + + Vector2 mousePos = GetCursorPosition(); + mousePos.Y = -mousePos.Y; + + foreach (CircuitBoxWire wire in CircuitBox.Wires) + { + wire.Renderer.Draw(spriteBatch, GetSelectionColor(wire)); + } + + foreach (var node in CircuitBox.Components) + { + if (node.IsSelected) + { + node.DrawSelection(spriteBatch, GetSelectionColor(node)); + } + + node.Draw(spriteBatch, node.Position, node.Item.Prefab.SignalComponentColor * CircuitBoxNode.Opacity); + } + + foreach (var ioNode in CircuitBox.InputOutputNodes) + { + if (ioNode.IsSelected) + { + ioNode.DrawSelection(spriteBatch, GetSelectionColor(ioNode)); + } + + Color color = ioNode.NodeType is CircuitBoxInputOutputNode.Type.Input ? GUIStyle.Green : GUIStyle.Red; + ioNode.Draw(spriteBatch, ioNode.Position, color * CircuitBoxNode.Opacity); + } + + if (MouseSnapshotHandler.IsDragging) + { + var draggedNodes = MouseSnapshotHandler.GetMoveAffectedComponents(); + Vector2 dragOffset = MouseSnapshotHandler.GetDragAmount(GetCursorPosition()); + foreach (CircuitBoxNode moveable in draggedNodes) + { + Color color = moveable switch + { + CircuitBoxComponent node => node.Item.Prefab.SignalComponentColor, + CircuitBoxInputOutputNode ioNode => ioNode.NodeType is CircuitBoxInputOutputNode.Type.Input ? GUIStyle.Green : GUIStyle.Red, + _ => Color.White + }; + moveable.Draw(spriteBatch, moveable.Position + dragOffset, color * 0.5f); + } + } + + if (DraggedWire.TryUnwrap(out CircuitBoxWireRenderer? draggedWire)) + { + draggedWire.Draw(spriteBatch, GUIStyle.Yellow); + } + } + + private Color GetSelectionColor(CircuitBoxNode node) + => GetSelectionColor(node.SelectedBy, node.IsSelectedByMe); + + private Color GetSelectionColor(CircuitBoxWire wire) + => GetSelectionColor(wire.SelectedBy, wire.IsSelectedByMe); + + private Color GetSelectionColor(ushort selectedBy, bool isSelectedByMe) + { +#if !DEBUG + if (isSelectedByMe) + { + return GUIStyle.Yellow; + } +#endif + + foreach (var (_, cursor) in CircuitBox.ActiveCursors) + { + if (cursor.Info.CharacterID == selectedBy) + { + return cursor.Color; + } + } + + return GUIStyle.Yellow; + } + + private Vector2 cursorPos; + public Vector2 GetCursorPosition() => cursorPos; + public Option GetDragStart() => selection.Select(static f => f.Location); + + public void Update(float deltaTime) + { + cursorPos = camera.ScreenToWorld(PlayerInput.MousePosition); + foreach (CircuitBoxWire wire in CircuitBox.Wires) + { + wire.Update(); + } + + bool foundSelected = false; + foreach (var node in CircuitBox.Components) + { + if (!node.IsSelectedByMe) { continue; } + + foundSelected = true; + if (circuitComponent is not null) + { + node.UpdateEditing(circuitComponent.RectTransform); + } + break; + } + + if (!foundSelected) + { + CircuitBoxComponent.RemoveEditingHUD(); + } + + bool isMouseOn = GUI.MouseOn == circuitComponent; + + if (isMouseOn) + { + Character.DisableControls = true; + } + camera.MoveCamera(deltaTime, allowMove: true, allowZoom: isMouseOn, allowInput: isMouseOn, followSub: false); + + if (camera.TargetPos != Vector2.Zero && MathUtils.NearlyEqual(camera.Position, camera.TargetPos, 0.01f)) + { + camera.TargetPos = Vector2.Zero; + } + + if (isMouseOn) + { + if (CircuitBox.HeldComponent.IsNone() && PlayerInput.PrimaryMouseButtonDown()) + { + MouseSnapshotHandler.StartDragging(); + } + + if (PlayerInput.MidButtonHeld() || (PlayerInput.IsAltDown() && PlayerInput.PrimaryMouseButtonHeld())) + { + Vector2 moveSpeed = PlayerInput.MouseSpeed / camera.Zoom; + moveSpeed.X = -moveSpeed.X; + camera.Position += moveSpeed; + } + + if (PlayerInput.PrimaryMouseButtonHeld()) + { + MouseSnapshotHandler.UpdateDrag(GetCursorPosition()); + } + + if (MouseSnapshotHandler.IsWiring && MouseSnapshotHandler.LastConnectorUnderCursor.TryUnwrap(out var c)) + { + Vector2 start = c.Rect.Center, + end = GetCursorPosition(); + + end.Y = -end.Y; + + if (!c.IsOutput) + { + (start, end) = (end, start); + } + + if (DraggedWire.TryUnwrap(out var wire)) + { + wire.Recompute(start, end, CircuitBoxWire.SelectedWirePrefab.SpriteColor); + } + else + { + DraggedWire = Option.Some(new CircuitBoxWireRenderer(Option.None,start, end, GUIStyle.Red, CircuitBox.WireSprite)); + } + } + else + { + DraggedWire = Option.None; + } + + if (PlayerInput.SecondaryMouseButtonClicked()) + { + OpenContextMenu(); + } + + if (PlayerInput.PrimaryMouseButtonClicked()) + { + if (CircuitBox.HeldComponent.TryUnwrap(out ItemPrefab? prefab)) + { + CircuitBox.AddComponent(prefab, cursorPos); + } + else + { + if (MouseSnapshotHandler.IsDragging && PlayerInput.PrimaryMouseButtonReleased()) + { + CircuitBox.MoveComponent(MouseSnapshotHandler.GetDragAmount(cursorPos), MouseSnapshotHandler.GetMoveAffectedComponents()); + } + else if (!MouseSnapshotHandler.IsWiring) + { + TrySelectComponentsUnderCursor(); + } + } + + if (MouseSnapshotHandler.IsWiring && MouseSnapshotHandler.LastConnectorUnderCursor.TryUnwrap(out var one)) + { + if (MouseSnapshotHandler.FindConnectorUnderCursor(cursorPos).TryUnwrap(out var two)) + { + CircuitBox.AddWire(one, two); + } + } + + CircuitBox.SelectWires(MouseSnapshotHandler.LastWireUnderCursor.TryUnwrap(out var wire) ? ImmutableArray.Create(wire) : ImmutableArray.Empty, !PlayerInput.IsShiftDown()); + + CircuitBox.HeldComponent = Option.None; + MouseSnapshotHandler.EndDragging(); + } + + if (MouseSnapshotHandler.GetLastComponentsUnderCursor().IsEmpty && MouseSnapshotHandler.LastConnectorUnderCursor.IsNone()) + { + UpdateSelection(); + } + + // Allow using both Delete key and Ctrl+D for those who don't have a Delete key + bool hitDeleteCombo = PlayerInput.KeyHit(Keys.Delete) || (PlayerInput.IsCtrlDown() && PlayerInput.KeyHit(Keys.D)); + + if (GUI.KeyboardDispatcher.Subscriber is null && hitDeleteCombo) + { + CircuitBox.RemoveComponents(CircuitBox.Components.Where(static node => node.IsSelectedByMe).ToArray()); + CircuitBox.RemoveWires(CircuitBox.Wires.Where(static wire => wire.IsSelectedByMe).ToImmutableArray()); + } + } + + if (componentMenu is { } menu && toggleMenuButton is { } button) + { + componentMenuOpenState = componentMenuOpen ? Math.Min(componentMenuOpenState + deltaTime * 5.0f, 1.0f) : Math.Max(componentMenuOpenState - deltaTime * 5.0f, 0.0f); + + menu.RectTransform.ScreenSpaceOffset = Vector2.Lerp(new Vector2(0.0f, menu.Rect.Height - 10), Vector2.Zero, componentMenuOpenState).ToPoint(); + button.RectTransform.AbsoluteOffset = new Point(menu.Rect.X + ((menu.Rect.Width / 2) - (button.Rect.Width / 2)), menu.Rect.Y - button.Rect.Height); + } + + camera.Position = Vector2.Clamp(camera.Position, + new Vector2(-CircuitBoxSizes.PlayableAreaSize / 2f), + new Vector2(CircuitBoxSizes.PlayableAreaSize / 2f)); + } + + private void UpdateSelection() + { + if (!PlayerInput.IsAltDown() && PlayerInput.PrimaryMouseButtonDown()) + { + selection = Option.Some(new RectangleF(GetCursorPosition(), Vector2.Zero)); + } + + if (!selection.TryUnwrap(out RectangleF rect)) { return; } + + if (!PlayerInput.PrimaryMouseButtonHeld()) + { + selection = Option.None; + RectangleF selectionRect = Submarine.AbsRectF(rect.Location, rect.Size); + + float treshold = 12f / camera.Zoom; + if (selectionRect.Size.X < treshold || selectionRect.Size.Y < treshold) { return; } + + CircuitBox.SelectComponents(MouseSnapshotHandler.Nodes.Where(n => selectionRect.Intersects(n.Rect)).ToImmutableHashSet(), !PlayerInput.IsShiftDown()); + } + else + { + RectangleF oldRect = rect; + rect.Size = camera.ScreenToWorld(PlayerInput.MousePosition) - rect.Location; + if (rect.Equals(oldRect)) { return; } + + selection = Option.Some(rect); + } + } + + private void TrySelectComponentsUnderCursor() + { + CircuitBoxNode? foundNode = GetTopmostNode(MouseSnapshotHandler.GetLastComponentsUnderCursor()); + + CircuitBox.SelectComponents(foundNode is null ? ImmutableArray.Empty : ImmutableArray.Create(foundNode), !PlayerInput.IsShiftDown()); + } + + private void OpenContextMenu() + { + var wireOption = MouseSnapshotHandler.FindWireUnderCursor(cursorPos); + var wireSelection = CircuitBox.Wires.Where(static w => w.IsSelectedByMe).ToImmutableArray(); + var nodeOption = GetTopmostNode(MouseSnapshotHandler.FindNodesUnderCursor(cursorPos)); + var nodeSelection = CircuitBox.Components.Where(static n => n.IsSelectedByMe).ToImmutableArray(); + + var option = new ContextMenuOption(TextManager.Get("delete"), isEnabled: wireOption.IsSome() || nodeOption is CircuitBoxComponent, () => + { + if (wireOption.TryUnwrap(out var wire)) + { + CircuitBox.RemoveWires(wire.IsSelected ? wireSelection : ImmutableArray.Create(wire)); + } + + if (nodeOption is CircuitBoxComponent node) + { + CircuitBox.RemoveComponents(node.IsSelected ? nodeSelection : ImmutableArray.Create(node)); + } + }); + + // show component name in the header to better indicate what is about to be deleted + if (nodeOption is CircuitBoxComponent comp) + { + GUIContextMenu.CreateContextMenu(PlayerInput.MousePosition, comp.Item.Name, comp.Item.Prefab.SignalComponentColor, option); + return; + } + + // also check if a wire is being deleted + if (wireOption.TryUnwrap(out var foundWire)) + { + GUIContextMenu.CreateContextMenu(PlayerInput.MousePosition, foundWire.UsedItemPrefab.Name, foundWire.Color, option); + return; + } + + GUIContextMenu.CreateContextMenu(option); + } + + public CircuitBoxNode? GetTopmostNode(ImmutableHashSet nodes) + { + CircuitBoxNode? foundNode = null; + + var allNodes = MouseSnapshotHandler.Nodes.ToImmutableArray(); + + for (int i = allNodes.Length - 1; i >= 0; i--) + { + CircuitBoxNode node = allNodes[i]; + + if (nodes.Contains(node)) + { + foundNode = node; + break; + } + } + + return foundNode; + } + + public void AddToGUIUpdateList() + { + toggleMenuButton?.AddToGUIUpdateList(); + selectedWireFrame?.AddToGUIUpdateList(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxWire.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxWire.cs new file mode 100644 index 000000000..26cfbad79 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxWire.cs @@ -0,0 +1,11 @@ +#nullable enable + +namespace Barotrauma +{ + internal partial class CircuitBoxWire + { + public CircuitBoxWireRenderer Renderer; + + public void Update() => Renderer.Recompute(From.AnchorPoint, To.AnchorPoint, Color); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxWireRenderer.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxWireRenderer.cs new file mode 100644 index 000000000..7b02ec1e2 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxWireRenderer.cs @@ -0,0 +1,271 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace Barotrauma +{ + internal class CircuitBoxWireRenderer + { + private const int VertsPerQuad = 4, // how many points per quad + QuadsPerLine = 10, // how many quads per line + VertsPerLine = QuadsPerLine * VertsPerQuad, // how many points we need to draw all the quads for a single line + TotalVertsPerWire = VertsPerLine * 2; // we are drawing 2 lines + + private readonly Texture2D texture; + + private VertexPositionColorTexture[] verts = new VertexPositionColorTexture[TotalVertsPerWire]; + private readonly Vector2[][] colliders = new Vector2[2][]; + private SquareLine skeleton; + + private Vector2 lastStart, lastEnd; + private Color lastColor; + private readonly Option wire; + + public CircuitBoxWireRenderer(Option wire, Vector2 start, Vector2 end, Color color, Sprite? wireSprite) + { + this.wire = wire; + texture = wireSprite?.Texture ?? GUI.WhiteTexture; + Recompute(start, end, color); + } + + private void UpdateColor(Color color) + { + for (int i = 0; i < TotalVertsPerWire; i++) + { + verts[i].Color = color; + } + + lastColor = color; + } + + public void Recompute(Vector2 start, Vector2 end, Color color) + { + if (MathUtils.NearlyEqual(lastStart, start) && MathUtils.NearlyEqual(lastEnd, end)) + { + if (lastColor == color) { return; } + + UpdateColor(color); + return; + } + + lastStart = start; + lastEnd = end; + lastColor = color; + + skeleton = ToolBox.GetSquareLineBetweenPoints(start, end, CircuitBoxSizes.WireKnobLength); + var points = skeleton.Points; + + Vector2 centerOfLine = (points[2] + points[3]) / 2f; + + ImmutableArray points1 = GetLinePoints(points[1], points[2], centerOfLine), + points2 = GetLinePoints(centerOfLine, points[3], points[4]); + + colliders[0] = ConstructQuads(ref verts, 0, points1, color); + colliders[1] = ConstructQuads(ref verts, VertsPerLine, points2, color); + + static ImmutableArray GetLinePoints(Vector2 start, Vector2 control, Vector2 end) + { + var points = ImmutableArray.CreateBuilder(QuadsPerLine); + for (int i = 0; i < QuadsPerLine; i++) + { + float t = (float)i / (QuadsPerLine - 1); + Vector2 pos = MathUtils.Bezier(start, control, end, t); + points.Add(pos); + } + + return points.ToImmutable(); + } + + static Vector2[] ConstructQuads(ref VertexPositionColorTexture[] verts, int startOffset, IReadOnlyList points, Color color) + { + // ok I don't know why this needs to be one quad less, maybe we are drawing with only 9 quads lol + var collider = new Vector2[VertsPerLine - VertsPerQuad]; + + int leftIndex = collider.Length - 1, + rightIndex = 0; + + // we need to calculate half of the width since the way we expand the quads from origin, otherwise the line will be twice as wide + const float halfWidth = CircuitBoxSizes.WireWidth / 2f; + + // draw the line using quads + for (int i = 0; i < points.Count - 1; i++) + { + bool isFirst = i == 0 && startOffset == 0, + isLast = i == points.Count - 2 && startOffset > 0; + + Vector2 start = points[i], + end = points[i + 1]; + + Vector2 dir = Vector2.Normalize(end - start); + Vector2 length = new Vector2(dir.Y, -dir.X) * halfWidth; + + int vertIndex = startOffset + i * 4; + + Vector2 topRight = end + length; + Vector2 topLeft = end - length; + + Vector2 bottomRight; + Vector2 bottomLeft; + + // get previous points if any + int prevIndex = vertIndex - 4; + + if ((prevIndex - startOffset) >= 0) + { + // connect the previous "upper" corners into the current "lower" corners to stitch the line together + Vector3 prevTopRight = verts[TopRight(prevIndex)].Position, + prevTopLeft = verts[TopLeft(prevIndex)].Position; + + bottomRight = ToVector2(prevTopRight); + bottomLeft = ToVector2(prevTopLeft); + } + else + { + bottomRight = start + length; + bottomLeft = start - length; + } + + if (isFirst) + { + if (MathF.Abs(dir.Y) > MathF.Abs(dir.X)) + { + float offset = dir.Y < 0 ? halfWidth : -halfWidth; + // if the line is more vertical than horizontal, we want to move the bottom corners to the left + bottomRight.Y = start.Y - offset; + bottomLeft.Y = start.Y - offset; + } + else + { + // otherwise we want to move the bottom corners to the top + bottomRight.X = start.X; + bottomLeft.X = start.X; + } + } + else if (isLast) + { + if (MathF.Abs(dir.Y) > MathF.Abs(dir.X)) + { + float offset = dir.Y < 0 ? halfWidth : -halfWidth; + // if the line is more vertical than horizontal, we want to move the bottom corners to the left + topRight.Y = end.Y + offset; + topLeft.Y = end.Y + offset; + } + else + { + // otherwise we want to move the bottom corners to the top + topRight.X = end.X; + topLeft.X = end.X; + } + } + + collider[rightIndex++] = bottomLeft; + collider[rightIndex++] = topLeft; + + collider[leftIndex--] = bottomRight; + collider[leftIndex--] = topRight; + + // adjust this if we want sprites to support sourceRects + Vector2 uvTopRight = new Vector2(0, 1), + uvTopLeft = new Vector2(0, 0), + uvBottomRight = new Vector2(1, 1), + uvBottomLeft = new Vector2(1, 0); + + SetPos(ref verts, TopRight(vertIndex), topRight, color, uvTopRight); + SetPos(ref verts, TopLeft(vertIndex), topLeft, color, uvTopLeft); + SetPos(ref verts, BottomRight(vertIndex), bottomRight, color, uvBottomRight); + SetPos(ref verts, BottomLeft(vertIndex), bottomLeft, color, uvBottomLeft); + + static void SetPos(ref VertexPositionColorTexture[] verts, int index, Vector2 pos, Color color, Vector2 uv) + { + verts[index].Position = ToVector3(pos); + verts[index].Color = color; + verts[index].TextureCoordinate = uv; + static Vector3 ToVector3(Vector2 v) => new Vector3(v.X, v.Y, 0f); + } + + static int TopRight(int vertIndex) => vertIndex; + static int TopLeft(int vertIndex) => vertIndex + 1; + static int BottomRight(int vertIndex) => vertIndex + 2; + static int BottomLeft(int vertIndex) => vertIndex + 3; + + static Vector2 ToVector2(Vector3 v) => new Vector2(v.X, v.Y); + } + + return collider; + } + } + + public bool Contains(Vector2 pos) + { + pos.Y = -pos.Y; + foreach (Vector2[] collider in colliders) + { + if (ToolBox.PointIntersectsWithPolygon(pos, collider, checkBoundingBox: false)) { return true; } + } + + return false; + } + + public void Draw(SpriteBatch spriteBatch, Color selectionColor) + { + if (GameMain.DebugDraw) + { + for (int i = 0; i < skeleton.Points.Length; i++) + { + Vector2 point = skeleton.Points[i]; + spriteBatch.DrawPoint(point, Color.White, 25f); + GUI.DrawString(spriteBatch, point - new Vector2(5f, 17f), i.ToString(), Color.Black, font: GUIStyle.LargeFont); + } + + spriteBatch.DrawLine(skeleton.Points[0], skeleton.Points[1], GUIStyle.Green, thickness: 2f); + spriteBatch.DrawLine(skeleton.Points[1], skeleton.Points[2], GUIStyle.Green, thickness: 2f); + spriteBatch.DrawLine(skeleton.Points[2], skeleton.Points[3], GUIStyle.Green, thickness: 2f); + spriteBatch.DrawLine(skeleton.Points[3], skeleton.Points[4], GUIStyle.Green, thickness: 2f); + spriteBatch.DrawLine(skeleton.Points[4], skeleton.Points[5], GUIStyle.Green, thickness: 2f); + } + + bool isSelected = wire.TryUnwrap(out var w) && w.IsSelected; + + if (isSelected) + { + foreach (var colliderPolys in colliders) + { + spriteBatch.DrawPolygon(Vector2.Zero, colliderPolys, selectionColor, 5f); + } + } + + spriteBatch.Draw(texture, verts, 0f); + + if (skeleton.Type is SquareLine.LineType.SixPointBackwardsLine) + { + // we need to expand the start and end points to make the line look like it's connected to the "smooth" part of the line + Vector2 expandedEnd = skeleton.Points[1], + expandedStart = skeleton.Points[4]; + + expandedEnd.X += CircuitBoxSizes.WireWidth / 2f; + expandedStart.X -= CircuitBoxSizes.WireWidth / 2f; + + spriteBatch.DrawLineWithTexture(texture, skeleton.Points[0], expandedEnd, lastColor, thickness: CircuitBoxSizes.WireWidth); + spriteBatch.DrawLineWithTexture(texture, expandedStart, skeleton.Points[5], lastColor, thickness: CircuitBoxSizes.WireWidth); + + const float rectSize = CircuitBoxSizes.WireWidth * 1.5f; + RectangleF startKnob = new RectangleF(skeleton.Points[1] - new Vector2(rectSize / 2f), new Vector2(rectSize)), + endKnob = new RectangleF(skeleton.Points[4] - new Vector2(rectSize / 2f), new Vector2(rectSize)); + + GUI.DrawFilledRectangle(spriteBatch, startKnob, lastColor); + GUI.DrawFilledRectangle(spriteBatch, endKnob, lastColor); + } + + if (!GameMain.DebugDraw) { return; } + + foreach (var colliderPolys in colliders) + { + spriteBatch.DrawPolygonInner(Vector2.Zero, colliderPolys, Color.Lime, 1f); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 908ce4426..d355c7cb9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -315,7 +315,7 @@ namespace Barotrauma else { var textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), listBox.Content.RectTransform), - msg.Text, font: GUIStyle.SmallFont, wrap: true) + RichString.Rich(msg.Text), font: GUIStyle.SmallFont, wrap: true) { CanBeFocused = false, TextColor = msg.Color @@ -403,7 +403,7 @@ namespace Barotrauma { if (Screen.Selected != GameMain.SubEditorScreen) return; - if (MapEntity.mapEntityList.Any(e => e is Hull || e is Gap)) + if (MapEntity.MapEntityList.Any(e => e is Hull || e is Gap)) { ShowQuestionPrompt("This submarine already has hulls and/or gaps. This command will delete them. Do you want to continue? Y/N", (option) => @@ -974,7 +974,7 @@ namespace Barotrauma if (Screen.Selected == GameMain.SubEditorScreen) { bool entityFound = false; - foreach (MapEntity entity in MapEntity.mapEntityList) + foreach (MapEntity entity in MapEntity.MapEntityList) { if (entity is Item item) { @@ -1096,19 +1096,19 @@ namespace Barotrauma commands.Add(new Command("cleansub", "", (string[] args) => { - for (int i = MapEntity.mapEntityList.Count - 1; i >= 0; i--) + for (int i = MapEntity.MapEntityList.Count - 1; i >= 0; i--) { - MapEntity me = MapEntity.mapEntityList[i]; + MapEntity me = MapEntity.MapEntityList[i]; if (me.SimPosition.Length() > 2000.0f) { NewMessage("Removed " + me.Name + " (simposition " + me.SimPosition + ")", Color.Orange); - MapEntity.mapEntityList.RemoveAt(i); + MapEntity.MapEntityList.RemoveAt(i); } else if (!me.ShouldBeSaved) { NewMessage("Removed " + me.Name + " (!ShouldBeSaved)", Color.Orange); - MapEntity.mapEntityList.RemoveAt(i); + MapEntity.MapEntityList.RemoveAt(i); } else if (me is Item) { @@ -1477,14 +1477,19 @@ namespace Barotrauma string itemNameOrId = args[0].ToLowerInvariant(); ItemPrefab itemPrefab = - (MapEntityPrefab.Find(itemNameOrId, identifier: null, showErrorMessages: false) ?? - MapEntityPrefab.Find(null, identifier: itemNameOrId.ToIdentifier(), showErrorMessages: false)) as ItemPrefab; + (MapEntityPrefab.FindByName(itemNameOrId) ?? + MapEntityPrefab.FindByIdentifier(itemNameOrId.ToIdentifier())) as ItemPrefab; if (itemPrefab == null) { NewMessage("Item not found for analyzing."); return; } + if (itemPrefab.DefaultPrice == null) + { + NewMessage($"Item \"{itemPrefab.Name}\" is not sellable/purchaseable."); + return; + } NewMessage("Analyzing item " + itemPrefab.Name + " with base cost " + itemPrefab.DefaultPrice.Price); var fabricationRecipe = fabricableItems.Find(f => f.TargetItem == itemPrefab); @@ -1847,6 +1852,12 @@ namespace Barotrauma } } + foreach (var eventPrefab in EventPrefab.Prefabs) + { + if (eventPrefab is not TraitorEventPrefab traitorEventPrefab) { continue; } + addIfMissing($"eventname.{traitorEventPrefab.Identifier}".ToIdentifier(), language); + } + foreach (Type itemComponentType in typeof(ItemComponent).Assembly.GetTypes().Where(type => type.IsSubclassOf(typeof(ItemComponent)))) { checkSerializableEntityType(itemComponentType); @@ -2346,7 +2357,20 @@ namespace Barotrauma WaterRenderer.BlurAmount = blurAmount; })); - + commands.Add(new Command("generatelevels", "generatelevels [amount]: generate a bunch of levels with the currently selected parameters in the level editor.", (string[] args) => + { + if (GameMain.GameSession == null) + { + int amount = 1; + if (args.Length > 0) { int.TryParse(args[0], out amount); } + GameMain.LevelEditorScreen.TestLevelGenerationForErrors(amount); + } + else + { + NewMessage("Can't use the command while round is running."); + } + })); + commands.Add(new Command("refreshrect", "Updates the dimensions of the selected items to match the ones defined in the prefab. Applied only in the subeditor.", (string[] args) => { //TODO: maybe do this automatically during loading when possible? @@ -2528,8 +2552,11 @@ namespace Barotrauma HashSet docs = new HashSet(); HashSet textIds = new HashSet(); + Dictionary existingTexts = new Dictionary(); + foreach (EventPrefab eventPrefab in EventSet.GetAllEventPrefabs()) { + if (eventPrefab is not TraitorEventPrefab) { continue; } if (eventPrefab.Identifier.IsEmpty) { continue; @@ -2566,35 +2593,74 @@ namespace Barotrauma 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); + string textAttribute = "text"; + XElement textElement = element; + if (text == null) + { + var subTextElement = element?.Element("Text"); + if (subTextElement != null) + { + textAttribute = "tag"; + text = subTextElement?.GetAttributeString(textAttribute, null); + textElement = subTextElement; + } + if (text == null) + { + AddWarning("Failed to find text from the element " + element.ToString()); + } } - int i = 1; + string textId = $"EventText.{parentName}"; + if (!string.IsNullOrEmpty(text) && !text.Contains("EventText.", StringComparison.OrdinalIgnoreCase)) + { + if (existingTexts.TryGetValue(text, out string existingTextId)) + { + textElement.SetAttributeValue(textAttribute, existingTextId); + } + else + { + textIds.Add(parentName); + list.Add($"<{textId}>{text}"); + existingTexts.Add(text, textId); + textElement.SetAttributeValue(textAttribute, textId); + } + } + + int conversationIndex = 1; + int objectiveIndex = 1; foreach (var subElement in element.Elements()) { + string elementName = parentName; + bool ignore = false; switch (subElement.Name.ToString().ToLowerInvariant()) { - case "conversationaction": - while (textIds.Contains(parentName+".c"+i)) + case "conversationaction": + while (textIds.Contains(elementName + ".c" + conversationIndex)) { - i++; + conversationIndex++; } - parentName += ".c" + i; + elementName += ".c" + conversationIndex; + break; + case "eventlogaction": + while (textIds.Contains(elementName + ".objective" + objectiveIndex)) + { + objectiveIndex++; + } + elementName += ".objective" + objectiveIndex; break; case "option": - while (textIds.Contains(parentName.Substring(0, parentName.Length - 3) + ".o" + i)) + while (textIds.Contains(elementName.Substring(0, elementName.Length - 3) + ".o" + conversationIndex)) { - i++; + conversationIndex++; } - parentName = parentName.Substring(0, parentName.Length - 3) + ".o" + i; + elementName = elementName.Substring(0, elementName.Length - 3) + ".o" + conversationIndex; + break; + case "text": + ignore = true; break; } - textIds.Add(parentName); - getTextsFromElement(subElement, list, parentName); + if (ignore) { continue; } + getTextsFromElement(subElement, list, elementName); } } })); @@ -2759,9 +2825,9 @@ namespace Barotrauma ep.DebugCreateInstance(); } - for (int i = 0; i < MapEntity.mapEntityList.Count; i++) + for (int i = 0; i < MapEntity.MapEntityList.Count; i++) { - var entity = MapEntity.mapEntityList[i] as ISerializableEntity; + var entity = MapEntity.MapEntityList[i] as ISerializableEntity; if (entity != null) { List<(object obj, SerializableProperty property)> allProperties = new List<(object obj, SerializableProperty property)>(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs index 0d7bb2e49..45893dc8b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs @@ -42,15 +42,15 @@ namespace Barotrauma 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); + CreateDialog(GetDisplayText(), 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) + public static void CreateDialog(LocalizedString 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, + private static void CreateDialog(LocalizedString 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); @@ -126,6 +126,7 @@ namespace Barotrauma if (actionInstance != null) { lastActiveAction = actionInstance; + actionInstance.lastActiveTime = Timing.TotalTime; actionInstance.dialogBox = messageBox; } else @@ -313,7 +314,7 @@ namespace Barotrauma }; } - private static List CreateConversation(GUIListBox parentBox, string text, Character speaker, IEnumerable options, bool drawChathead = true) + private static List CreateConversation(GUIListBox parentBox, LocalizedString text, Character speaker, IEnumerable options, bool drawChathead = true) { var content = new GUILayoutGroup(new RectTransform(Vector2.One, parentBox.Content.RectTransform), childAnchor: Anchor.TopLeft, isHorizontal: true) { @@ -322,9 +323,11 @@ namespace Barotrauma AlwaysOverrideCursor = true }; - LocalizedString translatedText = speaker?.DisplayName is not null ? - TextManager.GetWithVariable(text, "[speakername]", speaker?.DisplayName) : - TextManager.Get(text); + LocalizedString translatedText = text.Replace("\\n", "\n"); + if (speaker?.DisplayName is not null) + { + translatedText = translatedText.Replace("[speakername]", speaker.DisplayName); + } translatedText = TextManager.ParseInputTypes(translatedText).Fallback(text); if (speaker?.Info != null && drawChathead) @@ -341,7 +344,7 @@ namespace Barotrauma AbsoluteSpacing = GUI.IntScale(5) }; - var textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), textContent.RectTransform), translatedText, wrap: true) + var textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), textContent.RectTransform), RichString.Rich(translatedText), wrap: true) { AlwaysOverrideCursor = true, UserData = "text" diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/EventLogAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/EventLogAction.cs new file mode 100644 index 000000000..7be50d745 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/EventLogAction.cs @@ -0,0 +1,11 @@ +#nullable enable + +namespace Barotrauma; + +partial class EventLogAction : EventAction +{ + partial void AddEntryProjSpecific(EventLog? eventLog, string displayText) + { + eventLog?.AddEntry(ParentEvent.Prefab.Identifier, Id, displayText); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/EventObjectiveAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/EventObjectiveAction.cs new file mode 100644 index 000000000..2686bd1af --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/EventObjectiveAction.cs @@ -0,0 +1,66 @@ +#nullable enable + +namespace Barotrauma; + +partial class EventObjectiveAction : EventAction +{ + public static void Trigger( + SegmentActionType Type, + Identifier Identifier, + Identifier ObjectiveTag, + Identifier ParentObjectiveId, + Identifier TextTag, + bool CanBeCompleted, + bool autoPlayVideo = false, + string videoFile = "", + int width = 450, + int height = 80) + { + ObjectiveManager.Segment? segment = null; + // Only need to create the segment when it's being triggered (otherwise the tutorial already has the segment instance) + if (Type == SegmentActionType.Trigger) + { + segment = ObjectiveManager.Segment.CreateInfoBoxSegment(Identifier, ObjectiveTag, autoPlayVideo ? Tutorials.AutoPlayVideo.Yes : Tutorials.AutoPlayVideo.No, + new ObjectiveManager.Segment.Text(TextTag, width, height, Anchor.Center), + new ObjectiveManager.Segment.Video(videoFile, TextTag, width, height)); + } + else if (Type == SegmentActionType.Add) + { + segment = ObjectiveManager.Segment.CreateObjectiveSegment(Identifier, !ObjectiveTag.IsEmpty ? ObjectiveTag : Identifier); + } + if (segment is not null) + { + segment.CanBeCompleted = CanBeCompleted; + segment.ParentId = ParentObjectiveId; + } + switch (Type) + { + case SegmentActionType.Trigger: + case SegmentActionType.Add: + ObjectiveManager.TriggerSegment(segment); + break; + case SegmentActionType.Complete: + ObjectiveManager.CompleteSegment(Identifier); + break; + case SegmentActionType.Remove: + ObjectiveManager.RemoveSegment(Identifier); + break; + case SegmentActionType.CompleteAndRemove: + ObjectiveManager.CompleteSegment(Identifier); + ObjectiveManager.RemoveSegment(Identifier); + break; + case SegmentActionType.Fail: + ObjectiveManager.FailSegment(Identifier); + break; + case SegmentActionType.FailAndRemove: + ObjectiveManager.FailSegment(Identifier); + ObjectiveManager.RemoveSegment(Identifier); + break; + } + } + + partial void UpdateProjSpecific() + { + Trigger(Type, Identifier, ObjectiveTag, ParentObjectiveId, TextTag, CanBeCompleted, AutoPlayVideo, VideoFile, Width, Height); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/MessageBoxAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/MessageBoxAction.cs index 2cf27ab55..0929a180f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/MessageBoxAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/MessageBoxAction.cs @@ -17,7 +17,7 @@ partial class MessageBoxAction : EventAction var segment = ObjectiveManager.Segment.CreateMessageBoxSegment(id, ObjectiveTag, CreateMessageBox); segment.CanBeCompleted = ObjectiveCanBeCompleted; segment.ParentId = ParentObjectiveId; - ObjectiveManager.TriggerTutorialSegment(segment, connectObjective: Type == ActionType.ConnectObjective); + ObjectiveManager.TriggerSegment(segment, connectObjective: Type == ActionType.ConnectObjective); } } else if (Type == ActionType.Close) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialSegmentAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialSegmentAction.cs deleted file mode 100644 index 6420009e8..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialSegmentAction.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace Barotrauma; - -partial class TutorialSegmentAction : EventAction -{ - private ObjectiveManager.Segment segment; - - partial void UpdateProjSpecific() - { - // Only need to create the segment when it's being triggered (otherwise the tutorial already has the segment instance) - if (Type == SegmentActionType.Trigger) - { - segment = ObjectiveManager.Segment.CreateInfoBoxSegment(Identifier, ObjectiveTag, AutoPlayVideo ? Tutorials.AutoPlayVideo.Yes : Tutorials.AutoPlayVideo.No, - new ObjectiveManager.Segment.Text(TextTag, Width, Height, Anchor.Center), - new ObjectiveManager.Segment.Video(VideoFile, TextTag, Width, Height)); - } - else if (Type == SegmentActionType.Add) - { - segment = ObjectiveManager.Segment.CreateObjectiveSegment(Identifier, !ObjectiveTag.IsEmpty ? ObjectiveTag : Identifier); - } - if (segment is not null) - { - segment.CanBeCompleted = CanBeCompleted; - segment.ParentId = ParentObjectiveId; - } - switch (Type) - { - case SegmentActionType.Trigger: - case SegmentActionType.Add: - ObjectiveManager.TriggerTutorialSegment(segment); - break; - case SegmentActionType.Complete: - ObjectiveManager.CompleteTutorialSegment(Identifier); - break; - case SegmentActionType.Remove: - ObjectiveManager.RemoveTutorialSegment(Identifier); - break; - case SegmentActionType.CompleteAndRemove: - ObjectiveManager.CompleteTutorialSegment(Identifier); - ObjectiveManager.RemoveTutorialSegment(Identifier); - break; - } - } -} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventLog.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventLog.cs new file mode 100644 index 000000000..0db59ddea --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventLog.cs @@ -0,0 +1,61 @@ +#nullable enable + +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma; + +partial class EventLog +{ + public bool UnreadEntries { get; private set; } + + public void AddEntry(Identifier eventPrefabId, Identifier entryId, string text) + { + TryAddEntryInternal(eventPrefabId, entryId, text); + GameMain.GameSession?.EnableEventLogNotificationIcon(enabled: true); + UnreadEntries = true; + } + + public void CreateEventLogUI(GUIComponent parent, TraitorManager.TraitorResults? traitorResults = null) + { + UnreadEntries = false; + + int spacing = GUI.IntScale(5); + foreach (var ev in events.Values) + { + LocalizedString nameString = string.Empty; + int difficultyIconCount = 0; + + EventPrefab.Prefabs.TryGet(ev.EventIdentifier, out EventPrefab? eventPrefab); + if (eventPrefab is not null) + { + nameString = RichString.Rich(eventPrefab.Name); + if (eventPrefab is TraitorEventPrefab traitorEventPrefab) + { + difficultyIconCount = traitorEventPrefab.DangerLevel; + } + } + var textContent = new List(); + textContent.AddRange(ev.Entries.Select(e => (LocalizedString)e.Text)); + + var icon = GUIStyle.GetComponentStyle("TraitorMissionIcon")?.GetDefaultSprite(); + + RoundSummary.CreateMissionEntry( + parent, + nameString, + textContent, + difficultyIconCount, + icon, GUIStyle.Red, + out GUIImage missionIcon); + + if (traitorResults != null && + traitorResults.Value.TraitorEventIdentifier == ev.EventIdentifier) + { + RoundSummary.UpdateMissionStateIcon(traitorResults.Value.ObjectiveSuccessful, missionIcon); + } + } + } +} + diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs index 1615092b8..27007040d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs @@ -413,23 +413,9 @@ namespace Barotrauma 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"; - } - + string text = scriptedEvent.GetDebugInfo(); if (scriptedEvent.Targets != null) { foreach ((_, List entities) in scriptedEvent.Targets) @@ -452,10 +438,7 @@ namespace Barotrauma { debugPositions.Clear(); - string text = $"Finished: {artifactEvent.IsFinished.ColorizeObject()}\n" + - $"Item: {artifactEvent.Item.ColorizeObject()}\n" + - $"Spawn pending: {artifactEvent.SpawnPending.ColorizeObject()}\n" + - $"Spawn position: {artifactEvent.SpawnPos.ColorizeObject()}\n"; + string text = artifactEvent.GetDebugInfo(); if (artifactEvent.Item != null && !artifactEvent.Item.Removed) { @@ -470,10 +453,7 @@ namespace Barotrauma { debugPositions.Clear(); - 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"; + string text = monsterEvent.GetDebugInfo(); if (monsterEvent.SpawnPos != null && Submarine.MainSub != null) { @@ -712,7 +692,30 @@ namespace Barotrauma } } break; + case NetworkEventType.EVENTLOG: + ClientReadEventLog(GameMain.Client, msg); + break; + case NetworkEventType.EVENTOBJECTIVE: + ClientReadEventObjective(GameMain.Client, msg); + break; } } + + private void ClientReadEventLog(GameClient client, IReadMessage msg) + { + NetEventLogEntry entry = INetSerializableStruct.Read(msg); + EventLog.AddEntry(entry.EventPrefabId, entry.LogEntryId, entry.Text.Replace("\\n", "\n")); + } + private static void ClientReadEventObjective(GameClient client, IReadMessage msg) + { + NetEventObjective entry = INetSerializableStruct.Read(msg); + EventObjectiveAction.Trigger( + entry.Type, + entry.Identifier, + entry.ObjectiveTag, + entry.ParentObjectiveId, + entry.TextTag, + entry.CanBeCompleted); + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs index 41a7758b3..f0e471346 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using Barotrauma.Networking; using System.Collections.Generic; @@ -9,22 +10,37 @@ namespace Barotrauma public override bool DisplayAsCompleted => false; public override bool DisplayAsFailed => false; + public override int State + { + get => base.State; + set + { + base.State = value; + if (base.State > 0) + { + caves.ForEach(c => c.MissionsToDisplayOnSonar.Remove(this)); + } + } + } + public override void ClientReadInitial(IReadMessage msg) { base.ClientReadInitial(msg); byte caveCount = msg.ReadByte(); for (int i = 0; i < caveCount; i++) { - byte selectedCave = msg.ReadByte(); - if (selectedCave < 255 && Level.Loaded != null) + byte selectedCaveIndex = msg.ReadByte(); + if (selectedCaveIndex < 255 && Level.Loaded != null) { - if (selectedCave < Level.Loaded.Caves.Count) + if (selectedCaveIndex < Level.Loaded.Caves.Count) { - Level.Loaded.Caves[selectedCave].DisplayOnSonar = true; + var selectedCave = Level.Loaded.Caves[selectedCaveIndex]; + selectedCave.MissionsToDisplayOnSonar.Add(this); + caves.Add(selectedCave); } else { - DebugConsole.ThrowError($"Cave index out of bounds when reading nest mission data. Index: {selectedCave}, number of caves: {Level.Loaded.Caves.Count}"); + DebugConsole.ThrowError($"Cave index out of bounds when reading nest mission data. Index: {selectedCaveIndex}, number of caves: {Level.Loaded.Caves.Count}"); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs index 4a0e51172..fdf6ccfa8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using static Barotrauma.MissionPrefab; namespace Barotrauma { @@ -27,7 +28,11 @@ namespace Barotrauma public Color GetDifficultyColor() { - int v = Difficulty ?? MissionPrefab.MinDifficulty; + return GetDifficultyColor(Difficulty ?? MissionPrefab.MinDifficulty); + } + public static Color GetDifficultyColor(int difficulty) + { + int v = difficulty; float t = MathUtils.InverseLerp(MissionPrefab.MinDifficulty, MissionPrefab.MaxDifficulty, v); return ToolBox.GradientLerp(t, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); } @@ -61,36 +66,48 @@ namespace Barotrauma List reputationRewardTexts = new List(); foreach (var reputationReward in ReputationRewards) { - FactionPrefab targetFactionPrefab; - if (reputationReward.Key == "location" ) + FactionPrefab factionPrefab; + if (reputationReward.FactionIdentifier == "location" ) { - targetFactionPrefab = OriginLocation.Faction?.Prefab; + factionPrefab = OriginLocation.Faction?.Prefab; } else { - FactionPrefab.Prefabs.TryGet(reputationReward.Key, out targetFactionPrefab); - } - - if (targetFactionPrefab == null) - { - return string.Empty; + FactionPrefab.Prefabs.TryGet(reputationReward.FactionIdentifier, out factionPrefab); } - float totalReputationChange = reputationReward.Value; - if (GameMain.GameSession?.Campaign?.Factions.Find(f => f.Prefab == targetFactionPrefab) is Faction faction) + if (factionPrefab != null) { - totalReputationChange = reputationReward.Value * faction.Reputation.GetReputationChangeMultiplier(reputationReward.Value); + AddReputationText(factionPrefab, reputationReward.Amount); + if (!MathUtils.NearlyEqual(reputationReward.AmountForOpposingFaction, 0.0f) && + FactionPrefab.Prefabs.TryGet(factionPrefab.OpposingFaction, out var opposingFactionPrefab)) + { + AddReputationText(opposingFactionPrefab, reputationReward.AmountForOpposingFaction); + } + } + } + + void AddReputationText(FactionPrefab factionPrefab, float amount) + { + if (factionPrefab == null) { return; } + + float totalReputationChange = amount; + if (GameMain.GameSession?.Campaign?.Factions.Find(f => f.Prefab == factionPrefab) is Faction faction) + { + totalReputationChange = amount * faction.Reputation.GetReputationChangeMultiplier(amount); } - LocalizedString name = $"‖color:{XMLExtensions.ToStringHex(targetFactionPrefab.IconColor)}‖{targetFactionPrefab.Name}‖end‖"; + LocalizedString name = $"‖color:{XMLExtensions.ToStringHex(factionPrefab.IconColor)}‖{factionPrefab.Name}‖end‖"; float normalizedValue = MathUtils.InverseLerp(-100.0f, 100.0f, totalReputationChange); string formattedValue = ((int)Math.Round(totalReputationChange)).ToString("+#;-#;0"); //force plus sign for positive numbers LocalizedString rewardText = TextManager.GetWithVariables( "reputationformat", ("[reputationname]", name), - ("[reputationvalue]", $"‖color:{XMLExtensions.ToStringHex(Reputation.GetReputationColor(normalizedValue))}‖{formattedValue}‖end‖" )); + ("[reputationvalue]", $"‖color:{XMLExtensions.ToStringHex(Reputation.GetReputationColor(normalizedValue))}‖{formattedValue}‖end‖")); reputationRewardTexts.Add(rewardText.Value); } + + if (reputationRewardTexts.Any()) { return RichString.Rich(TextManager.AddPunctuation(':', TextManager.Get("reputation"), LocalizedString.Join(", ", reputationRewardTexts))); @@ -100,6 +117,20 @@ namespace Barotrauma return string.Empty; } } + partial void DistributeExperienceToCrew(IEnumerable crew, int experienceGain) + { + foreach (Character character in crew) + { + GiveMissionExperience(character.Info); + } + void GiveMissionExperience(CharacterInfo info) + { + if (info == null) { return; } + var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f); + info.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual); + info.GiveExperience((int)(experienceGain * experienceGainMultiplierIndividual.Value)); + } + } partial void ShowMessageProjSpecific(int missionState) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/NestMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/NestMission.cs index f9538a0a3..ff85bdc8d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/NestMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/NestMission.cs @@ -9,6 +9,19 @@ namespace Barotrauma public override bool DisplayAsCompleted => State > 0 && !requireDelivery; public override bool DisplayAsFailed => false; + public override int State + { + get => base.State; + set + { + base.State = value; + if (base.State > 0 && selectedCave != null) + { + selectedCave.MissionsToDisplayOnSonar.Remove(this); + } + } + } + public override void ClientReadInitial(IReadMessage msg) { base.ClientReadInitial(msg); @@ -20,8 +33,9 @@ namespace Barotrauma { if (selectedCaveIndex < Level.Loaded.Caves.Count) { - Level.Loaded.Caves[selectedCaveIndex].DisplayOnSonar = true; - SpawnNestObjects(Level.Loaded, Level.Loaded.Caves[selectedCaveIndex]); + selectedCave = Level.Loaded.Caves[selectedCaveIndex]; + selectedCave.MissionsToDisplayOnSonar.Add(this); + SpawnNestObjects(Level.Loaded, selectedCave); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs index 15860e012..4afb04ff2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs @@ -99,10 +99,10 @@ namespace Barotrauma })) .Aggregate(TextManager.SpeciallyHandledCharCategory.None, (current, category) => current | category); - public ScalableFont(ContentXElement element, GraphicsDevice gd = null) + public ScalableFont(ContentXElement element, uint defaultSize = 14, GraphicsDevice gd = null) : this( element.GetAttributeContentPath("file")?.Value, - (uint)element.GetAttributeInt("size", 14), + (uint)element.GetAttributeInt("size", (int)defaultSize), gd, element.GetAttributeBool("dynamicloading", false), ExtractShccFromXElement(element)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs index 2456e2ba8..7f185b4d4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs @@ -38,6 +38,7 @@ namespace Barotrauma private float prevUIScale; private readonly GUIFrame channelSettingsFrame; + private readonly GUITextBlock radioJammedWarning; private readonly GUITextBox channelText; private readonly GUILayoutGroup channelPickerContent; private readonly GUIButton memButton; @@ -107,6 +108,13 @@ namespace Barotrauma RelativeSpacing = 0.01f }; + radioJammedWarning = new GUITextBlock(new RectTransform(Vector2.One, channelSettingsFrame.RectTransform), TextManager.Get("radiojammedwarning"), + textColor: GUIStyle.Orange, color: Color.Black, + textAlignment: Alignment.Center, style: "OuterGlow") + { + ToolTip = TextManager.Get("hint.radiojammed") + }; + var buttonLeft = new GUIButton(new RectTransform(new Vector2(0.1f, 0.8f), channelSettingsContent.RectTransform), style: "DeviceButton") { PlaySoundOnSelect = false, @@ -643,7 +651,7 @@ namespace Barotrauma ToggleButton.RectTransform.AbsoluteOffset = new Point(GUIFrame.Rect.Right, GUIFrame.Rect.Y + HUDLayoutSettings.ChatBoxArea.Height - ToggleButton.Rect.Height); } - if (Character.Controlled != null && ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent radio)) + if (Character.Controlled != null && ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent radio, ignoreJamming: true)) { if (prevRadio != radio) { @@ -672,10 +680,11 @@ namespace Barotrauma } } channelSettingsFrame.Visible = true; + radioJammedWarning.Visible = radio is { JamTimer: > 0 }; } else { - channelSettingsFrame.Visible = false; + radioJammedWarning.Visible = channelSettingsFrame.Visible = false; channelPickerContent.Children.First().CanBeFocused = true; channelMemPending = false; memButton.Enabled = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs index f460ff480..c04c21183 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs @@ -756,7 +756,7 @@ namespace Barotrauma private bool CreateRenamingComponent(GUIButton button, object userData) { - if (!HasPermission || !(userData is CharacterInfo characterInfo)) { return false; } + if (!HasPermission || userData is not CharacterInfo characterInfo) { return false; } var outerGlowFrame = new GUIFrame(new RectTransform(new Vector2(1.25f, 1.25f), parentComponent.RectTransform, Anchor.Center), style: "OuterGlow", color: Color.Black * 0.7f); var frame = new GUIFrame(new RectTransform(new Vector2(0.33f, 0.4f), outerGlowFrame.RectTransform, anchor: Anchor.Center) @@ -843,7 +843,7 @@ namespace Barotrauma private bool FireCharacter(GUIButton button, object selection) { - if (!(selection is CharacterInfo characterInfo)) { return false; } + if (selection is not CharacterInfo characterInfo) { return false; } campaign.CrewManager.FireCharacter(characterInfo); SelectCharacter(null, null, null); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs index 373fecbbf..d0b10b1e3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs @@ -161,7 +161,7 @@ namespace Barotrauma return 1; } - return string.Compare(file1, file2); + return string.Compare(file1, file2, StringComparison.OrdinalIgnoreCase); } private static void InitIfNecessary() @@ -419,6 +419,8 @@ namespace Barotrauma }; } + fileList.Content.RectTransform.SortChildren(SortFiles); + directoryBox!.Text = currentDirectory; fileBox!.Text = ""; fileList.Deselect(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index 838c2ee27..3a0bff6d9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -111,7 +111,7 @@ namespace Barotrauma /// public static float AspectRatioAdjustment => HorizontalAspectRatio < 1.4f ? (1.0f - (1.4f - HorizontalAspectRatio)) : 1.0f; - public static bool IsUltrawide => HorizontalAspectRatio > 2.0f; + public static bool IsUltrawide => HorizontalAspectRatio > 2.3f; public static int UIWidth { @@ -1010,16 +1010,13 @@ namespace Barotrauma // Sub editor drag and highlight case SubEditorScreen editor: { - foreach (var mapEntity in MapEntity.mapEntityList) + if (MapEntity.StartMovingPos != Vector2.Zero || MapEntity.Resizing) { - if (MapEntity.StartMovingPos != Vector2.Zero) - { - return CursorState.Dragging; - } - if (mapEntity.IsHighlighted) - { - return CursorState.Hand; - } + return CursorState.Dragging; + } + if (MapEntity.HighlightedEntities.Any(h => !h.IsSelected)) + { + return CursorState.Hand; } break; } @@ -2438,13 +2435,14 @@ namespace Barotrauma var pauseMenuInner = new GUIFrame(new RectTransform(new Vector2(0.13f, 0.3f), PauseMenu.RectTransform, Anchor.Center) { MinSize = new Point(250, 300) }); - var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 0.6f), pauseMenuInner.RectTransform, Anchor.Center)) + float padding = 0.06f; + + var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 0.8f), pauseMenuInner.RectTransform, Anchor.BottomCenter) { RelativeOffset = new Vector2(0.0f, padding) }) { - Stretch = true, - RelativeSpacing = 0.05f + AbsoluteSpacing = IntScale(15) }; - new GUIButton(new RectTransform(new Vector2(0.1f, 0.1f), pauseMenuInner.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point((int)(15 * GUI.Scale)) }, + new GUIButton(new RectTransform(new Vector2(0.1f, 0.07f), pauseMenuInner.RectTransform, Anchor.TopRight) { RelativeOffset = new Vector2(padding) }, "", style: "GUIBugButton") { IgnoreLayoutGroups = true, @@ -2520,6 +2518,13 @@ namespace Barotrauma } GUITextBlock.AutoScaleAndNormalize(buttonContainer.Children.Where(c => c is GUIButton).Select(c => ((GUIButton)c).TextBlock)); + //scale to ensure there's enough room for all the buttons + pauseMenuInner.RectTransform.MinSize = new Point( + pauseMenuInner.RectTransform.MinSize.X, + Math.Max( + (int)(buttonContainer.Children.Sum(c => c.Rect.Height + buttonContainer.AbsoluteSpacing) / buttonContainer.RectTransform.RelativeSize.Y), + pauseMenuInner.RectTransform.MinSize.X)); + } void CreateButton(string textTag, GUIComponent parent, Action action, string verificationTextTag = null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index b6460f83f..e3ea42eaa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -781,7 +781,7 @@ namespace Barotrauma if (toolTipBlock.Rect.Right > GameMain.GraphicsWidth - 10) { - toolTipBlock.RectTransform.AbsoluteOffset -= new Point(toolTipBlock.Rect.Width, 0); + toolTipBlock.RectTransform.AbsoluteOffset -= new Point(toolTipBlock.Rect.Width + targetElement.Width, 0); } if (toolTipBlock.Rect.Bottom > GameMain.GraphicsHeight - 10) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessage.cs index 2d549b630..72a349ae2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessage.cs @@ -49,7 +49,7 @@ namespace Barotrauma get { return lifeTime; } } - public ScalableFont Font + public GUIFont Font { get; private set; @@ -69,7 +69,7 @@ namespace Barotrauma } } - public GUIMessage(string text, Color color, float lifeTime, ScalableFont font = null) + public GUIMessage(string text, Color color, float lifeTime, GUIFont font = null) { coloredText = new ColoredText(text, color, false, false); this.lifeTime = lifeTime; @@ -81,7 +81,7 @@ namespace Barotrauma Font = font; } - public GUIMessage(string text, Color color, Vector2 position, Vector2 velocity, float lifeTime, Alignment textAlignment = Alignment.Center, ScalableFont font = null, Submarine sub = null) + public GUIMessage(string text, Color color, Vector2 position, Vector2 velocity, float lifeTime, Alignment textAlignment = Alignment.Center, GUIFont font = null, Submarine sub = null) { coloredText = new ColoredText(text, color, false, false); WorldSpace = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs index 4d3f69ea8..c5e6fda31 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs @@ -693,5 +693,52 @@ namespace Barotrauma rectT.Parent = RectTransform; Buttons.Add(new GUIButton(rectT, text) { OnClicked = onClick }); } + + public static GUIMessageBox CreateLoadingBox(LocalizedString text, (LocalizedString Label, Action Action)[] buttons = null, Vector2? relativeSize = null) + { + buttons ??= Array.Empty<(LocalizedString Label, Action Action)>(); + var relativeSizeFallback = relativeSize ?? (0.7f, 0.5f); + var newMessageBox = new GUIMessageBox( + headerText: "", + text: "", + relativeSize: relativeSizeFallback, + buttons: buttons.Select(b => b.Label).ToArray()); + newMessageBox.InnerFrame.RectTransform.ScaleBasis = ScaleBasis.BothHeight; + + for (int i = 0; i < buttons.Length; i++) + { + var capturedIndex = i; + newMessageBox.Buttons[i].OnClicked = (_, _) => + { + buttons[capturedIndex].Action(newMessageBox); + return false; + }; + } + + const float throbberSize = 0.25f; + + new GUITextBlock( + new RectTransform((0.9f, 0f), newMessageBox.InnerFrame.RectTransform, Anchor.Center, Pivot.BottomCenter) { RelativeOffset = (0f, -throbberSize * 0.5f) }, + text: text, textAlignment: Alignment.Center, wrap: true); + + // Throbber + new GUICustomComponent( + new RectTransform(Vector2.One * throbberSize, newMessageBox.InnerFrame.RectTransform, Anchor.Center, scaleBasis: ScaleBasis.BothHeight), + onDraw: static (sb, component) => + { + GUIStyle.GenericThrobber.Draw( + sb, + spriteIndex: (int)(Timing.TotalTime * 20f) % GUIStyle.GenericThrobber.FrameCount, + pos: component.Rect.Center.ToVector2(), + color: Color.White, + origin: GUIStyle.GenericThrobber.FrameSize.ToVector2() * 0.5f, + rotate: 0f, + scale: component.Rect.Size.ToVector2() / GUIStyle.GenericThrobber.FrameSize.ToVector2()); + }); + + MessageBoxes.Remove(newMessageBox); + MessageBoxes.Insert(0, newMessageBox); + return newMessageBox; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs index ee15c0c06..8d77932bc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs @@ -132,7 +132,7 @@ namespace Barotrauma if (subElement.NameAsIdentifier() != "override") { continue; } if (ScalableFont.ExtractShccFromXElement(subElement).HasFlag(flag)) { - return new ScalableFont(subElement, GameMain.Instance.GraphicsDevice); + return new ScalableFont(subElement, font.Size, GameMain.Instance.GraphicsDevice); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScissorComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScissorComponent.cs index 3969a3db3..459447cd1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScissorComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScissorComponent.cs @@ -4,7 +4,7 @@ using Microsoft.Xna.Framework.Graphics; namespace Barotrauma { - public class GUIScissorComponent: GUIComponent + public sealed class GUIScissorComponent : GUIComponent { public GUIComponent Content; @@ -14,19 +14,14 @@ namespace Barotrauma { CanBeFocused = false }; + + rectT.ChildrenChanged += CheckForChildren; } - protected override void Update(float deltaTime) + private void CheckForChildren(RectTransform rectT) { - base.Update(deltaTime); - - foreach (GUIComponent child in Children) - { - if (child == Content) { continue; } - throw new InvalidOperationException($"Children were found in {nameof(GUIScissorComponent)}, Add them to {nameof(GUIScissorComponent)}.{nameof(Content)} instead."); - } - - ClampChildMouseRects(Content); + if (rectT == Content.RectTransform) { return; } + throw new InvalidOperationException($"Children were found in {nameof(GUIScissorComponent)}, Add them to {nameof(GUIScissorComponent)}.{nameof(Content)} instead."); } public override void DrawChildren(SpriteBatch spriteBatch, bool recursive) @@ -57,7 +52,13 @@ namespace Barotrauma spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: prevRasterizerState); } - private void ClampChildMouseRects(GUIComponent child) + protected override void Update(float deltaTime) + { + base.Update(deltaTime); + ClampChildMouseRects(Content); + } + + private static void ClampChildMouseRects(GUIComponent child) { child.ClampMouseRectToParent = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs index e3ad34ad7..05d7b129c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs @@ -142,11 +142,13 @@ namespace Barotrauma public readonly static GUIColor HealthBarColorHigh = new GUIColor("HealthBarColorHigh"); public readonly static GUIColor HealthBarColorPoisoned = new GUIColor("HealthBarColorPoisoned"); + private readonly static Point defaultItemFrameMargin = new Point(50, 56); + public static Point ItemFrameMargin { get { - Point size = new Point(50, 56).Multiply(GUI.SlicedSpriteScale); + Point size = defaultItemFrameMargin.Multiply(GUI.SlicedSpriteScale); var style = GetComponentStyle("ItemUI"); var sprite = style?.Sprites[GUIComponent.ComponentState.None].First(); @@ -159,6 +161,16 @@ namespace Barotrauma } } + public static int ItemFrameTopBarHeight + { + get + { + var style = GetComponentStyle("ItemUI"); + var sprite = style?.Sprites[GUIComponent.ComponentState.None].First(); + return (int)Math.Min(sprite?.Slices[0].Height ?? 0, defaultItemFrameMargin.Y / 2 * GUI.SlicedSpriteScale); + } + } + public static Point ItemFrameOffset => new Point(0, 3).Multiply(GUI.SlicedSpriteScale); public static GUIComponentStyle GetComponentStyle(string styleName) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs index 8ab3992d5..cc7359b59 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs @@ -12,8 +12,6 @@ namespace Barotrauma public delegate bool OnSelectedHandler(GUITickBox obj); public OnSelectedHandler OnSelected; - public static int size = 20; - private GUIRadioButtonGroup radioButtonGroup; public override bool Selected @@ -21,21 +19,7 @@ namespace Barotrauma get { return isSelected; } set { - if (value == isSelected) { return; } - if (radioButtonGroup != null && radioButtonGroup.SelectedRadioButton == this) - { - isSelected = true; - return; - } - - isSelected = value; - State = isSelected ? ComponentState.Selected : ComponentState.None; - if (value && radioButtonGroup != null) - { - radioButtonGroup.SelectRadioButton(this); - } - - OnSelected?.Invoke(this); + SetSelected(value, callOnSelected: true); } } @@ -186,6 +170,27 @@ namespace Barotrauma text.SetTextPos(); ContentWidth = box.Rect.Width + text.Padding.X + text.TextSize.X + text.Padding.Z; } + + public void SetSelected(bool selected, bool callOnSelected = true) + { + if (selected == isSelected) { return; } + if (radioButtonGroup != null && radioButtonGroup.SelectedRadioButton == this) + { + isSelected = true; + return; + } + + isSelected = selected; + State = isSelected ? ComponentState.Selected : ComponentState.None; + if (selected && radioButtonGroup != null) + { + radioButtonGroup.SelectRadioButton(this); + } + if (callOnSelected) + { + OnSelected?.Invoke(this); + } + } protected override void Update(float deltaTime) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs index 8442731b2..f6bb9831e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs @@ -160,8 +160,9 @@ namespace Barotrauma int crewAreaY = ButtonAreaTop.Bottom + Padding; int crewAreaHeight = ObjectiveAnchor.Top - Padding - crewAreaY; - float crewAreaWidthMultiplier = GUI.IsUltrawide ? GUI.HorizontalAspectRatio : 1.0f; - CrewArea = new Rectangle(Padding, crewAreaY, (int)(Math.Max(400 * GUI.Scale, 220) * crewAreaWidthMultiplier), crewAreaHeight); + CrewArea = new Rectangle(Padding, crewAreaY, + (int)MathHelper.Clamp(400 * GUI.Scale, 220, GameMain.GraphicsHeight * 0.4f), + crewAreaHeight); InventoryAreaLower = new Rectangle(ChatBoxArea.Right + Padding * 7, inventoryTopY, GameMain.GraphicsWidth - Padding * 9 - ChatBoxArea.Width, GameMain.GraphicsHeight - inventoryTopY); int healthWindowWidth = (int)(GameMain.GraphicsWidth * 0.5f); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs index 0c5c6cc17..52749f42b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs @@ -4,12 +4,13 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; namespace Barotrauma { - class LoadingScreen + sealed class LoadingScreen { private readonly Sprite defaultBackgroundTexture, overlay; private readonly SpriteSheet decorativeGraph, decorativeMap; @@ -37,36 +38,16 @@ namespace Barotrauma } } - private Queue pendingSplashScreens = new Queue(); /// /// Triplet.first = filepath, Triplet.second = resolution, Triplet.third = audio gain /// - public Queue PendingSplashScreens - { - get - { - lock (loadMutex) - { - return pendingSplashScreens; - } - } - set - { - lock (loadMutex) - { - pendingSplashScreens = value; - } - } - } + public readonly ConcurrentQueue PendingSplashScreens = new ConcurrentQueue(); public bool PlayingSplashScreen { get { - lock (loadMutex) - { - return currSplashScreen != null || pendingSplashScreens.Count > 0; - } + return currSplashScreen != null || PendingSplashScreens.Count > 0; } } @@ -76,33 +57,7 @@ namespace Barotrauma selectedTip = RichString.Rich(tip); } - private readonly object loadMutex = new object(); - private float? loadState; - - public float? LoadState - { - get - { - lock (loadMutex) - { - return loadState; - } - } - set - { - lock (loadMutex) - { - loadState = value; - DrawLoadingText = true; - } - } - } - - public bool DrawLoadingText - { - get; - set; - } + public float LoadState; public bool WaitForLanguageSelection { @@ -121,7 +76,7 @@ namespace Barotrauma overlay = new Sprite("Content/UI/MainMenuVignette.png", Vector2.Zero); noiseSprite = new Sprite("Content/UI/noise.png", Vector2.Zero); - DrawLoadingText = true; + SetSelectedTip(TextManager.Get("LoadingScreenTip")); } @@ -170,10 +125,11 @@ namespace Barotrauma { DrawLanguageSelectionPrompt(spriteBatch, graphics); } - else if (DrawLoadingText) + else { LocalizedString loadText; - if (LoadState == 100.0f) + var loadState = LoadState; // avoid multiple reads here to prevent jank + if (loadState >= 100.0f) { #if DEBUG if (GameSettings.CurrentConfig.AutomaticQuickStartEnabled || GameSettings.CurrentConfig.AutomaticCampaignLoadEnabled || (GameSettings.CurrentConfig.TestScreenEnabled && GameMain.FirstLoad)) @@ -191,17 +147,17 @@ namespace Barotrauma else { loadText = TextManager.Get("Loading"); - if (LoadState != null) + if (loadState >= 0f) { - loadText += " " + (int)LoadState + " %"; + loadText += $" {loadState:N0} %"; + } #if DEBUG - if (GameMain.FirstLoad && GameMain.CancelQuickStart) - { - loadText += " (Quickstart aborted)"; - } -#endif + if (GameMain.FirstLoad && GameMain.CancelQuickStart) + { + loadText += " (Quickstart aborted)"; } +#endif } if (GUIStyle.LargeFont.HasValue) @@ -343,11 +299,9 @@ namespace Barotrauma private void DrawSplashScreen(SpriteBatch spriteBatch, GraphicsDevice graphics) { - if (currSplashScreen == null && PendingSplashScreens.Count == 0) { return; } - if (currSplashScreen == null) { - var newSplashScreen = PendingSplashScreens.Dequeue(); + if (!PendingSplashScreens.TryDequeue(out var newSplashScreen)) { return; } string fileName = newSplashScreen.Filename; try { @@ -362,10 +316,10 @@ namespace Barotrauma PendingSplashScreens.Clear(); currSplashScreen = null; } - - if (currSplashScreen == null) { return; } } + if (currSplashScreen == null) { return; } + if (currSplashScreen.IsPlaying) { graphics.Clear(Color.Black); @@ -425,7 +379,7 @@ namespace Barotrauma public IEnumerable DoLoading(IEnumerable loader) { drawn = false; - LoadState = null; + LoadState = -1f; SetSelectedTip(TextManager.Get("LoadingScreenTip")); currentBackgroundTexture = LocationType.Prefabs.Where(p => p.UsePortraitInRandomLoadingScreens).GetRandomUnsynced()?.GetPortrait(Rand.Int(int.MaxValue)); if (GameMain.GameSession?.GameMode?.Missions is { } missions && missions.Any(m => m.Prefab.HasPortraits)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs index e19738609..85fe74086 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs @@ -946,7 +946,7 @@ namespace Barotrauma } if (truncated) { - descriptionBlock.Text += "..."; + descriptionBlock.Text += TextManager.Get("ellipsis"); } GUITextBlock priceBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.25f), bottomTextLayout.RectTransform), TextManager.FormatCurrency(affliction.Price), font: GUIStyle.SubHeadingFont); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs index 2e5826656..e7902d5ca 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs @@ -306,6 +306,8 @@ namespace Barotrauma { _scaleBasis = value; RecalculateAbsoluteSize(); + RecalculateAnchorPoint(); + RecalculatePivotOffset(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ShapeExtensions.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ShapeExtensions.cs index 3648f41af..71461fd0b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ShapeExtensions.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ShapeExtensions.cs @@ -91,8 +91,8 @@ namespace Barotrauma var angle = (float)Math.Atan2(point2.Y - point1.Y, point2.X - point1.X); var scale = new Vector2(length, thickness); Vector2 middle = new Vector2((point1.X + point2.X) / 2f, (point1.Y + point2.Y) / 2f); - Texture2D tex = GetTexture(spriteBatch); - spriteBatch.Draw(GetTexture(spriteBatch), middle, null, color, angle, new Vector2(tex.Width / 2f, tex.Height / 2f), scale, SpriteEffects.None, 0); + Texture2D tex = GUI.WhiteTexture; + spriteBatch.Draw(tex, middle, null, color, angle, new Vector2(tex.Width / 2f, tex.Height / 2f), scale, SpriteEffects.None, 0); } private static void DrawPolygonEdge(SpriteBatch spriteBatch, Vector2 point1, Vector2 point2, Color color, float thickness) @@ -112,6 +112,18 @@ namespace Barotrauma DrawLine(spriteBatch, new Vector2(x1, y1), new Vector2(x2, y2), color, thickness); } + public static void DrawLineWithTexture(this SpriteBatch spriteBatch, Texture2D tex, Vector2 point1, Vector2 point2, + Color color, float thickness = 1f) + { + // calculate the distance between the two vectors + var distance = Vector2.Distance(point1, point2); + + // calculate the angle between the two vectors + var angle = (float)Math.Atan2(point2.Y - point1.Y, point2.X - point1.X); + + DrawLine(spriteBatch, tex, point1, distance, angle, color, thickness); + } + /// /// Draws a line from point1 to point2 with an offset /// @@ -124,18 +136,18 @@ namespace Barotrauma // calculate the angle between the two vectors var angle = (float)Math.Atan2(point2.Y - point1.Y, point2.X - point1.X); - DrawLine(spriteBatch, point1, distance, angle, color, thickness); + DrawLine(spriteBatch, GetTexture(spriteBatch), point1, distance, angle, color, thickness); } /// /// Draws a line from point1 to point2 with an offset /// - public static void DrawLine(this SpriteBatch spriteBatch, Vector2 point, float length, float angle, Color color, + public static void DrawLine(this SpriteBatch spriteBatch, Texture2D tex, Vector2 point, float length, float angle, Color color, float thickness = 1f) { - var origin = new Vector2(0f, 0.5f); - var scale = new Vector2(length, thickness); - spriteBatch.Draw(GetTexture(spriteBatch), point, null, color, angle, origin, scale, SpriteEffects.None, 0); + var origin = new Vector2(0f, tex.Height / 2f); + var scale = new Vector2(length / tex.Width, thickness / tex.Height); + spriteBatch.Draw(tex, point, null, color, angle, origin, scale, SpriteEffects.None, 0); } /// diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 6b8ccfbb8..24362e63c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -19,7 +19,7 @@ namespace Barotrauma private static UISprite spectateIcon, disconnectedIcon; private static Sprite ownerIcon, moderatorIcon; - public enum InfoFrameTab { Crew, Mission, Reputation, Traitor, Submarine, Talents }; + public enum InfoFrameTab { Crew, Mission, Reputation, Submarine, Talents }; public static InfoFrameTab SelectedTab { get; private set; } private GUIFrame infoFrame, contentFrame; @@ -299,9 +299,15 @@ namespace Barotrauma var crewButton = createTabButton(InfoFrameTab.Crew, "crew"); - if (!(GameMain.GameSession?.GameMode is TestGameMode)) + if (GameMain.GameSession?.GameMode is not TestGameMode) { - createTabButton(InfoFrameTab.Mission, "mission"); + var missionBtn = createTabButton(InfoFrameTab.Mission, "mission"); + eventLogNotification = GameSession.CreateNotificationIcon(missionBtn); + eventLogNotification.Visible = GameMain.GameSession.EventManager?.EventLog?.UnreadEntries ?? false; + if (eventLogNotification.Visible) + { + eventLogNotification.Pulsate(Vector2.One, Vector2.One * 2, 1.0f); + } } if (GameMain.GameSession?.GameMode is CampaignMode campaignMode) @@ -340,14 +346,6 @@ namespace Barotrauma text.Text = TextManager.GetWithVariable("bankbalanceformat", "[money]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", balance)); } } - else - { - bool isTraitor = GameMain.Client?.Character?.IsTraitor ?? false; - if (isTraitor && GameMain.Client.TraitorMission != null) - { - var traitorButton = createTabButton(InfoFrameTab.Traitor, "tabmenu.traitor"); - } - } var submarineButton = createTabButton(InfoFrameTab.Submarine, "submarine"); @@ -361,7 +359,7 @@ namespace Barotrauma } }; - talentPointNotification = GameSession.CreateTalentIconNotification(talentsButton); + talentPointNotification = GameSession.CreateNotificationIcon(talentsButton); } public void SelectInfoFrameTab(InfoFrameTab selectedTab) @@ -387,12 +385,6 @@ namespace Barotrauma GameMain.GameSession.RoundSummary.CreateReputationInfoPanel(reputationFrame, campaignMode); } break; - case InfoFrameTab.Traitor: - TraitorMissionPrefab traitorMission = GameMain.Client?.TraitorMission; - Character traitor = GameMain.Client?.Character; - if (traitor == null || traitorMission == null) { return; } - CreateTraitorInfo(infoFrameHolder, traitorMission, traitor); - break; case InfoFrameTab.Submarine: CreateSubmarineInfo(infoFrameHolder, Submarine.MainSub); break; @@ -1539,96 +1531,44 @@ namespace Barotrauma int locationInfoYOffset = locationInfoContainer.Rect.Height + padding * 2; - GUIListBox missionList = new GUIListBox(new RectTransform(new Point(contentWidth, missionFrameContent.Rect.Height - locationInfoYOffset), missionFrameContent.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, locationInfoYOffset) }); missionList.ContentBackground.Color = Color.Transparent; missionList.Spacing = GUI.IntScale(15); if (GameMain.GameSession?.Missions != null) { - int spacing = GUI.IntScale(5); - int iconSize = (int)(GUIStyle.LargeFont.MeasureChar('T').Y + GUIStyle.Font.MeasureChar('T').Y * 4 + spacing * 4); - foreach (Mission mission in GameMain.GameSession.Missions) { if (!mission.Prefab.ShowInMenus) { continue; } - GUIFrame missionDescriptionHolder = new GUIFrame(new RectTransform(Vector2.One, missionList.Content.RectTransform), style: null); - GUILayoutGroup missionTextGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.744f, 0f), missionDescriptionHolder.RectTransform, Anchor.CenterLeft) { AbsoluteOffset = new Point(iconSize + spacing, 0) }, false, childAnchor: Anchor.TopLeft) + + var textContent = new List() { - AbsoluteSpacing = spacing + mission.GetMissionRewardText(Submarine.MainSub), + mission.GetReputationRewardText(), + mission.Description }; - LocalizedString descriptionText = mission.Description; - foreach (LocalizedString missionMessage in mission.ShownMessages) + textContent.AddRange(mission.ShownMessages); + + RoundSummary.CreateMissionEntry( + missionList.Content, + mission.Name, + textContent, + mission.Difficulty ?? 0, + mission.Prefab.Icon, mission.Prefab.IconColor, + out GUIImage missionIcon); + if (missionIcon != null) { - descriptionText += "\n\n" + missionMessage; - } - RichString rewardText = mission.GetMissionRewardText(Submarine.MainSub); - RichString reputationText = mission.GetReputationRewardText(); + UpdateMissionStateIcon(); + mission.OnMissionStateChanged += (mission) => UpdateMissionStateIcon(); - Func wrapMissionText(GUIFont font) - { - return (str) => ToolBox.WrapText(str, missionTextGroup.Rect.Width, font.Value); - } - RichString missionNameString = RichString.Rich(mission.Name, wrapMissionText(GUIStyle.LargeFont)); - RichString missionRewardString = RichString.Rich(rewardText, wrapMissionText(GUIStyle.Font)); - RichString missionReputationString = RichString.Rich(reputationText, wrapMissionText(GUIStyle.Font)); - RichString missionDescriptionString = RichString.Rich(descriptionText, wrapMissionText(GUIStyle.Font)); - - Vector2 missionNameSize = GUIStyle.LargeFont.MeasureString(missionNameString.SanitizedValue); - Vector2 missionDescriptionSize = GUIStyle.Font.MeasureString(missionDescriptionString.SanitizedValue); - Vector2 missionRewardSize = GUIStyle.Font.MeasureString(missionRewardString.SanitizedValue); - Vector2 missionReputationSize = GUIStyle.Font.MeasureString(missionReputationString.SanitizedValue); - - float ySize = missionNameSize.Y + missionDescriptionSize.Y + missionRewardSize.Y + missionReputationSize.Y + missionTextGroup.AbsoluteSpacing * 4; - bool displayDifficulty = mission.Difficulty.HasValue; - if (displayDifficulty) { ySize += missionRewardSize.Y; } - - missionDescriptionHolder.RectTransform.NonScaledSize = new Point(missionDescriptionHolder.RectTransform.NonScaledSize.X, (int)ySize); - missionTextGroup.RectTransform.NonScaledSize = new Point(missionTextGroup.RectTransform.NonScaledSize.X, missionDescriptionHolder.RectTransform.NonScaledSize.Y); - - 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);*/ - - var icon = new GUIImage(new RectTransform(new Point(iconSize), missionDescriptionHolder.RectTransform), mission.Prefab.Icon, null, true) + void UpdateMissionStateIcon() { - Color = mission.Prefab.IconColor, - HoverColor = mission.Prefab.IconColor, - SelectedColor = mission.Prefab.IconColor, - CanBeFocused = false - }; - UpdateMissionStateIcon(mission, icon); - mission.OnMissionStateChanged += (mission) => UpdateMissionStateIcon(mission, icon); - } - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionNameString, font: GUIStyle.LargeFont); - GUILayoutGroup difficultyIndicatorGroup = null; - if (displayDifficulty) - { - difficultyIndicatorGroup = new GUILayoutGroup(new RectTransform(new Point(missionTextGroup.Rect.Width, (int)missionRewardSize.Y), parent: missionTextGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - AbsoluteSpacing = 1 - }; - var difficultyColor = mission.GetDifficultyColor(); - for (int i = 0; i < mission.Difficulty.Value; i++) - { - new GUIImage(new RectTransform(Vector2.One, difficultyIndicatorGroup.RectTransform, scaleBasis: ScaleBasis.Smallest), "DifficultyIndicator", scaleToFit: true) + if (mission.DisplayAsCompleted || mission.DisplayAsFailed) { - CanBeFocused = false, - Color = difficultyColor - }; + RoundSummary.UpdateMissionStateIcon(mission.DisplayAsCompleted, missionIcon); + } } } - var rewardTextBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionRewardString); - if (difficultyIndicatorGroup != null) - { - difficultyIndicatorGroup.RectTransform.Resize(new Point((int)(difficultyIndicatorGroup.Rect.Width - rewardTextBlock.Padding.X - rewardTextBlock.Padding.Z), difficultyIndicatorGroup.Rect.Height)); - difficultyIndicatorGroup.RectTransform.AbsoluteOffset = new Point((int)rewardTextBlock.Padding.X, 0); - } - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionReputationString); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionDescriptionString); } } else @@ -1636,66 +1576,14 @@ namespace Barotrauma GUILayoutGroup missionTextGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0f), missionList.RectTransform, Anchor.CenterLeft), false, childAnchor: Anchor.TopLeft); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), TextManager.Get("NoMission"), font: GUIStyle.LargeFont); } + + GameMain.GameSession?.EventManager?.EventLog?.CreateEventLogUI(missionList.Content); + GameMain.GameSession.EnableEventLogNotificationIcon(enabled: false); + + RoundSummary.AddSeparators(missionList.Content); } - private void UpdateMissionStateIcon(Mission mission, GUIImage missionIcon) - { - if (mission == null || missionIcon == null) { return; } - string style = string.Empty; - if (mission.DisplayAsFailed) - { - style = "MissionFailedIcon"; - } - else if (mission.DisplayAsCompleted) - { - style = "MissionCompletedIcon"; - } - GUIImage stateIcon = missionIcon.GetChild(); - if (string.IsNullOrEmpty(style)) - { - if (stateIcon != null) - { - stateIcon.Visible = false; - } - } - else - { - stateIcon ??= new GUIImage(new RectTransform(Vector2.One, missionIcon.RectTransform), style, scaleToFit: true); - stateIcon.Visible = true; - } - } - - private void CreateTraitorInfo(GUIFrame infoFrame, TraitorMissionPrefab traitorMission, Character traitor) - { - GUIFrame missionFrame = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox"); - - int padding = (int)(0.0245f * missionFrame.Rect.Height); - - GUIFrame missionDescriptionHolder = new GUIFrame(new RectTransform(new Point(missionFrame.Rect.Width - padding * 2, 0), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, padding) }, style: null); - GUILayoutGroup missionTextGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.65f, 0f), missionDescriptionHolder.RectTransform, Anchor.CenterLeft) { RelativeOffset = new Vector2(0.319f, 0f) }, false, childAnchor: Anchor.TopLeft); - - LocalizedString missionNameString = ToolBox.WrapText(TextManager.Get("tabmenu.traitor"), missionTextGroup.Rect.Width, GUIStyle.LargeFont); - LocalizedString missionDescriptionString = ToolBox.WrapText(traitor.TraitorCurrentObjective, missionTextGroup.Rect.Width, GUIStyle.Font); - - Vector2 missionNameSize = GUIStyle.LargeFont.MeasureString(missionNameString); - Vector2 missionDescriptionSize = GUIStyle.Font.MeasureString(missionDescriptionString); - - missionDescriptionHolder.RectTransform.NonScaledSize = new Point(missionDescriptionHolder.RectTransform.NonScaledSize.X, (int)(missionNameSize.Y + missionDescriptionSize.Y)); - missionTextGroup.RectTransform.NonScaledSize = new Point(missionTextGroup.RectTransform.NonScaledSize.X, missionDescriptionHolder.RectTransform.NonScaledSize.Y); - - float aspectRatio = traitorMission.Icon.SourceRect.Width / traitorMission.Icon.SourceRect.Height; - - int iconWidth = (int)(0.319f * missionDescriptionHolder.RectTransform.NonScaledSize.X); - int iconHeight = Math.Max(missionTextGroup.RectTransform.NonScaledSize.Y, (int)(iconWidth * aspectRatio)); - Point iconSize = new Point(iconWidth, iconHeight); - - new GUIImage(new RectTransform(iconSize, missionDescriptionHolder.RectTransform), traitorMission.Icon, null, true) { Color = traitorMission.IconColor }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionNameString, font: GUIStyle.LargeFont); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionDescriptionString); - } - - private void CreateSubmarineInfo(GUIFrame infoFrame, Submarine sub) + private static void CreateSubmarineInfo(GUIFrame infoFrame, Submarine sub) { GUIFrame subInfoFrame = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox"); GUIFrame paddedFrame = new GUIFrame(new RectTransform(Vector2.One * 0.97f, subInfoFrame.RectTransform, Anchor.Center), style: null); @@ -1793,7 +1681,7 @@ namespace Barotrauma } } - private GUIImage talentPointNotification; + private GUIImage talentPointNotification, eventLogNotification; public static void CreateSkillList(Character character, CharacterInfo info, GUIListBox parent) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs index fc8dfd1f5..90c860332 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs @@ -117,6 +117,9 @@ namespace Barotrauma return MathHelper.Clamp(Math.Min(Math.Min(scale.X, scale.Y), GUI.SlicedSpriteScale), minBorderScale, maxBorderScale); } + public void Draw(SpriteBatch spriteBatch, RectangleF rect, Color color, SpriteEffects spriteEffects = SpriteEffects.None, Vector2? uvOffset = null) + => Draw(spriteBatch, new Rectangle(rect.Location.ToPoint(), rect.Size.ToPoint()), color, spriteEffects, uvOffset); + public void Draw(SpriteBatch spriteBatch, Rectangle rect, Color color, SpriteEffects spriteEffects = SpriteEffects.None, Vector2? uvOffset = null) { uvOffset ??= Vector2.Zero; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs index a0a55d717..7ffcbfde4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs @@ -11,7 +11,7 @@ namespace Barotrauma { if (consentTextAvailable) { - var background = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), style: "GUIBackgroundBlocker"); + var background = new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas), style: "GUIBackgroundBlocker"); var frame = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.7f), background.RectTransform, Anchor.Center) { MinSize = new Point(800, 0), MaxSize = new Point(1500, int.MaxValue) }); var content = new GUILayoutGroup(new RectTransform(new Vector2(0.95f), frame.RectTransform, Anchor.Center)) @@ -55,7 +55,8 @@ namespace Barotrauma yesBtn.OnClicked += (btn, userdata) => { GUIMessageBox.MessageBoxes.Remove(background); - SetConsentInternal(Consent.Yes); + var loadingBox = GUIMessageBox.CreateLoadingBox(TextManager.Get("PleaseWait")); + SetConsentInternal(Consent.Yes, onAnswerSent: loadingBox.Close); return true; }; yesBtn.Enabled = false; @@ -76,7 +77,8 @@ namespace Barotrauma noBtn.OnClicked += (btn, userdata) => { GUIMessageBox.MessageBoxes.Remove(background); - SetConsent(Consent.No); + var loadingBox = GUIMessageBox.CreateLoadingBox(TextManager.Get("PleaseWait")); + SetConsent(Consent.No, onAnswerSent: loadingBox.Close); return true; }; noBtn.Enabled = false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 8d8f7a838..4f4e5a284 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -111,7 +111,8 @@ namespace Barotrauma public static LoadingScreen TitleScreen; private bool loadingScreenOpen; - private CoroutineHandle loadingCoroutine; + private Thread initialLoadingThread; + public bool HasLoaded { get; private set; } private readonly GameTime fixedTime; @@ -411,24 +412,21 @@ namespace Barotrauma WaitForLanguageSelection = GameSettings.CurrentConfig.Language == LanguageIdentifier.None }; - bool canLoadInSeparateThread = true; - - loadingCoroutine = CoroutineManager.StartCoroutine(Load(canLoadInSeparateThread), "Load", canLoadInSeparateThread); + initialLoadingThread = new Thread(Load); + initialLoadingThread.Start(); } - public class LoadingException : Exception + private void Load() { - public LoadingException(Exception e) : base("Loading was interrupted due to an error.", innerException: e) + static void log(string str) { + if (GameSettings.CurrentConfig.VerboseLogging) + { + DebugConsole.NewMessage(str, Color.Lime); + } } - } - - private IEnumerable Load(bool isSeparateThread) - { - if (GameSettings.CurrentConfig.VerboseLogging) - { - DebugConsole.NewMessage("LOADING COROUTINE", Color.Lime); - } + + log("LOADING COROUTINE"); ContentPackageManager.LoadVanillaFileList(); @@ -438,7 +436,7 @@ namespace Barotrauma TitleScreen.AvailableLanguages = TextManager.AvailableLanguages.OrderBy(l => l.Value != "english".ToIdentifier()).ThenBy(l => l.Value).ToArray(); while (TitleScreen.WaitForLanguageSelection) { - yield return CoroutineStatus.Running; + Thread.Sleep((int)(Timing.Step * 1000)); } ContentPackageManager.VanillaCorePackage.UnloadFilesOfType(); } @@ -450,25 +448,13 @@ namespace Barotrauma { var pendingSplashScreens = TitleScreen.PendingSplashScreens; float baseVolume = MathHelper.Clamp(GameSettings.CurrentConfig.Audio.SoundVolume * 2.0f, 0.0f, 1.0f); - pendingSplashScreens?.Enqueue(new LoadingScreen.PendingSplashScreen("Content/SplashScreens/Splash_UTG.webm", baseVolume * 0.5f)); - pendingSplashScreens?.Enqueue(new LoadingScreen.PendingSplashScreen("Content/SplashScreens/Splash_FF.webm", baseVolume)); - pendingSplashScreens?.Enqueue(new LoadingScreen.PendingSplashScreen("Content/SplashScreens/Splash_Daedalic.webm", baseVolume * 0.1f)); - } - - //if not loading in a separate thread, wait for the splash screens to finish before continuing the loading - //otherwise the videos will look extremely choppy - if (!isSeparateThread) - { - while (TitleScreen.PlayingSplashScreen || TitleScreen.PendingSplashScreens.Count > 0) - { - yield return CoroutineStatus.Running; - } + pendingSplashScreens.Enqueue(new LoadingScreen.PendingSplashScreen("Content/SplashScreens/Splash_UTG.webm", baseVolume * 0.5f)); + pendingSplashScreens.Enqueue(new LoadingScreen.PendingSplashScreen("Content/SplashScreens/Splash_FF.webm", baseVolume)); + pendingSplashScreens.Enqueue(new LoadingScreen.PendingSplashScreen("Content/SplashScreens/Splash_Daedalic.webm", baseVolume * 0.1f)); } GUI.Init(); - yield return CoroutineStatus.Running; - LegacySteamUgcTransition.Prepare(); var contentPackageLoadRoutine = ContentPackageManager.Init(); foreach (var progress in contentPackageLoadRoutine @@ -476,7 +462,6 @@ namespace Barotrauma { const float min = 1f, max = 70f; TitleScreen.LoadState = MathHelper.Lerp(min, max, progress); - yield return CoroutineStatus.Running; } var corePackage = ContentPackageManager.EnabledPackages.Core; @@ -505,7 +490,6 @@ namespace Barotrauma TaskPool.Add("InitRelayNetworkAccess", SteamManager.InitRelayNetworkAccess(), (t) => { }); HintManager.Init(); - yield return CoroutineStatus.Running; CoreEntityPrefab.InitCorePrefabs(); GameModePreset.Init(); @@ -513,7 +497,6 @@ namespace Barotrauma SubmarineInfo.RefreshSavedSubs(); TitleScreen.LoadState = 75.0f; - yield return CoroutineStatus.Running; GameScreen = new GameScreen(GraphicsDeviceManager.GraphicsDevice); @@ -521,13 +504,11 @@ namespace Barotrauma LightManager = new Lights.LightManager(base.GraphicsDevice); TitleScreen.LoadState = 80.0f; - yield return CoroutineStatus.Running; MainMenuScreen = new MainMenuScreen(this); ServerListScreen = new ServerListScreen(); TitleScreen.LoadState = 85.0f; - yield return CoroutineStatus.Running; #if USE_STEAM if (SteamManager.IsInitialized) @@ -553,12 +534,10 @@ namespace Barotrauma TestScreen = new TestScreen(); TitleScreen.LoadState = 90.0f; - yield return CoroutineStatus.Running; ParticleEditorScreen = new ParticleEditorScreen(); TitleScreen.LoadState = 95.0f; - yield return CoroutineStatus.Running; LevelEditorScreen = new LevelEditorScreen(); SpriteEditorScreen = new SpriteEditorScreen(); @@ -566,8 +545,6 @@ namespace Barotrauma CharacterEditorScreen = new CharacterEditor.CharacterEditorScreen(); CampaignEndScreen = new CampaignEndScreen(); - yield return CoroutineStatus.Running; - #if DEBUG LevelGenerationParams.CheckValidity(); #endif @@ -583,12 +560,7 @@ namespace Barotrauma TitleScreen.LoadState = 100.0f; HasLoaded = true; - if (GameSettings.CurrentConfig.VerboseLogging) - { - DebugConsole.NewMessage("LOADING COROUTINE FINISHED", Color.Lime); - } - yield return CoroutineStatus.Success; - + log("LOADING COROUTINE FINISHED"); } /// @@ -741,11 +713,6 @@ namespace Barotrauma #endif Client?.Update((float)Timing.Step); - - if (!HasLoaded && !CoroutineManager.IsCoroutineRunning(loadingCoroutine)) - { - throw new LoadingException(loadingCoroutine.Exception); - } } else if (HasLoaded) { @@ -1174,7 +1141,7 @@ namespace Barotrauma { waitForKeyHit = waitKeyHit; loadingScreenOpen = true; - TitleScreen.LoadState = null; + TitleScreen.LoadState = 0f; return CoroutineManager.StartCoroutine(TitleScreen.DoLoading(loader)); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index eddd741f2..b8966494d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -34,6 +34,8 @@ namespace Barotrauma private bool _isCrewMenuOpen = true; private Point crewListEntrySize; + private readonly List traitorButtons = new List(); + /// /// Present only in single player games. In multiplayer. The chatbox is found from GameSession.Client. /// @@ -95,6 +97,7 @@ namespace Barotrauma { CanBeFocused = false }; + crewArea.RectTransform.NonScaledSize = HUDLayoutSettings.CrewArea.Size; // AbsoluteOffset is set in UpdateProjectSpecific based on crewListOpenState crewList = new GUIListBox(new RectTransform(Vector2.One, crewArea.RectTransform), style: null, isScrollBarOnDefaultSide: false) @@ -193,7 +196,7 @@ namespace Barotrauma }; } - var reports = OrderPrefab.Prefabs.Where(o => o.IsReport && o.SymbolSprite != null && !o.Hidden).OrderBy(o => o.Identifier).ToArray(); + var reports = OrderPrefab.Prefabs.Where(o => o.IsVisibleAsReportButton).OrderBy(o => o.Identifier).ToArray(); if (reports.None()) { DebugConsole.ThrowError("No valid orders for report buttons found! Cannot create report buttons. The orders for the report buttons must have 'targetallcharacters' attribute enabled and a valid 'symbolsprite' defined."); @@ -225,7 +228,8 @@ namespace Barotrauma //report buttons foreach (OrderPrefab orderPrefab in reports) { - if (!orderPrefab.IsReport || orderPrefab.SymbolSprite == null || orderPrefab.Hidden) { continue; } + if (!orderPrefab.IsVisibleAsReportButton) { continue; } + var btn = new GUIButton(new RectTransform(Vector2.One, parent.RectTransform, scaleBasis: isHorizontal ? ScaleBasis.BothHeight : ScaleBasis.BothWidth), style: null) { OnClicked = (button, userData) => @@ -366,7 +370,7 @@ namespace Barotrauma } } - int iconsVisible = isJobIconVisible ? 5 : 4; + int iconsVisible = isJobIconVisible ? 6 : 5; var nameRelativeWidth = 1.0f // Start padding - paddingRelativeWidth @@ -412,7 +416,6 @@ namespace Barotrauma { AllowMouseWheelScroll = false, CurrentDragMode = GUIListBox.DragMode.DragWithinBox, - HideChildrenOutsideFrame = false, KeepSpaceForScrollBar = false, OnRearranged = OnOrdersRearranged, ScrollBarVisible = false, @@ -438,13 +441,13 @@ namespace Barotrauma Stretch = false }; - var extraIconFrame = new GUIFrame(new RectTransform(new Vector2(0.8f * iconRelativeWidth, 0.8f), layoutGroup.RectTransform), style: null) + var extraIconFrame = new GUIFrame(new RectTransform(new Vector2(0.8f * iconRelativeWidth * 2, 0.8f), layoutGroup.RectTransform), style: null) { CanBeFocused = false, UserData = "extraicons" }; - var soundIconParent = new GUIFrame(new RectTransform(Vector2.One, extraIconFrame.RectTransform), style: null) + var soundIconParent = new GUIFrame(new RectTransform(new Vector2(0.8f), extraIconFrame.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.Smallest), style: null) { CanBeFocused = false, UserData = "soundicons", @@ -469,16 +472,6 @@ namespace Barotrauma Visible = false }; - if (character.IsBot) - { - new GUIFrame(new RectTransform(Vector2.One, extraIconFrame.RectTransform), style: null) - { - CanBeFocused = false, - UserData = "objectiveicon", - Visible = false - }; - } - new GUIButton(new RectTransform(new Point((int)commandButtonAbsoluteHeight), background.RectTransform), style: "CrewListCommandButton") { ToolTip = TextManager.Get("inputtype.command"), @@ -489,6 +482,44 @@ namespace Barotrauma return true; } }; + if (character.IsBot) + { + new GUIFrame(new RectTransform(Vector2.One, extraIconFrame.RectTransform, scaleBasis: ScaleBasis.Smallest), style: null) + { + CanBeFocused = false, + UserData = "objectiveicon", + Visible = false + }; + } + else if (GameMain.GameSession is { TraitorsEnabled: true } && GameMain.Client != null && character != Character.Controlled) + { + Client targetClient = GameMain.Client.ConnectedClients.FirstOrDefault(c => c.Character == character); + if (targetClient != null) + { + if (OrderPrefab.Prefabs.TryGet("reporttraitor", out OrderPrefab order)) + { + var voteTraitorBtn = new GUITickBox(new RectTransform(Vector2.One, extraIconFrame.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.Smallest), label: string.Empty, style: "TraitorVoteButton") + { + UserData = character, + ToolTip = + RichString.Rich( + $"‖color:{XMLExtensions.ToStringHex(GUIStyle.TextColorBright)}‖{TextManager.Get("traitor.blamebutton")}‖color:end‖\n" + + TextManager.Get("traitor.blamebutton.tooltip")), + OnSelected = (GUITickBox obj) => + { + foreach (var traitorBtn in traitorButtons) + { + //deselect other traitor buttons + if (traitorBtn != obj) { traitorBtn.SetSelected(false, callOnSelected: false); } + } + GameMain.Client?.Vote(VoteType.Traitor, obj.Selected ? targetClient : null); + return true; + } + }; + traitorButtons.Add(voteTraitorBtn); + } + } + } return background; } @@ -498,6 +529,7 @@ namespace Barotrauma if (crewList?.Content.GetChildByUserData(character) is { } component) { crewList.RemoveChild(component); + traitorButtons.RemoveAll(t => t.IsChildOf(component, recursive: true)); } } @@ -1204,7 +1236,7 @@ namespace Barotrauma } } - crewArea.Visible = !(GameMain.GameSession?.GameMode is CampaignMode campaign) || (!campaign.ForceMapUI && !campaign.ShowCampaignUI); + crewArea.Visible = GameMain.GameSession?.GameMode is not CampaignMode campaign || (!campaign.ForceMapUI && !campaign.ShowCampaignUI); guiFrame.AddToGUIUpdateList(); } @@ -1371,7 +1403,8 @@ namespace Barotrauma if (PlayerInput.KeyDown(InputType.Command) && (GUI.KeyboardDispatcher.Subscriber == null || (GUI.KeyboardDispatcher.Subscriber is GUIComponent component && (component == crewList || component.IsChildOf(crewList)))) && - commandFrame == null && !clicklessSelectionActive && CanIssueOrders && !(GameMain.GameSession?.Campaign?.ShowCampaignUI ?? false)) + commandFrame == null && !clicklessSelectionActive && CanIssueOrders && !(GameMain.GameSession?.Campaign?.ShowCampaignUI ?? false) && + Character.Controlled?.SelectedItem?.Prefab is not { DisableCommandMenuWhenSelected: true }) { if (PlayerInput.IsShiftDown()) { @@ -1556,6 +1589,12 @@ namespace Barotrauma { if (characterComponent.UserData is Character character) { + if (character.Removed) + { + characterComponent.Visible = false; + continue; + } + characterComponent.Visible = Character.Controlled == null || Character.Controlled.TeamID == character.TeamID; if (character.TeamID == CharacterTeamType.FriendlyNPC && Character.Controlled != null && (character.CurrentHull == Character.Controlled.CurrentHull || Vector2.DistanceSquared(Character.Controlled.WorldPosition, character.WorldPosition) < 500.0f * 500.0f)) @@ -1632,6 +1671,8 @@ namespace Barotrauma } } + traitorButtons.ForEach(btn => btn.Visible = Character.Controlled is { IsDead: false } && btn.UserData as Character != Character.Controlled); + crewArea.RectTransform.AbsoluteOffset = Vector2.SmoothStep( new Vector2(-crewArea.Rect.Width - HUDLayoutSettings.Padding, 0.0f), Vector2.Zero, @@ -2395,7 +2436,7 @@ namespace Barotrauma if (!(GetTargetSubmarine() is { } sub)) { return; } shortcutNodes.Clear(); var subItems = sub.GetItems(false); - if (CanFitMoreNodes() && subItems.Find(i => i.HasTag("reactor") && i.IsPlayerTeamInteractable)?.GetComponent() is Reactor reactor) + if (CanFitMoreNodes() && subItems.Find(i => i.HasTag(Tags.Reactor) && i.IsPlayerTeamInteractable)?.GetComponent() is Reactor reactor) { float reactorOutput = -reactor.CurrPowerConsumption; // If player is not an engineer AND the reactor is not powered up AND nobody is using the reactor @@ -2414,7 +2455,7 @@ namespace Barotrauma // If player is not a captain AND nobody is using the nav terminal AND the nav terminal is powered up // --> Create shortcut node for Steer order if (CanFitMoreNodes() && ShouldDelegateOrder("steer") && IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs["steer"]) && - subItems.Find(i => i.HasTag("navterminal") && i.IsPlayerTeamInteractable) is Item nav && characters.None(c => c.SelectedItem == nav) && + subItems.Find(i => i.HasTag(Tags.NavTerminal) && i.IsPlayerTeamInteractable) is Item nav && characters.None(c => c.SelectedItem == nav) && nav.GetComponent() is Steering steering && steering.Voltage > steering.MinVoltage) { var order = new Order(OrderPrefab.Prefabs["steer"], steering.Item, steering); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index 89364a512..28556fd9d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -157,11 +157,15 @@ namespace Barotrauma SlideshowPlayer?.DrawManually(spriteBatch); - if (GUI.DisableHUD || GUI.DisableUpperHUD || ForceMapUI || CoroutineManager.IsCoroutineRunning("LevelTransition")) + if (GUI.DisableHUD || GUI.DisableUpperHUD || ForceMapUI || + CoroutineManager.IsCoroutineRunning("LevelTransition")) { endRoundButton.Visible = false; - if (ReadyCheckButton != null) { ReadyCheckButton.Visible = false; } - return; + if (ReadyCheckButton != null) + { + ReadyCheckButton.Visible = false; + } + return; } if (Submarine.MainSub == null || Level.Loaded == null) { return; } @@ -216,11 +220,15 @@ namespace Barotrauma } break; } - if (Level.IsLoadedOutpost && !ObjectiveManager.AllActiveObjectivesCompleted()) + if (Level.IsLoadedOutpost && + (!ObjectiveManager.AllActiveObjectivesCompleted() && this is not MultiPlayerCampaign)) { allowEndingRound = false; } - if (ReadyCheckButton != null) { ReadyCheckButton.Visible = allowEndingRound; } + if (ReadyCheckButton != null) + { + ReadyCheckButton.Visible = allowEndingRound && GameMain.GameSession != null && GameMain.GameSession.RoundDuration > 10.0f; + } endRoundButton.Visible = allowEndingRound && Character.Controlled is { IsIncapacitated: false }; if (endRoundButton.Visible) @@ -384,8 +392,11 @@ namespace Barotrauma protected void TryEndRoundWithFuelCheck(Action onConfirm, Action onReturnToMapScreen) { Submarine.MainSub.CheckFuel(); - SubmarineInfo nextSub = PendingSubmarineSwitch ?? Submarine.MainSub.Info; - bool lowFuel = nextSub.Name == Submarine.MainSub.Info.Name ? Submarine.MainSub.Info.LowFuel : nextSub.LowFuel; + bool lowFuel = Submarine.MainSub.Info.LowFuel; + if (PendingSubmarineSwitch != null) + { + lowFuel = TransferItemsOnSubSwitch ? (lowFuel && PendingSubmarineSwitch.LowFuel) : PendingSubmarineSwitch.LowFuel; + } if (Level.IsLoadedFriendlyOutpost && lowFuel && CargoManager.PurchasedItems.None(i => i.Value.Any(pi => pi.ItemPrefab.Tags.Contains("reactorfuel")))) { var extraConfirmationBox = @@ -433,11 +444,21 @@ namespace Barotrauma { GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData is RoundSummary); } +#if DEBUG + if (GUI.KeyboardDispatcher.Subscriber == null && PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.M)) + { + if (GUIMessageBox.MessageBoxes.Any()) { GUIMessageBox.MessageBoxes.Remove(GUIMessageBox.MessageBoxes.Last()); } + GUIFrame summaryFrame = GameMain.GameSession.RoundSummary.CreateSummaryFrame(GameMain.GameSession, ""); + GUIMessageBox.MessageBoxes.Add(summaryFrame); + GameMain.GameSession.RoundSummary.ContinueButton.OnClicked = (_, __) => { GUIMessageBox.MessageBoxes.Remove(summaryFrame); return true; }; + } +#endif if (ShowCampaignUI || ForceMapUI) { CampaignUI?.Update(deltaTime); } } + } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index ab23c52d3..a3be9801c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -284,7 +284,7 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - protected override IEnumerable DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror, List traitorResults = null) + protected override IEnumerable DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror) { yield return CoroutineStatus.Success; } @@ -1014,9 +1014,9 @@ namespace Barotrauma public void LoadState(string filePath) { DebugConsole.Log($"Loading save file for an existing game session ({filePath})"); - SaveUtil.DecompressToDirectory(filePath, SaveUtil.TempPath, null); + SaveUtil.DecompressToDirectory(filePath, SaveUtil.TempPath); - string gamesessionDocPath = Path.Combine(SaveUtil.TempPath, "gamesession.xml"); + string gamesessionDocPath = Path.Combine(SaveUtil.TempPath, SaveUtil.GameSessionFileName); XDocument doc = XMLExtensions.TryLoadXml(gamesessionDocPath); if (doc == null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index d2c611e10..fa274ad8f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -368,7 +368,7 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - protected override IEnumerable DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror, List traitorResults = null) + protected override IEnumerable DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror) { NextLevel = newLevel; bool success = CrewManager.GetCharacters().Any(c => !c.IsDead); @@ -382,7 +382,7 @@ namespace Barotrauma // Event history must be registered before ending the round or it will be cleared GameMain.GameSession.EventManager.RegisterEventHistory(); } - GameMain.GameSession.EndRound("", traitorResults, transitionType); + GameMain.GameSession.EndRound("", transitionType); var continueButton = GameMain.GameSession.RoundSummary?.ContinueButton; RoundSummary roundSummary = null; if (GUIMessageBox.VisibleBox?.UserData is RoundSummary) @@ -513,17 +513,6 @@ namespace Barotrauma } } -#if DEBUG - if (GUI.KeyboardDispatcher.Subscriber == null && PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.M)) - { - 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; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs index d6c8f225d..59031cb75 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs @@ -8,7 +8,7 @@ namespace Barotrauma.Tutorials { enum AutoPlayVideo { Yes, No }; - enum TutorialSegmentType { MessageBox, InfoBox, Objective }; + enum SegmentType { MessageBox, InfoBox, Objective }; sealed class Tutorial { @@ -108,7 +108,7 @@ namespace Barotrauma.Tutorials GameMain.GameSession.StartRound(LevelSeed); } - GameMain.GameSession.EventManager.ActiveEvents.Clear(); + GameMain.GameSession.EventManager.ClearEvents(); GameMain.GameSession.EventManager.Enabled = true; GameMain.GameScreen.Select(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs index 90226c5e0..9c42d6beb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs @@ -1,5 +1,4 @@ -using Barotrauma.Tutorials; -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace Barotrauma @@ -15,8 +14,6 @@ namespace Barotrauma public static bool IsTabMenuOpen => GameMain.GameSession?.tabMenu != null; public static TabMenu TabMenuInstance => GameMain.GameSession?.tabMenu; - private float prevHudScale; - private TabMenu tabMenu; public bool ToggleTabMenu() @@ -27,7 +24,7 @@ namespace Barotrauma GameMain.NetLobbyScreen.CharacterAppearanceCustomizationMenu = null; if (GameMain.NetLobbyScreen.JobSelectionFrame != null) { GameMain.NetLobbyScreen.JobSelectionFrame.Visible = false; } } - if (tabMenu == null && !(GameMode is TutorialMode) && !ConversationAction.IsDialogOpen) + if (tabMenu == null && GameMode is not TutorialMode && !ConversationAction.IsDialogOpen) { tabMenu = new TabMenu(); HintManager.OnShowTabMenu(); @@ -49,6 +46,8 @@ namespace Barotrauma private GUITextBlock respawnInfoText; private GUITickBox respawnTickBox; + private GUIImage eventLogNotification; + private void CreateTopLeftButtons() { if (topLeftButtonGroup != null) @@ -96,7 +95,8 @@ namespace Barotrauma OnClicked = (button, userData) => ToggleTabMenu() }; - talentPointNotification = CreateTalentIconNotification(tabMenuButton); + talentPointNotification = CreateNotificationIcon(tabMenuButton); + eventLogNotification = CreateNotificationIcon(tabMenuButton); GameMain.Instance.ResolutionChanged += CreateTopLeftButtons; @@ -121,7 +121,6 @@ namespace Barotrauma return true; } }; - prevHudScale = GameSettings.CurrentConfig.Graphics.HUDScale; } public void AddToGUIUpdateList() @@ -152,7 +151,7 @@ namespace Barotrauma } } - public static GUIImage CreateTalentIconNotification(GUIComponent parent, bool offset = true) + public static GUIImage CreateNotificationIcon(GUIComponent parent, bool offset = true) { GUIImage indicator = new GUIImage(new RectTransform(new Vector2(0.45f), parent.RectTransform, anchor: Anchor.TopRight, scaleBasis: ScaleBasis.BothWidth), style: "TalentPointNotification") { @@ -167,19 +166,22 @@ namespace Barotrauma return indicator; } + public void EnableEventLogNotificationIcon(bool enabled) + { + if (eventLogNotification == null) { return; } + if (!eventLogNotification.Visible && enabled) + { + eventLogNotification.Pulsate(Vector2.One, Vector2.One * 2, 1.0f); + } + eventLogNotification.Visible = enabled; + } + public static void UpdateTalentNotificationIndicator(GUIImage indicator) { - if (indicator != null) - { - if (Character.Controlled?.Info == null) - { - indicator.Visible = false; - } - else - { - indicator.Visible = Character.Controlled.Info.GetAvailableTalentPoints() > 0 && !Character.Controlled.HasUnlockedAllTalents(); - } - } + if (indicator == null) { return; } + indicator.Visible = + Character.Controlled?.Info != null && + Character.Controlled.Info.GetAvailableTalentPoints() > 0 && !Character.Controlled.HasUnlockedAllTalents(); } public void HUDScaleChanged() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs index 49f2a6fcc..8d222c5a2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs @@ -1,7 +1,6 @@ using Barotrauma.Extensions; using Barotrauma.IO; using Barotrauma.Items.Components; -using Barotrauma.Tutorials; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -152,8 +151,8 @@ namespace Barotrauma } // onstartedinteracting.turretperiscope - if (item.HasTag("periscope") && - item.GetConnectedComponents().FirstOrDefault(t => t.Item.HasTag("turret")) is Turret) + if (item.HasTag(Tags.Periscope) && + item.GetConnectedComponents().FirstOrDefault(t => t.Item.HasTag(Tags.Turret)) is Turret) { if (DisplayHint($"{hintIdentifierBase}.turretperiscope".ToIdentifier(), variables: new[] @@ -175,6 +174,17 @@ namespace Barotrauma } } + public static void OnStartRepairing(Character character, Repairable repairable) + { + if (repairable.ForceDeteriorationTimer > 0.0f && !character.IsTraitor) + { + CoroutineManager.Invoke(() => + { + DisplayHint($"repairingsabotageditem".ToIdentifier()); + }, delay: 5.0f); + } + } + private static void CheckIsInteracting() { if (!CanDisplayHints()) { return; } @@ -182,7 +192,7 @@ namespace Barotrauma if (Character.Controlled.SelectedItem.GetComponent() is Reactor reactor && reactor.PowerOn && Character.Controlled.SelectedItem.OwnInventory?.AllItems is IEnumerable containedItems && - containedItems.Count(i => i.HasTag("reactorfuel")) > 1) + containedItems.Count(i => i.HasTag(Tags.Fuel)) > 1) { if (DisplayHint("onisinteracting.reactorwithextrarods".ToIdentifier())) { return; } } @@ -210,7 +220,7 @@ namespace Barotrauma { if (item.CurrentHull == null) { continue; } if (item.GetComponent() == null) { continue; } - if (!item.HasTag("ballast") && !item.CurrentHull.RoomName.Contains("ballast", StringComparison.OrdinalIgnoreCase)) { continue; } + if (!item.HasTag(Tags.Ballast) && !item.CurrentHull.RoomName.Contains("ballast", StringComparison.OrdinalIgnoreCase)) { continue; } BallastHulls.Add(item.CurrentHull); } } @@ -246,8 +256,14 @@ namespace Barotrauma }); } + if (GameMain.GameSession is { TraitorsEnabled: true }) + { + DisplayHint("traitorsonboard".ToIdentifier()); + DisplayHint("traitorsonboard2".ToIdentifier()); + } yield return CoroutineStatus.Success; } + } public static void OnRoundEnded() @@ -395,8 +411,8 @@ namespace Barotrauma if (DisplayHint($"onobtaineditem.{tag}".ToIdentifier())) { return; } } - if ((item.HasTag("geneticmaterial") && character.Inventory.FindItemByTag("geneticdevice".ToIdentifier(), recursive: true) != null) || - (item.HasTag("geneticdevice") && character.Inventory.FindItemByTag("geneticmaterial".ToIdentifier(), recursive: true) != null)) + if ((item.HasTag(Tags.GeneticMaterial) && character.Inventory.FindItemByTag(Tags.GeneticMaterial, recursive: true) != null) || + (item.HasTag(Tags.GeneticDevice) && character.Inventory.FindItemByTag(Tags.GeneticDevice, recursive: true) != null)) { if (DisplayHint($"geneticmaterial.useinstructions".ToIdentifier())) { return; } } @@ -437,6 +453,14 @@ namespace Barotrauma }); } + public static void OnRadioJammed(Item radioItem) + { + if (!CanDisplayHints()) { return; } + if (radioItem?.ParentInventory is not CharacterInventory characterInventory) { return; } + if (characterInventory.Owner != Character.Controlled) { return; } + DisplayHint("radiojammed".ToIdentifier()); + } + public static void OnReactorOutOfFuel(Reactor reactor) { if (!CanDisplayHints()) { return; } @@ -450,6 +474,13 @@ namespace Barotrauma }); } + public static void OnAssignedAsTraitor() + { + if (!CanDisplayHints()) { return; } + DisplayHint("assignedastraitor".ToIdentifier()); + DisplayHint("assignedastraitor2".ToIdentifier()); + } + public static void OnAvailableTransition(CampaignMode.TransitionType transitionType) { if (!CanDisplayHints()) { return; } @@ -556,7 +587,7 @@ namespace Barotrauma private static void CheckIfDivingGearOutOfOxygen() { if (!CanDisplayHints()) { return; } - var divingGear = Character.Controlled.GetEquippedItem("diving", InvSlotType.OuterClothes); + var divingGear = Character.Controlled.GetEquippedItem(Tags.DivingGear, InvSlotType.OuterClothes); if (divingGear?.OwnInventory == null) { return; } if (divingGear.GetContainedItemConditionPercentage() > 0.0f) { return; } DisplayHint("ondivinggearoutofoxygen".ToIdentifier(), onUpdate: () => @@ -599,7 +630,7 @@ namespace Barotrauma if (adjacentHull.WaterPercentage > 75 && !BallastHulls.Contains(adjacentHull) && DisplayHint("onadjacenthull.highwaterpercentage".ToIdentifier())) { return; } } - static bool IsWearingDivingSuit() => Character.Controlled.GetEquippedItem("deepdiving", InvSlotType.OuterClothes) is Item; + static bool IsWearingDivingSuit() => Character.Controlled.GetEquippedItem(Tags.HeavyDivingGear, InvSlotType.OuterClothes) is Item; } static bool IsOnFriendlySub() => Character.Controlled.Submarine is Submarine sub && (sub.TeamID == Character.Controlled.TeamID || sub.TeamID == CharacterTeamType.FriendlyNPC); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ObjectiveManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ObjectiveManager.cs index ad65dcb8f..0a70bc1d9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ObjectiveManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ObjectiveManager.cs @@ -49,7 +49,7 @@ static class ObjectiveManager public Identifier ParentId { get; set; } - public TutorialSegmentType SegmentType { get; private set; } + public SegmentType SegmentType { get; private set; } public static Segment CreateInfoBoxSegment(Identifier id, Identifier objectiveTextTag, AutoPlayVideo autoPlayVideo, Text textContent = default, Video videoContent = default) { @@ -69,31 +69,31 @@ static class ObjectiveManager private Segment(Identifier id, Identifier objectiveTextTag, AutoPlayVideo autoPlayVideo, Text textContent = default, Video videoContent = default) { Id = id; - ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag)); + ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag).Fallback(objectiveTextTag.Value)); AutoPlayVideo = autoPlayVideo; TextContent = textContent; VideoContent = videoContent; - SegmentType = TutorialSegmentType.InfoBox; + SegmentType = SegmentType.InfoBox; } private Segment(Identifier id, Identifier objectiveTextTag, Action onClickObjective) { Id = id; - ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag)); + ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag).Fallback(objectiveTextTag.Value)); OnClickObjective = onClickObjective; - SegmentType = TutorialSegmentType.MessageBox; + SegmentType = SegmentType.MessageBox; } private Segment(Identifier id, Identifier objectiveTextTag) { Id = id; - ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag)); - SegmentType = TutorialSegmentType.Objective; + ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag).Fallback(objectiveTextTag.Value)); + SegmentType = SegmentType.Objective; } public void ConnectMessageBox(Segment messageBoxSegment) { - SegmentType = TutorialSegmentType.MessageBox; + SegmentType = SegmentType.MessageBox; OnClickObjective = messageBoxSegment.OnClickObjective; } } @@ -139,9 +139,9 @@ static class ObjectiveManager VideoPlayer.AddToGUIUpdateList(order: 100); } - public static void TriggerTutorialSegment(Segment segment, bool connectObjective = false) + public static void TriggerSegment(Segment segment, bool connectObjective = false) { - if (segment.SegmentType != TutorialSegmentType.InfoBox) + if (segment.SegmentType != SegmentType.InfoBox) { activeObjectives.Add(segment); AddToObjectiveList(segment, connectObjective); @@ -153,15 +153,15 @@ static class ObjectiveManager ActiveContentSegment = segment; var title = TextManager.Get(segment.Id); - LocalizedString tutorialText = TextManager.GetFormatted(segment.TextContent.Tag); - tutorialText = TextManager.ParseInputTypes(tutorialText); + LocalizedString text = TextManager.GetFormatted(segment.TextContent.Tag).Fallback(segment.TextContent.Tag.Value); + text = TextManager.ParseInputTypes(text); switch (segment.AutoPlayVideo) { case AutoPlayVideo.Yes: infoBox = CreateInfoFrame( title, - tutorialText, + text, segment.TextContent.Width, segment.TextContent.Height, segment.TextContent.Anchor, @@ -171,7 +171,7 @@ static class ObjectiveManager case AutoPlayVideo.No: infoBox = CreateInfoFrame( title, - tutorialText, + text, segment.TextContent.Width, segment.TextContent.Height, segment.TextContent.Anchor, @@ -182,31 +182,54 @@ static class ObjectiveManager } } - public static void CompleteTutorialSegment(Identifier segmentId) + public static void CompleteSegment(Identifier segmentId) { if (GetActiveObjective(segmentId) is not Segment segment || !segment.CanBeCompleted || segment.IsCompleted) { return; } - if (!MarkSegmentCompleted(segment)) + CompleteSegment(segment, failed: false); + } + + public static void FailSegment(Identifier segmentId) + { + if (GetActiveObjective(segmentId) is not Segment segment) { return; } + CompleteSegment(segment, failed: true); + } + + private static void CompleteSegment(Segment segment, bool failed = false) + { + if (failed) + { + if (!MarkSegmentFailed(segment)) { return; } + } + else + { + if (!MarkSegmentCompleted(segment)) { return; } + } if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode) { - GameAnalyticsManager.AddDesignEvent($"Tutorial:{tutorialMode.Tutorial?.Identifier}:{segmentId}:Completed"); - } - else if (GameMain.GameSession?.GameMode is CampaignMode campaign) - { - GameAnalyticsManager.AddDesignEvent($"Tutorial:CampaignMode:{segmentId}:Completed"); - campaign?.CampaignMetadata?.SetValue(segmentId, true); + GameAnalyticsManager.AddDesignEvent($"Tutorial:{tutorialMode.Tutorial?.Identifier}:{segment.Id}:{(failed ? "Failed" : "Completed")}"); } } - public static bool MarkSegmentCompleted(Segment segment, bool flash = true) + private static bool MarkSegmentCompleted(Segment segment, bool flash = true) + { + return MarkSegment(segment, "ObjectiveIndicatorCompleted", flash, flashColor: GUIStyle.Green); + } + + private static bool MarkSegmentFailed(Segment segment, bool flash = true) + { + return MarkSegment(segment, "MissionFailedIcon", flash, flashColor: GUIStyle.Red); + } + + private static bool MarkSegment(Segment segment, string iconStyleName, bool flash, Color flashColor) { segment.IsCompleted = true; - if (GUIStyle.GetComponentStyle("ObjectiveIndicatorCompleted") is GUIComponentStyle style) + if (GUIStyle.GetComponentStyle(iconStyleName) is GUIComponentStyle style) { if (segment.ObjectiveStateIndicator.Style == style) { @@ -216,21 +239,17 @@ static class ObjectiveManager } if (flash) { - segment.ObjectiveStateIndicator.Parent.Flash(color: GUIStyle.Green, flashDuration: 0.35f, useRectangleFlash: true); - } + segment.ObjectiveStateIndicator.Parent.Flash(color: flashColor, flashDuration: 0.35f, useRectangleFlash: true); + } segment.ObjectiveButton.OnClicked = null; segment.ObjectiveButton.CanBeFocused = false; return true; } - public static void RemoveTutorialSegment(Identifier segmentId) + public static void RemoveSegment(Identifier segmentId) { if (GetActiveObjective(segmentId) is not Segment segment) { - if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode) - { - DebugConsole.AddWarning($"Warning: tried to remove the tutorial segment \"{segmentId}\" in tutorial \"{tutorialMode.Tutorial?.Identifier}\" but it isn't active!"); - } return; } segment.ObjectiveStateIndicator.FadeOut(ObjectiveComponentAnimationTime, false); @@ -400,10 +419,10 @@ static class ObjectiveManager void SetButtonBehavior(Segment segment) { - segment.ObjectiveButton.CanBeFocused = segment.SegmentType != TutorialSegmentType.Objective; + segment.ObjectiveButton.CanBeFocused = segment.SegmentType != SegmentType.Objective; segment.ObjectiveButton.OnClicked = (GUIButton btn, object userdata) => { - if (segment.SegmentType == TutorialSegmentType.InfoBox) + if (segment.SegmentType == SegmentType.InfoBox) { if (segment.AutoPlayVideo == AutoPlayVideo.Yes) { @@ -414,7 +433,7 @@ static class ObjectiveManager ShowSegmentText(segment); } } - else if (segment.SegmentType == TutorialSegmentType.MessageBox) + else if (segment.SegmentType == SegmentType.MessageBox) { segment.OnClickObjective?.Invoke(); } @@ -438,8 +457,8 @@ static class ObjectiveManager ContentRunning = true; ActiveContentSegment = segment; infoBox = CreateInfoFrame( - TextManager.Get(segment.Id), - TextManager.Get(segment.TextContent.Tag), + TextManager.Get(segment.Id).Fallback(segment.Id.Value), + TextManager.Get(segment.TextContent.Tag).Fallback(segment.TextContent.Tag.Value), segment.TextContent.Width, segment.TextContent.Height, segment.TextContent.Anchor, diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs index 8ba436270..f2d7b680f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs @@ -10,12 +10,12 @@ namespace Barotrauma { internal partial class ReadyCheck { - private static LocalizedString readyCheckBody(string name) => string.IsNullOrWhiteSpace(name) ? TextManager.Get("readycheck.serverbody") : TextManager.GetWithVariable("readycheck.body", "[player]", name); + private static LocalizedString ReadyCheckBody(string name) => string.IsNullOrWhiteSpace(name) ? TextManager.Get("readycheck.serverbody") : TextManager.GetWithVariable("readycheck.body", "[player]", name); - private static LocalizedString readyCheckStatus(int ready, int total) => TextManager.GetWithVariables("readycheck.readycount", + private static LocalizedString ReadyCheckStatus(int ready, int total) => TextManager.GetWithVariables("readycheck.readycount", ("[ready]", ready.ToString()), ("[total]", total.ToString())); - private static LocalizedString readyCheckPleaseWait(int seconds) => TextManager.GetWithVariable("readycheck.pleasewait", "[seconds]", seconds.ToString()); + private static LocalizedString ReadyCheckPleaseWait(int seconds) => TextManager.GetWithVariable("readycheck.pleasewait", "[seconds]", seconds.ToString()); private static readonly LocalizedString readyCheckHeader = TextManager.Get("ReadyCheck.Title"); @@ -42,7 +42,7 @@ namespace Barotrauma { Vector2 relativeSize = new Vector2(0.2f / GUI.AspectRatioAdjustment, 0.15f); Point minSize = new Point(300, 200); - msgBox = new GUIMessageBox(readyCheckHeader, readyCheckBody(author), new[] { yesButton, noButton }, relativeSize, minSize, type: GUIMessageBox.Type.Vote) { UserData = PromptData, Draggable = true }; + msgBox = new GUIMessageBox(readyCheckHeader, ReadyCheckBody(author), new[] { yesButton, noButton }, relativeSize, minSize, type: GUIMessageBox.Type.Vote) { UserData = PromptData, Draggable = true }; GUILayoutGroup contentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.125f), msgBox.Content.RectTransform), childAnchor: Anchor.Center); new GUIProgressBar(new RectTransform(new Vector2(0.8f, 1f), contentLayout.RectTransform), 0.0f, GUIStyle.Orange) { UserData = TimerData }; @@ -82,9 +82,15 @@ namespace Barotrauma GUIListBox listBox = new GUIListBox(new RectTransform(new Vector2(1f, 0.8f), resultsBox.Content.RectTransform)) { UserData = UserListData }; - foreach (var (id, _) in Clients) + foreach (var (id, status) in Clients) { Client? client = GameMain.Client.ConnectedClients.FirstOrDefault(c => c.SessionId == id); + if (client == null) + { + string list = GameMain.Client.ConnectedClients.Aggregate("Available clients:\n", (current, c) => current + $"{c.SessionId}: {c.Name}\n"); + DebugConsole.AddWarning($"Client ID {id} was reported in ready check but was not found.\n" + list.TrimEnd('\n')); + continue; + } GUIFrame container = new GUIFrame(new RectTransform(new Vector2(1f, 0.15f), listBox.Content.RectTransform), style: "ListBoxElement") { UserData = id }; GUILayoutGroup frame = new GUILayoutGroup(new RectTransform(Vector2.One, container.RectTransform), isHorizontal: true) { Stretch = true }; @@ -92,11 +98,6 @@ namespace Barotrauma JobPrefab? jobPrefab = client?.Character?.Info?.Job?.Prefab; - if (client == null) - { - string list = GameMain.Client.ConnectedClients.Aggregate("Available clients:\n", (current, c) => current + $"{c.SessionId}: {c.Name}\n"); - DebugConsole.ThrowError($"Client ID {id} was reported in ready check but was not found.\n" + list.TrimEnd('\n')); - } if (jobPrefab?.Icon != null) { @@ -105,7 +106,8 @@ namespace Barotrauma } new GUITextBlock(new RectTransform(new Vector2(0.75f, 1), frame.RectTransform), client?.Name ?? $"Unknown ID {id}", jobPrefab?.UIColor ?? Color.White, textAlignment: Alignment.Center) { AutoScaleHorizontal = true }; - new GUIImage(new RectTransform(new Point(height, height), frame.RectTransform), null, scaleToFit: true) { UserData = ReadySpriteData }; + var statusIcon = new GUIImage(new RectTransform(new Point(height, height), frame.RectTransform), null, scaleToFit: true) { UserData = ReadySpriteData }; + UpdateStatusIcon(statusIcon, status); } resultsBox.Buttons[0].OnClicked = delegate @@ -214,10 +216,7 @@ namespace Barotrauma case ReadyCheckState.Update: ReadyStatus newState = (ReadyStatus)inc.ReadByte(); byte targetId = inc.ReadByte(); - if (crewManager.ActiveReadyCheck != null) - { - crewManager.ActiveReadyCheck?.UpdateState(targetId, newState); - } + crewManager.ActiveReadyCheck?.UpdateState(targetId, newState); break; case ReadyCheckState.End: ushort count = inc.ReadUInt16(); @@ -242,7 +241,7 @@ namespace Barotrauma int readyCount = Clients.Count(static pair => pair.Value == ReadyStatus.Yes); int totalCount = Clients.Count; - GameMain.Client.AddChatMessage(ChatMessage.Create(string.Empty, readyCheckStatus(readyCount, totalCount).Value, ChatMessageType.Server, null)); + GameMain.Client.AddChatMessage(ChatMessage.Create(string.Empty, ReadyCheckStatus(readyCount, totalCount).Value, ChatMessageType.Server, null)); } private void UpdateState(byte id, ReadyStatus status) @@ -253,31 +252,28 @@ namespace Barotrauma } if (resultsBox == null || resultsBox.Closed || !GUIMessageBox.MessageBoxes.Contains(resultsBox)) { return; } - if (resultsBox.Content.FindChild(UserListData) is not GUIListBox userList) { return; } - // for some reason FindChild doesn't work here? - foreach (GUIComponent child in userList.Content.Children) + var child = userList.Content.FindChild(id); + if (child?.GetChild().FindChild(ReadySpriteData) is not GUIImage image) { return; } + UpdateStatusIcon(image, status); + } + + private static void UpdateStatusIcon(GUIImage image, ReadyStatus status) + { + string style; + switch (status) { - if (child.UserData is not byte b || b != id) { continue; } - - if (child.GetChild().FindChild(ReadySpriteData) is not GUIImage image) { continue; } - - string style; - switch (status) - { - case ReadyStatus.Yes: - style = "MissionCompletedIcon"; - break; - case ReadyStatus.No: - style = "MissionFailedIcon"; - break; - default: - return; - } - - image.ApplyStyle(GUIStyle.GetComponentStyle(style)); + case ReadyStatus.Yes: + style = "MissionCompletedIcon"; + break; + case ReadyStatus.No: + style = "MissionFailedIcon"; + break; + default: + return; } + image.ApplyStyle(GUIStyle.GetComponentStyle(style)); } private static void SendState(ReadyStatus status) @@ -303,7 +299,7 @@ namespace Barotrauma return; } - GUIMessageBox msgBox = new GUIMessageBox(readyCheckHeader, readyCheckPleaseWait((ReadyCheckCooldown - DateTime.Now).Seconds), new[] { closeButton }); + GUIMessageBox msgBox = new GUIMessageBox(readyCheckHeader, ReadyCheckPleaseWait((ReadyCheckCooldown - DateTime.Now).Seconds), new[] { closeButton }); msgBox.Buttons[0].OnClicked = delegate { msgBox.Close(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index f6730cbd5..96fb2522a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -10,6 +10,9 @@ namespace Barotrauma { class RoundSummary { + private float crewListAnimDelay = 0.25f; + private float missionIconAnimDelay; + private const float jobColumnWidthPercentage = 0.11f; private const float characterColumnWidthPercentage = 0.44f; private const float statusColumnWidthPercentage = 0.45f; @@ -44,7 +47,7 @@ namespace Barotrauma } } - public GUIFrame CreateSummaryFrame(GameSession gameSession, string endMessage, List traitorResults, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None) + public GUIFrame CreateSummaryFrame(GameSession gameSession, string endMessage, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None, TraitorManager.TraitorResults? traitorResults = null) { bool singleplayer = GameMain.NetworkMember == null; bool gameOver = @@ -70,7 +73,7 @@ namespace Barotrauma //crew panel ------------------------------------------------------------------------------- - GUIFrame crewFrame = new GUIFrame(new RectTransform(new Vector2(0.35f, 0.45f), background.RectTransform, Anchor.TopCenter, minSize: new Point(minWidth, minHeight))); + GUIFrame crewFrame = new GUIFrame(new RectTransform(new Vector2(0.35f, 0.4f), 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)) @@ -82,7 +85,14 @@ namespace Barotrauma TextManager.Get("crew"), textAlignment: Alignment.TopLeft, font: GUIStyle.SubHeadingFont); crewHeader.RectTransform.MinSize = new Point(0, GUI.IntScale(crewHeader.Rect.Height * 2.0f)); - CreateCrewList(crewContent, gameSession.CrewManager.GetCharacterInfos().Where(c => c.TeamID != CharacterTeamType.Team2)); + var crewList = CreateCrewList(crewContent, gameSession.CrewManager.GetCharacterInfos().Where(c => c.TeamID != CharacterTeamType.Team2), traitorResults); + if (traitorResults != null && traitorResults.Value.VotedAsTraitorClientSessionId > 0) + { + var traitorInfoPanel = CreateTraitorInfoPanel(crewList.Content, traitorResults.Value, crewListAnimDelay); + traitorInfoPanel.RectTransform.SetAsFirstChild(); + var spacing = new GUIFrame(new RectTransform(new Point(0, GUI.IntScale(20)), crewList.Content.RectTransform), style: null); + spacing.RectTransform.RepositionChildInHierarchy(1); + } //another crew frame for the 2nd team in combat missions if (gameSession.Missions.Any(m => m is CombatMission)) @@ -98,7 +108,7 @@ namespace Barotrauma var crewHeader2 = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), crewContent2.RectTransform), CombatMission.GetTeamName(CharacterTeamType.Team2), textAlignment: Alignment.TopLeft, font: GUIStyle.SubHeadingFont); crewHeader2.RectTransform.MinSize = new Point(0, GUI.IntScale(crewHeader2.Rect.Height * 2.0f)); - CreateCrewList(crewContent2, gameSession.CrewManager.GetCharacterInfos().Where(c => c.TeamID == CharacterTeamType.Team2)); + CreateCrewList(crewContent2, gameSession.CrewManager.GetCharacterInfos().Where(c => c.TeamID == CharacterTeamType.Team2), traitorResults); } //header ------------------------------------------------------------------------------- @@ -111,71 +121,6 @@ namespace Barotrauma headerText, textAlignment: Alignment.BottomLeft, font: GUIStyle.LargeFont, wrap: true); } - //traitor panel ------------------------------------------------------------------------------- - - if (traitorResults != null && traitorResults.Any()) - { - 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 traitorContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), traitorframeInner.RectTransform, Anchor.Center)) - { - Stretch = true - }; - - var traitorHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), traitorContent.RectTransform), - TextManager.Get("traitors"), font: GUIStyle.SubHeadingFont); - traitorHeader.RectTransform.MinSize = new Point(0, GUI.IntScale(traitorHeader.Rect.Height * 2.0f)); - - GUIListBox listBox = CreateCrewList(traitorContent, traitorResults.SelectMany(tr => tr.Characters.Select(c => c.Info))); - - foreach (var traitorResult in traitorResults) - { - var traitorMission = TraitorMissionPrefab.Prefabs.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) - { - RelativeSpacing = 0.05f, - Stretch = true - }; - - new GUIImage(new RectTransform(new Point(traitorResultHorizontal.Rect.Height), traitorResultHorizontal.RectTransform), traitorMission.Icon, scaleToFit: true) - { - Color = traitorMission.IconColor - }; - - LocalizedString traitorMessage = TextManager.GetServerMessage(traitorResult.EndMessage); - if (!traitorMessage.IsNullOrEmpty()) - { - 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 ? GUIStyle.Green : GUIStyle.Red, font: GUIStyle.SubHeadingFont); - - var traitorMissionInfo = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), textContent.RectTransform), - traitorMessage, font: GUIStyle.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); - } - } - } - //reputation panel ------------------------------------------------------------------------------- var campaignMode = gameMode as CampaignMode; @@ -185,7 +130,7 @@ namespace Barotrauma 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)) + var reputationContent = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.95f), reputationframeInner.RectTransform, Anchor.Center)) { Stretch = true }; @@ -199,15 +144,15 @@ namespace Barotrauma //mission panel ------------------------------------------------------------------------------- - GUIFrame missionframe = new GUIFrame(new RectTransform(new Vector2(0.39f, 0.3f), background.RectTransform, Anchor.TopCenter, minSize: new Point(minWidth, minHeight / 4))); + GUIFrame missionframe = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.4f), background.RectTransform, Anchor.TopCenter, minSize: new Point(minWidth, minHeight / 4))); GUILayoutGroup missionFrameContent = new GUILayoutGroup(new RectTransform(new Point(missionframe.Rect.Width - padding * 2, missionframe.Rect.Height - padding * 2), missionframe.RectTransform, Anchor.Center)) { Stretch = true, - RelativeSpacing = 0.05f + RelativeSpacing = 0.03f }; GUIFrame missionframeInner = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), missionFrameContent.RectTransform, Anchor.Center), style: "InnerFrame"); - var missionContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.93f), missionframeInner.RectTransform, Anchor.Center)) + var missionContent = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.93f), missionframeInner.RectTransform, Anchor.Center)) { Stretch = true }; @@ -227,16 +172,9 @@ namespace Barotrauma } } - if (missionsToDisplay.Any()) - { - var missionHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionContent.RectTransform), - TextManager.Get(missionsToDisplay.Count > 1 ? "Missions" : "Mission"), textAlignment: Alignment.TopLeft, font: GUIStyle.SubHeadingFont); - missionHeader.RectTransform.MinSize = new Point(0, (int)(missionHeader.Rect.Height * 1.2f)); - } - GUIListBox missionList = new GUIListBox(new RectTransform(Vector2.One, missionContent.RectTransform, Anchor.Center)) { - Padding = new Vector4(4, 10, 0, 0) * GUI.Scale + Spacing = GUI.IntScale(15) }; missionList.ContentBackground.Color = Color.Transparent; @@ -255,120 +193,76 @@ namespace Barotrauma { CanBeFocused = false }; - endText.RectTransform.MinSize = new Point(0, endText.Rect.Height); - var line = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.1f), missionList.Content.RectTransform), style: "HorizontalLine"); - line.RectTransform.NonScaledSize = new Point(line.Rect.Width, GUI.IntScale(5.0f)); } - foreach (Mission displayedMission in missionsToDisplay) + float animDelay = missionIconAnimDelay; + foreach (Mission mission in missionsToDisplay) { - var missionContentHorizontal = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.8f), missionList.Content.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true) - { - RelativeSpacing = 0.025f, - Stretch = true - }; + var textContent = new List(); - LocalizedString missionMessage = - selectedMissions.Contains(displayedMission) ? - displayedMission.Completed ? displayedMission.SuccessMessage : displayedMission.FailureMessage : - displayedMission.Description; - GUIImage missionIcon = new GUIImage(new RectTransform(new Point((int)(missionContentHorizontal.Rect.Height)), missionContentHorizontal.RectTransform), displayedMission.Prefab.Icon, scaleToFit: true) + if (selectedMissions.Contains(mission)) { - Color = displayedMission.Prefab.IconColor, - HoverColor = displayedMission.Prefab.IconColor, - SelectedColor = displayedMission.Prefab.IconColor - }; - missionIcon.RectTransform.MinSize = new Point((int)(missionContentHorizontal.Rect.Height * 0.9f)); - if (selectedMissions.Contains(displayedMission)) - { - new GUIImage(new RectTransform(Vector2.One, missionIcon.RectTransform), displayedMission.Completed ? "MissionCompletedIcon" : "MissionFailedIcon", scaleToFit: true); - } + textContent.Add(mission.Completed ? mission.SuccessMessage : mission.FailureMessage); - var missionTextContent = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 1.0f), missionContentHorizontal.RectTransform)) - { - AbsoluteSpacing = GUI.IntScale(5) - }; - missionContentHorizontal.Recalculate(); - var missionNameTextBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), - displayedMission.Name, font: GUIStyle.SubHeadingFont); - if (displayedMission.Difficulty.HasValue) - { - var groupSize = missionNameTextBlock.Rect.Size; - groupSize.X -= (int)(missionNameTextBlock.Padding.X + missionNameTextBlock.Padding.Z); - var indicatorGroup = new GUILayoutGroup(new RectTransform(groupSize, missionTextContent.RectTransform) { AbsoluteOffset = new Point((int)missionNameTextBlock.Padding.X, 0) }, - isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - AbsoluteSpacing = 1 - }; - var difficultyColor = displayedMission.GetDifficultyColor(); - for (int i = 0; i < displayedMission.Difficulty; i++) - { - new GUIImage(new RectTransform(Vector2.One, indicatorGroup.RectTransform, scaleBasis: ScaleBasis.Smallest) { IsFixedSize = true }, "DifficultyIndicator", scaleToFit: true) - { - CanBeFocused = false, - Color = difficultyColor - }; - } - } + var repText = mission.GetReputationRewardText(); + if (!repText.IsNullOrEmpty()) { textContent.Add(repText); } - var missionDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), - RichString.Rich(missionMessage), wrap: true); - if (selectedMissions.Contains(displayedMission)) - { - RichString reputationText = displayedMission.GetReputationRewardText(); - if (!reputationText.IsNullOrEmpty()) - { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), reputationText, wrap: true); - } - - int totalReward = displayedMission.GetFinalReward(Submarine.MainSub); + int totalReward = mission.GetFinalReward(Submarine.MainSub); if (totalReward > 0) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(displayedMission.GetMissionRewardText(Submarine.MainSub))); - if (GameMain.IsMultiplayer && Character.Controlled is { } controlled && displayedMission.Completed) + textContent.Add(mission.GetMissionRewardText(Submarine.MainSub)); + if (GameMain.IsMultiplayer && Character.Controlled is { } controlled && mission.Completed) { var (share, percentage, _) = Mission.GetRewardShare(controlled.Wallet.RewardDistribution, GameSession.GetSessionCrewCharacters(CharacterType.Player).Where(c => c != controlled), Option.Some(totalReward)); if (share > 0) { string shareFormatted = string.Format(CultureInfo.InvariantCulture, "{0:N0}", share); - RichString yourShareString = RichString.Rich(TextManager.GetWithVariables("crewwallet.missionreward.get", ("[money]", $"{shareFormatted}"), ("[share]", $"{percentage}"))); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), yourShareString); + RichString yourShareString = TextManager.GetWithVariables("crewwallet.missionreward.get", ("[money]", $"{shareFormatted}"), ("[share]", $"{percentage}")); + textContent.Add(yourShareString); } } } } - - if (displayedMission != missionsToDisplay.Last()) + else { - var spacing = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.0f), missionList.Content.RectTransform) { MaxSize = new Point(int.MaxValue, GUI.IntScale(15)) }, style: null); - new GUIFrame(new RectTransform(new Vector2(0.8f, 1.0f), spacing.RectTransform, Anchor.Center) { RelativeOffset = new Vector2(0.1f, 0.0f) }, "HorizontalLine"); + var repText = mission.GetReputationRewardText(); + if (!repText.IsNullOrEmpty()) { textContent.Add(repText); } + textContent.Add(mission.GetMissionRewardText(Submarine.MainSub)); + textContent.Add(mission.Description); + textContent.AddRange(mission.ShownMessages); } - foreach (GUIComponent child in missionTextContent.Children) + CreateMissionEntry( + missionList.Content, + mission.Name, + textContent, + mission.Difficulty ?? 0, + mission.Prefab.Icon, mission.Prefab.IconColor, + out GUIImage missionIcon); + + if (selectedMissions.Contains(mission)) { - child.RectTransform.IsFixedSize = true; + UpdateMissionStateIcon(mission.Completed, missionIcon, animDelay); + animDelay += 0.25f; } - missionTextContent.RectTransform.MinSize = new Point(0, missionTextContent.Children.Sum(c => c.Rect.Height + missionTextContent.AbsoluteSpacing)); - missionContentHorizontal.RectTransform.MinSize = new Point(0, (int)(missionTextContent.Rect.Height / missionTextContent.RectTransform.RelativeSize.Y)); } if (!missionsToDisplay.Any()) { - var missionContentHorizontal = new GUILayoutGroup(new RectTransform(Vector2.One, missionList.Content.RectTransform), childAnchor: Anchor.TopLeft, isHorizontal: true) + var missionContentHorizontal = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.4f), missionList.Content.RectTransform), childAnchor: Anchor.TopLeft, isHorizontal: true) { RelativeSpacing = 0.025f, - Stretch = true + Stretch = true, + CanBeFocused = true }; - GUIImage missionIcon = new GUIImage(new RectTransform(new Point((int)(missionContentHorizontal.Rect.Height * 0.7f)), missionContentHorizontal.RectTransform), style: "NoMissionIcon", scaleToFit: true); - missionIcon.RectTransform.MinSize = new Point((int)(missionContentHorizontal.Rect.Height * 0.7f)); + GUIImage missionIcon = new GUIImage(new RectTransform(new Point(missionContentHorizontal.Rect.Height), missionContentHorizontal.RectTransform), style: "NoMissionIcon", scaleToFit: true); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionContentHorizontal.RectTransform), TextManager.Get("nomission"), font: GUIStyle.LargeFont); } - /*missionContentHorizontal.Recalculate(); - missionContent.Recalculate(); - missionIcon.RectTransform.MinSize = new Point(0, missionContentHorizontal.Rect.Height); - missionTextContent.RectTransform.MaxSize = new Point(int.MaxValue, missionIcon.Rect.Width);*/ + gameSession?.EventManager?.EventLog?.CreateEventLogUI(missionList.Content, traitorResults); + + AddSeparators(missionList.Content); 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); @@ -405,10 +299,7 @@ namespace Barotrauma public void CreateReputationInfoPanel(GUIComponent parent, CampaignMode campaignMode) { - GUIListBox reputationList = new GUIListBox(new RectTransform(Vector2.One, parent.RectTransform)) - { - Padding = new Vector4(4, 10, 0, 0) * GUI.Scale - }; + GUIListBox reputationList = new GUIListBox(new RectTransform(Vector2.One, parent.RectTransform)); reputationList.ContentBackground.Color = Color.Transparent; foreach (Faction faction in campaignMode.Factions.OrderBy(f => f.Prefab.MenuOrder).ThenBy(f => f.Prefab.Name)) @@ -506,6 +397,171 @@ namespace Barotrauma } } + private static GUIComponent CreateTraitorInfoPanel(GUIComponent parent, TraitorManager.TraitorResults traitorResults, float iconAnimDelay) + { + var traitorCharacter = traitorResults.GetTraitorClient()?.Character; + + string resultTag = + traitorResults.VotedCorrectTraitor ? + traitorResults.ObjectiveSuccessful ? "traitor.blameresult.correct.objectivesuccessful" : "traitor.blameresult.correct.objectivefailed" : + "traitor.blameresult.failure"; + + var textContent = new List() + { + TextManager.GetWithVariable("traitor.blameresult", "[name]", traitorCharacter?.Name ?? "unknown"), + TextManager.Get(resultTag) + }; + + if (traitorResults.MoneyPenalty > 0) + { + textContent.Add( + TextManager.GetWithVariable( + "traitor.blameresult.failure.penalty", + "[money]", + TextManager.FormatCurrency(traitorResults.MoneyPenalty, includeCurrencySymbol: false))); + } + + var icon = GUIStyle.GetComponentStyle("TraitorMissionIcon")?.GetDefaultSprite(); + + var content = CreateMissionEntry( + parent, + string.Empty, + textContent, + difficultyIconCount: 0, + icon, GUIStyle.Red, + out GUIImage missionIcon); + UpdateMissionStateIcon(traitorResults.VotedCorrectTraitor, missionIcon, iconAnimDelay); + return content; + } + + public static GUIComponent CreateMissionEntry(GUIComponent parent, LocalizedString header, List textContent, int difficultyIconCount, + Sprite icon, Color iconColor, out GUIImage missionIcon) + { + int spacing = GUI.IntScale(5); + + int defaultLineHeight = (int)GUIStyle.Font.MeasureChar('T').Y; + + //make the icon big enough for header + some lines of text + int iconSize = (int)(GUIStyle.SubHeadingFont.MeasureChar('T').Y + defaultLineHeight * 6); + + GUILayoutGroup content = new GUILayoutGroup(new RectTransform(new Point(parent.Rect.Width, iconSize), parent.RectTransform), isHorizontal: true) + { + Stretch = true, + AbsoluteSpacing = spacing, + CanBeFocused = true + }; + if (icon != null) + { + missionIcon = new GUIImage(new RectTransform(new Point(iconSize), content.RectTransform), icon, null, true) + { + Color = iconColor, + HoverColor = iconColor, + SelectedColor = iconColor, + CanBeFocused = false + }; + missionIcon.RectTransform.IsFixedSize = true; + } + else + { + missionIcon = null; + } + GUILayoutGroup missionTextGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.744f, 0f), content.RectTransform, Anchor.CenterLeft), childAnchor: Anchor.TopLeft) + { + AbsoluteSpacing = spacing + }; + content.Recalculate(); + + RichString missionNameString = RichString.Rich(header); + List contentStrings = new List(textContent.Select(t => RichString.Rich(t))); + + if (!header.IsNullOrEmpty()) + { + var nameText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), + missionNameString, font: GUIStyle.SubHeadingFont, wrap: true); + nameText.RectTransform.MinSize = new Point(0, (int)nameText.TextSize.Y); + } + + GUILayoutGroup difficultyIndicatorGroup = null; + if (difficultyIconCount > 0) + { + difficultyIndicatorGroup = new GUILayoutGroup(new RectTransform(new Point(missionTextGroup.Rect.Width, defaultLineHeight), parent: missionTextGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + AbsoluteSpacing = 1 + }; + difficultyIndicatorGroup.RectTransform.MinSize = new Point(0, defaultLineHeight); + var difficultyColor = Mission.GetDifficultyColor(difficultyIconCount); + for (int i = 0; i < difficultyIconCount; i++) + { + new GUIImage(new RectTransform(Vector2.One, difficultyIndicatorGroup.RectTransform, scaleBasis: ScaleBasis.Smallest), "DifficultyIndicator", scaleToFit: true) + { + CanBeFocused = false, + Color = difficultyColor + }; + } + } + + GUITextBlock firstContentText = null; + foreach (var contentString in contentStrings) + { + var text = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), contentString, wrap: true); + text.RectTransform.MinSize = new Point(0, (int)text.TextSize.Y); + firstContentText ??= text; + } + if (difficultyIndicatorGroup != null && firstContentText != null) + { + //make the icons align with the text content + difficultyIndicatorGroup.RectTransform.AbsoluteOffset = new Point((int)firstContentText.Padding.X, 0); + } + missionTextGroup.RectTransform.MinSize = + new Point(0, missionTextGroup.Children.Sum(c => c.Rect.Height + missionTextGroup.AbsoluteSpacing) - missionTextGroup.AbsoluteSpacing); + missionTextGroup.Recalculate(); + content.RectTransform.MinSize = new Point(0, Math.Max(missionTextGroup.Rect.Height, iconSize)); + + return content; + } + + public static void AddSeparators(GUIComponent container) + { + var children = container.Children.ToList(); + if (children.Count < 2) { return; } + + var lastChild = children.Last(); + foreach (var child in children) + { + if (child != lastChild) + { + var separator = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.1f), container.RectTransform), style: "HorizontalLine"); + separator.RectTransform.RepositionChildInHierarchy(container.GetChildIndex(child) + 1); + } + } + } + + public static void UpdateMissionStateIcon(bool success, GUIImage missionIcon, float delay = 0.5f) + { + if (missionIcon == null) { return; } + string style = success ? "MissionCompletedIcon" : "MissionFailedIcon"; + GUIImage stateIcon = missionIcon.GetChild(); + if (string.IsNullOrEmpty(style)) + { + if (stateIcon != null) + { + stateIcon.Visible = false; + } + } + else + { + bool wasVisible = stateIcon is { Visible: true }; + stateIcon ??= new GUIImage(new RectTransform(Vector2.One, missionIcon.RectTransform, Anchor.Center), style, scaleToFit: true); + stateIcon.Visible = true; + if (!wasVisible) + { + stateIcon.FadeIn(delay, 0.15f); + stateIcon.Pulsate(Vector2.One, Vector2.One * 1.5f, 1.0f + delay); + } + } + } + + private LocalizedString GetHeaderText(bool gameOver, CampaignMode.TransitionType transitionType) { string locationName = Submarine.MainSub is { AtEndExit: true } ? endLocation?.Name : startLocation?.Name; @@ -568,7 +624,7 @@ namespace Barotrauma return TextManager.GetWithVariables(textTag, ("[sub]", subName), ("[location]", locationName)); } - private GUIListBox CreateCrewList(GUIComponent parent, IEnumerable characterInfos) + private GUIListBox CreateCrewList(GUIComponent parent, IEnumerable characterInfos, TraitorManager.TraitorResults? traitorResults) { 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) { @@ -602,16 +658,19 @@ namespace Barotrauma headerFrame.RectTransform.RelativeSize -= new Vector2(crewList.ScrollBar.RectTransform.RelativeSize.X, 0.0f); + float delay = crewListAnimDelay; foreach (CharacterInfo characterInfo in characterInfos) { if (characterInfo == null) { continue; } - CreateCharacterElement(characterInfo, crewList); + CreateCharacterElement(characterInfo, crewList, traitorResults, delay); + delay += crewListAnimDelay; } + missionIconAnimDelay = delay; return crewList; } - private void CreateCharacterElement(CharacterInfo characterInfo, GUIListBox listBox) + private void CreateCharacterElement(CharacterInfo characterInfo, GUIListBox listBox, TraitorManager.TraitorResults? traitorResults, float animDelay) { GUIFrame frame = new GUIFrame(new RectTransform(new Point(listBox.Content.Rect.Width, GUI.IntScale(45)), listBox.Content.RectTransform), style: "ListBoxElement") { @@ -684,9 +743,31 @@ namespace Barotrauma GUITextBlock statusBlock = new GUITextBlock(new RectTransform(new Point(statusColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), ToolBox.LimitString(statusText.Value, GUIStyle.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: statusColor); + + frame.FadeIn(animDelay, 0.15f); + foreach (var child in frame.GetAllChildren()) + { + child.FadeIn(animDelay, 0.15f); + } + + if (traitorResults.HasValue && GameMain.NetworkMember != null) + { + var clientVotedAsTraitor = traitorResults.Value.GetTraitorClient(); + bool isTraitor = clientVotedAsTraitor != null && clientVotedAsTraitor.Character == character; + if (isTraitor) + { + var img = new GUIImage(new RectTransform(new Point(paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.CenterRight), style: "TraitorVoteButton") + { + IgnoreLayoutGroups = true, + ToolTip = TextManager.GetWithVariable("traitor.blameresult", "[name]", characterInfo.Name) + }; + img.FadeIn(1.0f + animDelay, 0.15f); + img.Pulsate(Vector2.One, Vector2.One * 1.5f, 1.5f + animDelay); + } + } } - private GUIFrame CreateReputationElement(GUIComponent parent, + private static GUIFrame CreateReputationElement(GUIComponent parent, LocalizedString name, Reputation reputation, float initialReputation, LocalizedString shortDescription, LocalizedString fullDescription, Sprite icon, Sprite backgroundPortrait, Color iconColor) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 289b08e4b..54b27c3e0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -1,5 +1,6 @@ using Barotrauma.Extensions; using Barotrauma.Items.Components; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; @@ -42,16 +43,29 @@ namespace Barotrauma 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))); + foreach (InvSlotType invSlotType in Enum.GetValues(typeof(InvSlotType))) + { + var sprite = GUIStyle.GetComponentStyle($"InventorySlot.{invSlotType}")?.GetDefaultSprite(); + if (sprite != null) + { + limbSlotIcons.Add(invSlotType, sprite); + } + } - 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))); - limbSlotIcons.Add(InvSlotType.Bag, new Sprite("Content/UI/CommandUIAtlas.png", new Rectangle(639, 926, 128,80))); + int margin = 2; + AddIfMissing(InvSlotType.Headset, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(384 + margin, 128 + margin, 128 - margin * 2, 128 - margin * 2))); + AddIfMissing(InvSlotType.InnerClothes, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(512 + margin, 128 + margin, 128 - margin * 2, 128 - margin * 2))); + AddIfMissing(InvSlotType.Card, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(640 + margin, 128 + margin, 128 - margin * 2, 128 - margin * 2))); + AddIfMissing(InvSlotType.Head, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(896 + margin, 128 + margin, 128 - margin * 2, 128 - margin * 2))); + AddIfMissing(InvSlotType.LeftHand, new Sprite("Content/UI/InventoryUIAtlas.png", new Rectangle(634, 0, 128, 128))); + AddIfMissing(InvSlotType.RightHand, new Sprite("Content/UI/InventoryUIAtlas.png", new Rectangle(762, 0, 128, 128))); + AddIfMissing(InvSlotType.OuterClothes, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(256 + margin, 128 + margin, 128 - margin * 2, 128 - margin * 2))); + AddIfMissing(InvSlotType.Bag, new Sprite("Content/UI/CommandUIAtlas.png", new Rectangle(639, 926, 128,80))); + + static void AddIfMissing(InvSlotType slotType, Sprite sprite) + { + limbSlotIcons.TryAdd(slotType, sprite); + } } return limbSlotIcons; } @@ -179,11 +193,30 @@ namespace Barotrauma BackgroundFrame = frame; } - protected override bool HideSlot(int i) + public override bool HideSlot(int i) { if (visualSlots[i].Disabled || (slots[i].HideIfEmpty && slots[i].Empty())) { return true; } - if (CharacterHealth.OpenHealthWindow != Character.Controlled?.CharacterHealth && SlotTypes[i] == InvSlotType.HealthInterface) { return true; } + if (SlotTypes[i] == InvSlotType.HealthInterface) + { + //hide health interface slot unless this character's health window is open + if (CharacterHealth.OpenHealthWindow == null || Character.Controlled == null) + { + return true; + } + if (character == Character.Controlled) + { + bool ownHealthWindowOpen = CharacterHealth.OpenHealthWindow == Character.Controlled.CharacterHealth; + if (!ownHealthWindowOpen) { return true; } + } + else if (character == Character.Controlled.SelectedCharacter) + { + bool otherCharacterHealthWindowOpen = CharacterHealth.OpenHealthWindow == Character.Controlled?.SelectedCharacter?.CharacterHealth; + if (!otherCharacterHealthWindowOpen) { return true; } + //can't access the health interface slot of a non-incapacitated player (bots are fine though) + if (character.IsPlayer && !character.IsIncapacitated) { return true; } + } + } if (layout == Layout.Default) { @@ -216,6 +249,11 @@ namespace Barotrauma return false; } + public void RefreshSlotPositions() + { + SetSlotPositions(CurrentLayout); + } + private void SetSlotPositions(Layout layout) { int spacing = GUI.IntScale(5); @@ -450,6 +488,9 @@ namespace Barotrauma ((s.SlotIndex < 0 || s.SlotIndex >= slots.Length || slots[s.SlotIndex] == null) || (Character.Controlled != null && !Character.Controlled.CanAccessInventory(s.Inventory)))); //remove highlighted subinventory slots that refer to items no longer in this inventory highlightedSubInventorySlots.RemoveWhere(s => s.Item != null && s.ParentInventory == this && s.Item.ParentInventory != this); + //remove highlighted subinventory slots if we're dragging that item out of the inventory + highlightedSubInventorySlots.RemoveWhere(s => s.Item != null && s.ParentInventory == this && DraggingItems.Contains(s.Item)); + tempHighlightedSubInventorySlots.Clear(); tempHighlightedSubInventorySlots.AddRange(highlightedSubInventorySlots); foreach (var highlightedSubInventorySlot in tempHighlightedSubInventorySlots) @@ -525,6 +566,7 @@ namespace Barotrauma var itemContainer = item.GetComponent(); if (itemContainer != null && itemContainer.KeepOpenWhenEquippedBy(character) && + !DraggingItems.Contains(item) && character.CanAccessInventory(itemContainer.Inventory) && !highlightedSubInventorySlots.Any(s => s.Inventory == itemContainer.Inventory)) { @@ -538,9 +580,11 @@ namespace Barotrauma if (doubleClickedItems.Any()) { var quickUseAction = GetQuickUseAction(doubleClickedItems.First(), true, true, true); + int itemCount = 0; foreach (Item doubleClickedItem in doubleClickedItems) { QuickUseItem(doubleClickedItem, true, true, true, quickUseAction, playSound: doubleClickedItem == doubleClickedItems.First()); + itemCount++; //only use one item if we're equipping or using it as a treatment if (quickUseAction == QuickUseAction.Equip || quickUseAction == QuickUseAction.UseTreatment) { @@ -556,6 +600,12 @@ namespace Barotrauma { break; } + if ((quickUseAction == QuickUseAction.TakeFromContainer || quickUseAction == QuickUseAction.PutToEquippedItem) && + doubleClickedItem.ParentInventory != null && + itemCount >= doubleClickedItem.Prefab.GetMaxStackSize(doubleClickedItem.ParentInventory)) + { + break; + } } } @@ -789,10 +839,30 @@ namespace Barotrauma { return QuickUseAction.TakeFromCharacter; } - else if (character.HeldItems.Any(i => + else if (character.HeldItems.FirstOrDefault(i => i.OwnInventory != null && - (i.OwnInventory.CanBePut(item) || ((i.OwnInventory.Capacity == 1 || i.OwnInventory.Container.HasSubContainers) && i.OwnInventory.AllowSwappingContainedItems && i.OwnInventory.Container.CanBeContained(item))))) + (i.OwnInventory.CanBePut(item) || ((i.OwnInventory.Capacity == 1 || i.OwnInventory.Container.HasSubContainers) && i.OwnInventory.AllowSwappingContainedItems && i.OwnInventory.Container.CanBeContained(item)))) is { } equippedContainer) { + if (allowEquip) + { + if (!character.HasEquippedItem(item)) + { + if (equippedContainer.GetComponent() is { QuickUseMovesItemsInside: false}) + { + //put the item in a hand slot if that hand is free + if ((item.AllowedSlots.Contains(InvSlotType.RightHand) && character.Inventory.GetItemInLimbSlot(InvSlotType.RightHand) == null) || + (item.AllowedSlots.Contains(InvSlotType.LeftHand) && character.Inventory.GetItemInLimbSlot(InvSlotType.LeftHand) == null)) + { + return QuickUseAction.Equip; + } + } + } + //equipped -> attempt to unequip + else if (item.AllowedSlots.Contains(InvSlotType.Any)) + { + return QuickUseAction.Unequip; + } + } return QuickUseAction.PutToEquippedItem; } else if (allowEquip) //doubleclicked and no other inventory is selected @@ -1171,5 +1241,11 @@ namespace Barotrauma GUIComponent.DrawToolTip(spriteBatch, highlightedQuickUseSlot.QuickUseButtonToolTip, highlightedQuickUseSlot.EquipButtonRect); } } + + public void ClientEventWrite(IWriteMessage msg, Character.InventoryStateEventData extraData) + { + SharedWrite(msg, extraData.SlotRange); + syncItemsDelay = 1.0f; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs index 0be3b921f..f7062ac72 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs @@ -185,9 +185,9 @@ namespace Barotrauma.Items.Components } } - public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) { - Color color = item.GetSpriteColor(withHighlight: true); + Color color = overrideColor ?? item.GetSpriteColor(withHighlight: true); if (brokenSprite == null) { //broken doors turn black if no broken sprite has been configured @@ -202,7 +202,7 @@ namespace Barotrauma.Items.Components weldSpritePos.Y = -weldSpritePos.Y; weldedSprite.Draw(spriteBatch, - weldSpritePos, item.SpriteColor * (stuck / 100.0f), scale: item.Scale); + weldSpritePos, overrideColor ?? (item.SpriteColor * (stuck / 100.0f)), scale: item.Scale); } if (openState >= 1.0f) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/EntitySpawnerComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/EntitySpawnerComponent.cs index a7229fbf9..761d5b394 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/EntitySpawnerComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/EntitySpawnerComponent.cs @@ -8,7 +8,7 @@ namespace Barotrauma.Items.Components { public Vector2 DrawSize => Vector2.Zero; - public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) { if (!editing) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs index 5380f8940..d0f607e20 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs @@ -13,7 +13,7 @@ namespace Barotrauma.Items.Components get { return item.Rect.Size.ToVector2(); } } - public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) { if (!IsActive || picker == null || !CanBeAttached(picker) || !picker.IsKeyDown(InputType.Aim) || picker != Character.Controlled) { @@ -50,7 +50,17 @@ namespace Barotrauma.Items.Components Submarine.DrawGrid(spriteBatch, 14, gridPos, roundedGridPos, alpha: 0.4f); - item.Sprite.Draw( + Sprite sprite = item.Sprite; + foreach (ContainedItemSprite containedSprite in item.Prefab.ContainedSprites) + { + if (containedSprite.UseWhenAttached) + { + sprite = containedSprite.Sprite; + break; + } + } + + sprite.Draw( spriteBatch, new Vector2(attachPos.X, -attachPos.Y), item.SpriteColor * 0.5f, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs index e19b1dd82..e1b21fe1e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs @@ -1,11 +1,8 @@ -using System; +using Barotrauma.IO; using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; -using System.Xml.Linq; -using Barotrauma.Extensions; -using Barotrauma.IO; namespace Barotrauma.Items.Components { @@ -48,33 +45,36 @@ namespace Barotrauma.Items.Components return; } - foreach (ContentXElement limbElement in characterInfo.Ragdoll.MainElement.Elements()) + if (characterInfo.Ragdoll.MainElement?.Elements() is { } limbElements) { - if (!limbElement.GetAttributeString("type", "").Equals("head", StringComparison.OrdinalIgnoreCase)) { continue; } - - ContentXElement spriteElement = limbElement.GetChildElement("sprite"); - if (spriteElement == null) { continue; } - - ContentPath contentPath = spriteElement.GetAttributeContentPath("texture"); - - string spritePath = characterInfo.ReplaceVars(contentPath.Value); - string fileName = Path.GetFileNameWithoutExtension(spritePath); - - //go through the files in the directory to find a matching sprite - foreach (string file in Directory.GetFiles(Path.GetDirectoryName(spritePath))) + foreach (ContentXElement limbElement in limbElements) { - if (!file.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) + if (!limbElement.GetAttributeString("type", "").Equals("head", StringComparison.OrdinalIgnoreCase)) { continue; } + + ContentXElement spriteElement = limbElement.GetChildElement("sprite"); + if (spriteElement == null) { continue; } + + ContentPath contentPath = spriteElement.GetAttributeContentPath("texture"); + + string spritePath = characterInfo.ReplaceVars(contentPath.Value); + string fileName = Path.GetFileNameWithoutExtension(spritePath); + + //go through the files in the directory to find a matching sprite + foreach (string file in Directory.GetFiles(Path.GetDirectoryName(spritePath))) { - continue; + if (!file.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + string fileWithoutTags = Path.GetFileNameWithoutExtension(file); + fileWithoutTags = fileWithoutTags.Split('[', ']').First(); + if (fileWithoutTags != fileName) { continue; } + Portrait = new Sprite(spriteElement, "", file) { RelativeOrigin = Vector2.Zero }; + break; } - string fileWithoutTags = Path.GetFileNameWithoutExtension(file); - fileWithoutTags = fileWithoutTags.Split('[', ']').First(); - if (fileWithoutTags != fileName) { continue; } - Portrait = new Sprite(spriteElement, "", file) { RelativeOrigin = Vector2.Zero }; + break; } - - break; } if (characterInfo.Wearables != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs index cd0b92161..099f54b34 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs @@ -60,7 +60,7 @@ namespace Barotrauma.Items.Components } } - public override void UpdateHUD(Character character, float deltaTime, Camera cam) + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { currentCrossHairScale = currentCrossHairPointerScale = cam == null ? 1.0f : cam.Zoom; if (crosshairSprite != null) @@ -118,6 +118,7 @@ namespace Barotrauma.Items.Components else if (chargeSoundChannel != null) { chargeSoundChannel.FrequencyMultiplier = MathHelper.Lerp(0.5f, 1.5f, chargeRatio); + chargeSoundChannel.Position = new Vector3(item.WorldPosition, 0.0f); } break; default: diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs index 93769ab4b..35f3edba3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs @@ -51,7 +51,7 @@ namespace Barotrauma.Items.Components private int spraySetting = 0; private readonly Point[] sprayArray = new Point[8]; - public override void UpdateHUD(Character character, float deltaTime, Camera cam) + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { if (character == null || !character.IsKeyDown(InputType.Aim)) return; @@ -130,7 +130,7 @@ namespace Barotrauma.Items.Components if (body.UserData is Item item) { var door = item.GetComponent(); - if (door != null && door.CanBeTraversed) { continue; } + if (door != null && (door.IsOpen || door.IsBroken)) { continue; } } targetHull = null; @@ -288,7 +288,7 @@ namespace Barotrauma.Items.Components } } - public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) { #if DEBUG if (GameMain.DebugDraw && Character.Controlled != null && Character.Controlled.IsKeyDown(InputType.Aim)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index ebd078a7e..32dca442a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -150,7 +150,9 @@ namespace Barotrauma.Items.Components public GUIFrame GuiFrame { get; set; } - public bool LockGuiFramePosition; + private GUIDragHandle guiFrameDragHandle; + + private bool guiFrameUpdatePending; [Serialize(false, IsPropertySaveable.No)] public bool AllowUIOverlap @@ -466,7 +468,21 @@ namespace Barotrauma.Items.Components GuiFrame?.AddToGUIUpdateList(order: order); } - public virtual void UpdateHUD(Character character, float deltaTime, Camera cam) { } + public void UpdateHUD(Character character, float deltaTime, Camera cam) + { + UpdateHUDComponentSpecific(character, deltaTime, cam); + if (guiFrameUpdatePending && !PlayerInput.PrimaryMouseButtonHeld()) + { + //send a guiframe position update once the player stops dragging the frame + guiFrameUpdatePending = false; + if (SerializableProperties.TryGetValue(nameof(GuiFrameOffset).ToIdentifier(), out var property)) + { + GameMain.Client?.CreateEntityEvent(Item, new Item.ChangePropertyEventData(property, this)); + } + } + } + + public virtual void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { } public virtual void UpdateEditing(float deltaTime) { } @@ -572,6 +588,7 @@ namespace Barotrauma.Items.Components } string style = GuiFrameSource.Attribute("style") == null ? null : GuiFrameSource.GetAttributeString("style", ""); GuiFrame = new GUIFrame(RectTransform.Load(GuiFrameSource, GUI.Canvas, Anchor.Center), style, color); + GuiFrame.RectTransform.ScreenSpaceOffset = GuiFrameOffset; TryCreateDragHandle(); @@ -583,24 +600,25 @@ namespace Barotrauma.Items.Components GameMain.Instance.ResolutionChanged += OnResolutionChangedPrivate; } - protected virtual void TryCreateDragHandle() + protected void TryCreateDragHandle() { if (GuiFrame != null && GuiFrameSource.GetAttributeBool("draggable", true)) { bool hideDragIcons = GuiFrameSource.GetAttributeBool("hidedragicons", false); - var handle = new GUIDragHandle(new RectTransform(Vector2.One, GuiFrame.RectTransform, Anchor.Center), + guiFrameDragHandle = new GUIDragHandle(new RectTransform(Vector2.One, GuiFrame.RectTransform, Anchor.Center), GuiFrame.RectTransform, style: null) { + Enabled = !LockGuiFramePosition, DragArea = HUDLayoutSettings.ItemHUDArea }; int iconHeight = GUIStyle.ItemFrameMargin.Y / 4; - var dragIcon = new GUIImage(new RectTransform(new Point(GuiFrame.Rect.Width, iconHeight), handle.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, iconHeight / 2) }, + var dragIcon = new GUIImage(new RectTransform(new Point(GuiFrame.Rect.Width, iconHeight), guiFrameDragHandle.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, iconHeight / 2) }, style: "GUIDragIndicatorHorizontal"); dragIcon.RectTransform.MinSize = new Point(0, iconHeight); - handle.ValidatePosition = (RectTransform rectT) => + guiFrameDragHandle.ValidatePosition = (RectTransform rectT) => { var activeHuds = Character.Controlled?.SelectedItem?.ActiveHUDs ?? item.ActiveHUDs; foreach (ItemComponent ic in activeHuds) @@ -624,11 +642,13 @@ namespace Barotrauma.Items.Components //refresh slots to ensure they're rendered at the correct position (ic as ItemContainer)?.Inventory.CreateSlots(); } + GuiFrameOffset = GuiFrame.RectTransform.ScreenSpaceOffset; + guiFrameUpdatePending = true; return true; }; int buttonHeight = (int)(GUIStyle.ItemFrameMargin.Y * 0.4f); - var settingsIcon = new GUIButton(new RectTransform(new Point(buttonHeight), handle.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(buttonHeight / 4), MinSize = new Point(buttonHeight) }, + var settingsIcon = new GUIButton(new RectTransform(new Point(buttonHeight), guiFrameDragHandle.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(buttonHeight / 4), MinSize = new Point(buttonHeight) }, style: "GUIButtonSettings") { OnClicked = (btn, userdata) => @@ -636,6 +656,14 @@ namespace Barotrauma.Items.Components GUIContextMenu.CreateContextMenu( new ContextMenuOption("item.resetuiposition", isEnabled: true, onSelected: () => { + foreach (var ic in item.Components) + { + if (ic.GuiFrame != null && ic.GuiFrameOffset != Point.Zero) + { + ic.GuiFrameOffset = Point.Zero; + ic.guiFrameUpdatePending = true; + } + } if (Character.Controlled?.SelectedItem != null && item != Character.Controlled.SelectedItem) { Character.Controlled.SelectedItem.ForceHUDLayoutUpdate(ignoreLocking: true); @@ -648,7 +676,11 @@ namespace Barotrauma.Items.Components new ContextMenuOption(TextManager.Get(LockGuiFramePosition ? "item.unlockuiposition" : "item.lockuiposition"), isEnabled: true, onSelected: () => { LockGuiFramePosition = !LockGuiFramePosition; - handle.Enabled = !LockGuiFramePosition; + guiFrameDragHandle.Enabled = !LockGuiFramePosition; + if (SerializableProperties.TryGetValue(nameof(LockGuiFramePosition).ToIdentifier(), out var property)) + { + GameMain.Client?.CreateEntityEvent(Item, new Item.ChangePropertyEventData(property, this)); + } })); return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index 17293bf7f..830703d01 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -1,9 +1,9 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; -using System.Collections; +using System.Collections.Generic; using System.Linq; -using static Barotrauma.Inventory; namespace Barotrauma.Items.Components { @@ -97,6 +97,8 @@ namespace Barotrauma.Items.Components partial void InitProjSpecific(ContentXElement element) { slotIcons = new Sprite[capacity]; + + int currCapacity = MainContainerCapacity; foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -127,6 +129,19 @@ namespace Barotrauma.Items.Components } } break; + case "subcontainer": + int subContainerCapacity = subElement.GetAttributeInt("capacity", 1); + var slotIconElement = subElement.GetChildElement("sloticon"); + if (slotIconElement != null) + { + var slotIcon = new Sprite(slotIconElement); + for (int i = currCapacity; i < currCapacity + subContainerCapacity; i++) + { + slotIcons[i] = slotIcon; + } + } + currCapacity += subContainerCapacity; + break; } } @@ -180,11 +195,40 @@ namespace Barotrauma.Items.Components }; LocalizedString labelText = GetUILabel(); - GUITextBlock label = null; - if (!labelText.IsNullOrEmpty()) + GUITextBlock label = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform, Anchor.TopCenter), + labelText, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft, wrap: true) + { + IgnoreLayoutGroups = true + }; + + int buttonSize = GUIStyle.ItemFrameTopBarHeight; + Point margin = new Point(buttonSize / 4, buttonSize / 6); + + GUILayoutGroup buttonArea = new GUILayoutGroup(new RectTransform(new Point(GuiFrame.Rect.Width - margin.X * 2, buttonSize - margin.Y * 2), GuiFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, margin.Y) }, + isHorizontal: true, childAnchor: Anchor.TopRight) { - label = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform, Anchor.TopCenter), - labelText, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center, wrap: true); + AbsoluteSpacing = margin.X / 2 + }; + if (Inventory.Capacity > 1) + { + new GUIButton(new RectTransform(Vector2.One, buttonArea.RectTransform, scaleBasis: ScaleBasis.Smallest), style: "SortItemsButton") + { + ToolTip = TextManager.Get("SortItemsAlphabetically"), + OnClicked = (btn, userdata) => + { + SortItems(); + return true; + } + }; + new GUIButton(new RectTransform(Vector2.One, buttonArea.RectTransform, scaleBasis: ScaleBasis.Smallest), style: "MergeStacksButton") + { + ToolTip = TextManager.Get("MergeItemStacks"), + OnClicked = (btn, userdata) => + { + MergeStacks(); + return true; + } + }; } float minInventoryAreaSize = 0.5f; @@ -215,6 +259,71 @@ namespace Barotrauma.Items.Components Inventory.RectTransform = guiCustomComponent.RectTransform; } + private void SortItems() + { + List> itemsPerSlot = new List>(); + + for (int i = 0; i < Inventory.Capacity; i++) + { + var items = Inventory.GetItemsAt(i).ToList(); + if (items.Any()) + { + itemsPerSlot.Add(items); + items.ForEach(it => it.Drop(dropper: null, createNetworkEvent: false, setTransform: false)); + } + } + + itemsPerSlot.Sort((i1, i2) => i1.First().Name.CompareTo(i2.First().Name)); + foreach (var items in itemsPerSlot) + { + int firstFreeSlot = -1; + for (int i = 0; i < Inventory.Capacity; i++) + { + if (Inventory.GetItemAt(i) == null && Inventory.CanBePut(items.First())) + { + firstFreeSlot = i; + break; + } + } + if (firstFreeSlot == -1) + { + items.ForEach(it => it.Drop(dropper: null)); + continue; + } + foreach (var item in items) + { + if (!Inventory.TryPutItem(item, firstFreeSlot, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: false)) + { + //if putting in the specific slot fails (prevented by containable restrictions?), just put in the first free slot + if (!Inventory.TryPutItem(item, user: null, createNetworkEvent: false)) + { + item.Drop(dropper: null); + } + } + } + } + Inventory.CreateNetworkEvent(); + } + + private void MergeStacks() + { + for (int i = Inventory.Capacity - 1; i >= 0; i--) + { + var items = Inventory.GetItemsAt(i).ToList(); + if (items.None()) { continue; } + //find the first stack we can put the item in + for (int j = 0; j < i; j++) + { + if (Inventory.GetItemsAt(j).Any() && Inventory.CanBePutInSlot(items.First(), j)) + { + items.ForEach(it => Inventory.TryPutItem(it, j, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: false)); + break; + } + } + } + Inventory.CreateNetworkEvent(); + } + public LocalizedString GetUILabel() { if (UILabel == string.Empty) { return string.Empty; } @@ -277,7 +386,7 @@ namespace Barotrauma.Items.Components int ignoredItemCount = 0; var subContainableItems = AllSubContainableItems; - float targetSlotCapacity = GetMaxStackSize(targetSlot); + float targetSlotCapacity = Math.Min(containedItem.Prefab.MaxStackSize, GetMaxStackSize(targetSlot)); float capacity = targetSlotCapacity * MainContainerCapacity; if (subContainableItems != null) { @@ -310,29 +419,36 @@ namespace Barotrauma.Items.Components int itemCount = Inventory.AllItems.Count() - ignoredItemCount; return Math.Min(itemCount / Math.Max(capacity, 1), 1); } + + //display the state of an item in a specific slot + if (Inventory.Capacity == 1 || ContainedStateIndicatorSlot > -1) + { + if (containedItem == null) { return 0.0f; } + //if the contained item has some contained state indicator, show that + if (containedItem.GetComponent() is { ShowContainedStateIndicator: true } containedItemContainer) + { + return containedItemContainer.GetContainedIndicatorState(); + } + int maxStackSize = Math.Min(containedItem.Prefab.GetMaxStackSize(Inventory), GetMaxStackSize(targetSlot)); + if (maxStackSize == 1) + { + return containedItem.Condition / containedItem.MaxCondition; + } + return containedItems.Count() / (float)maxStackSize; + } else { - if (containedItem != null && (Inventory.Capacity == 1 || HasSubContainers)) - { - int maxStackSize = Math.Min(containedItem.Prefab.MaxStackSize, GetMaxStackSize(targetSlot)); - if (maxStackSize > 1 || containedItem.Prefab.HideConditionBar) - { - return containedItems.Count() / (float)maxStackSize; - } - } - return Inventory.Capacity == 1 || ContainedStateIndicatorSlot > -1 ? - (containedItem == null ? 0.0f : containedItem.Condition / containedItem.MaxCondition) : - Inventory.EmptySlotCount / (float)Inventory.Capacity; - } + return Inventory.EmptySlotCount / (float)Inventory.Capacity; + } } - public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1, Color? overrideColor = null) { if (hideItems || (item.body != null && !item.body.Enabled)) { return; } - DrawContainedItems(spriteBatch, itemDepth); + DrawContainedItems(spriteBatch, itemDepth, overrideColor); } - public void DrawContainedItems(SpriteBatch spriteBatch, float itemDepth) + public void DrawContainedItems(SpriteBatch spriteBatch, float itemDepth, Color? overrideColor = null) { Vector2 transformedItemPos = ItemPos * item.Scale; Vector2 transformedItemInterval = ItemInterval * item.Scale; @@ -388,7 +504,7 @@ namespace Barotrauma.Items.Components bool isWiringMode = SubEditorScreen.TransparentWiringMode && SubEditorScreen.IsWiringMode(); int i = 0; - foreach (DrawableContainedItem contained in drawableContainedItems) + foreach (ContainedItem contained in containedItems) { Vector2 itemPos = currentItemPos; @@ -466,7 +582,7 @@ namespace Barotrauma.Items.Components contained.Item.Sprite.Draw( spriteBatch, new Vector2(itemPos.X, -itemPos.Y), - isWiringMode ? contained.Item.GetSpriteColor(withHighlight: true) * 0.15f : contained.Item.GetSpriteColor(withHighlight: true), + overrideColor ?? (isWiringMode ? contained.Item.GetSpriteColor(withHighlight: true) * 0.15f : contained.Item.GetSpriteColor(withHighlight: true)), origin, -(contained.Item.body == null ? 0.0f : contained.Item.body.DrawRotation), contained.Item.Scale, @@ -476,7 +592,7 @@ namespace Barotrauma.Items.Components foreach (ItemContainer ic in contained.Item.GetComponents()) { if (ic.hideItems) { continue; } - ic.DrawContainedItems(spriteBatch, containedSpriteDepth); + ic.DrawContainedItems(spriteBatch, containedSpriteDepth, overrideColor); } i++; @@ -497,7 +613,7 @@ namespace Barotrauma.Items.Components } } - public override void UpdateHUD(Character character, float deltaTime, Camera cam) + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { if (!item.IsInteractable(character)) { return; } if (Inventory.RectTransform != null) @@ -512,19 +628,10 @@ namespace Barotrauma.Items.Components //if the item is in the character's inventory, no need to update the item's inventory //because the player can see it by hovering the cursor over the item - guiCustomComponent.Visible = DrawInventory && item.ParentInventory?.Owner != character; + guiCustomComponent.Visible = DrawInventory && (item.ParentInventory?.Owner != character || Inventory.DrawWhenEquipped); if (!guiCustomComponent.Visible) { return; } Inventory.Update(deltaTime, cam); } - - /*public override void DrawHUD(SpriteBatch spriteBatch, Character character) - { - //if the item is in the character's inventory, no need to draw the item's inventory - //because the player can see it by hovering the cursor over the item - if (item.ParentInventory?.Owner == character || !DrawInventory) return; - - Inventory.Draw(spriteBatch); - }*/ } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs index 9b1334f3a..a6ff486bb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs @@ -311,7 +311,7 @@ namespace Barotrauma.Items.Components prevRect = item.Rect; } - public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1, Color? overrideColor = null) { if (item.ParentInventory != null) { return; } if (editing) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Ladder.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Ladder.cs index 8911109d8..8fc4a144c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Ladder.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Ladder.cs @@ -19,13 +19,14 @@ namespace Barotrauma.Items.Components private Sprite backgroundSprite; - public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) { if (backgroundSprite == null) { return; } backgroundSprite.DrawTiled(spriteBatch, 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, + new Vector2(backgroundSprite.size.X * item.Scale, item.Rect.Height), + color: overrideColor ?? item.Color, textureScale: Vector2.One * item.Scale, depth: BackgroundSpriteDepth); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index 763d89b4f..540bc1759 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -79,7 +79,7 @@ namespace Barotrauma.Items.Components } } - public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1, Color? overrideColor = null) { if (Light?.LightSprite == null) { return; } if ((item.body == null || item.body.Enabled) && lightBrightness > 0.0f && IsOn && Light.Enabled) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs index 71cfdb1b4..ba8b62d21 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs @@ -428,7 +428,7 @@ namespace Barotrauma.Items.Components } } - public override void UpdateHUD(Character character, float deltaTime, Camera cam) + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { inSufficientPowerWarning.Visible = IsActive && !hasPower; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs index 745eca08b..8c3d920c6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs @@ -108,7 +108,7 @@ namespace Barotrauma.Items.Components } } - public override void UpdateHUD(Character character, float deltaTime, Camera cam) + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { powerIndicator.Selected = hasPower && IsActive; autoControlIndicator.Selected = controlLockTimer > 0.0f; @@ -138,14 +138,14 @@ namespace Barotrauma.Items.Components } } - public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) { if (propellerSprite != null) { Vector2 drawPos = item.DrawPosition; drawPos += PropellerPos; drawPos.Y = -drawPos.Y; - propellerSprite.Draw(spriteBatch, (int)Math.Floor(spriteIndex), drawPos, Color.White, propellerSprite.Origin, 0.0f, Vector2.One); + propellerSprite.Draw(spriteBatch, (int)Math.Floor(spriteIndex), drawPos, overrideColor ?? Color.White, propellerSprite.Origin, 0.0f, Vector2.One); } if (editing && !DisablePropellerDamage && propellerDamage != null && !GUI.DisableHUD) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index 3ac43ec4a..c86d674e5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -38,6 +38,11 @@ namespace Barotrauma.Items.Components } private FabricationRecipe selectedItem; + /// + /// Which character's skills the current view is displayed based on + /// + private Character displayingForCharacter; + public Identifier SelectedItemIdentifier => SelectedItem?.TargetItem.Identifier ?? Identifier.Empty; private GUIComponent inSufficientPowerWarning; @@ -358,9 +363,11 @@ namespace Barotrauma.Items.Components if (inputInventoryHolder != null) { inputContainer.AllowUIOverlap = true; + inputContainer.Inventory.DrawWhenEquipped = true; inputContainer.Inventory.RectTransform = inputInventoryHolder.RectTransform; } outputContainer.AllowUIOverlap = true; + outputContainer.Inventory.DrawWhenEquipped = true; outputContainer.Inventory.RectTransform = outputInventoryHolder.RectTransform; } @@ -453,13 +460,8 @@ namespace Barotrauma.Items.Components requiresRecipeText.RectTransform.RepositionChildInHierarchy(itemList.Content.RectTransform.GetChildIndex(firstRequiresRecipe.RectTransform)); } + FilterEntities(selectedItemCategory, itemFilterBox?.Text ?? string.Empty); HideEmptyItemListCategories(); - - if (selectedItem != null) - { - //reselect to recreate the info based on the new user's skills - SelectItem(character, selectedItem); - } } private readonly Dictionary missingIngredientCounts = new Dictionary(); @@ -538,6 +540,8 @@ namespace Barotrauma.Items.Components int slotIndex = 0; foreach (var kvp in missingIngredientCounts) { + if (inputContainer.Inventory?.visualSlots == null) { break; } + var requiredItem = kvp.Key; int missingCount = kvp.Value; @@ -560,11 +564,10 @@ namespace Barotrauma.Items.Components var requiredItemPrefab = requiredItem.FirstMatchingPrefab; float iconAlpha = 0.0f; - ItemPrefab requiredItemToDisplay; - int count = requiredItem.ItemPrefabs.Count(); - if (count > 1) + ItemPrefab requiredItemToDisplay = requiredItem.DefaultItem.IsEmpty ? null : requiredItem.ItemPrefabs.FirstOrDefault(p => p.Identifier == requiredItem.DefaultItem); + if (requiredItemToDisplay == null && requiredItem.ItemPrefabs.Multiple()) { - float iconCycleSpeed = 0.5f / count; + float iconCycleSpeed = 0.75f; float iconCycleT = (float)Timing.TotalTime * iconCycleSpeed; int iconIndex = (int)(iconCycleT % requiredItem.ItemPrefabs.Count()); @@ -573,7 +576,7 @@ namespace Barotrauma.Items.Components } else { - requiredItemToDisplay = requiredItem.ItemPrefabs.FirstOrDefault(); + requiredItemToDisplay ??= requiredItem.ItemPrefabs.FirstOrDefault(); iconAlpha = 1.0f; } if (iconAlpha > 0.0f) @@ -616,9 +619,12 @@ namespace Barotrauma.Items.Components if (slotRect.Contains(PlayerInput.MousePosition)) { - var suitableIngredients = requiredItem.ItemPrefabs.Select(ip => ip.Name).Distinct(); - LocalizedString toolTipText = string.Join(", ", suitableIngredients.Count() > 3 ? suitableIngredients.SkipLast(suitableIngredients.Count() - 3) : suitableIngredients); - if (suitableIngredients.Count() > 3) { toolTipText += "..."; } + LocalizedString toolTipText = requiredItem.OverrideHeader; + if (requiredItem.OverrideHeader.IsNullOrEmpty()) + { + var suitableIngredients = requiredItem.ItemPrefabs.Where(ip => !ip.HideInMenus).OrderBy(ip => ip.DefaultPrice?.Price ?? 0).Select(ip => ip.Name).Distinct(); + toolTipText = GetSuitableIngredientText(suitableIngredients); + } if (requiredItem.UseCondition && requiredItem.MinCondition < 1.0f) { toolTipText += " " + (int)Math.Round(requiredItem.MinCondition * 100) + "%"; @@ -656,15 +662,68 @@ namespace Barotrauma.Items.Components } } + private LocalizedString GetSuitableIngredientText(IEnumerable itemNameList) + { + int count = itemNameList.Count(); + if (count == 0) + { + return string.Empty; + } + else if (count == 1) + { + return itemNameList.First(); + } + else if (count == 2) + { + //[item1] or [item2] + return TextManager.GetWithVariables( + "DialogRequiredTreatmentOptionsLast", + ("[treatment1]", itemNameList.ElementAt(0)), + ("[treatment2]", itemNameList.ElementAt(1))); + } + else + { + // [item1], [item2], [item3] ... or [lastitem] + LocalizedString itemListStr = TextManager.GetWithVariables( + "DialogRequiredTreatmentOptionsFirst", + ("[treatment1]", itemNameList.ElementAt(0)), + ("[treatment2]", itemNameList.ElementAt(1))); + + int i; + bool isTruncated = false; + for (i = 2; i < count - 1; i++) + { + if (itemListStr.Length > 50) + { + isTruncated = true; + break; + } + itemListStr = TextManager.GetWithVariables( + "DialogRequiredTreatmentOptionsFirst", + ("[treatment1]", itemListStr), + ("[treatment2]", itemNameList.ElementAt(i))); + } + itemListStr = TextManager.GetWithVariables( + "DialogRequiredTreatmentOptionsLast", + ("[treatment1]", itemListStr), + ("[treatment2]", itemNameList.ElementAt(i))); + + if (isTruncated) + { + itemListStr += TextManager.Get("ellipsis"); + } + return itemListStr; + } + } + private void DrawOutputOverLay(SpriteBatch spriteBatch, GUICustomComponent overlayComponent) { overlayComponent.RectTransform.SetAsLastChild(); FabricationRecipe targetItem = fabricatedItem ?? selectedItem; - if (targetItem != null) + if (targetItem != null && outputContainer.Inventory?.visualSlots != null) { Rectangle slotRect = outputContainer.Inventory.visualSlots[0].Rect; - if (fabricatedItem != null) { float clampedProgressState = Math.Clamp(progressState, 0f, 1f); @@ -699,6 +758,16 @@ namespace Barotrauma.Items.Components { FabricationRecipe recipe = child.UserData as FabricationRecipe; if (recipe?.DisplayName == null) { continue; } + + if (recipe.HideForNonTraitors) + { + if (Character.Controlled is not { IsTraitor: true }) + { + child.Visible = false; + continue; + } + } + child.Visible = (string.IsNullOrWhiteSpace(filter) || recipe.DisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase)) && (!category.HasValue || recipe.TargetItem.Category.HasFlag(category.Value)); @@ -749,8 +818,9 @@ namespace Barotrauma.Items.Components private bool SelectItem(Character user, FabricationRecipe selectedItem, float? overrideRequiredTime = null) { this.selectedItem = selectedItem; + displayingForCharacter = user; - int max = Math.Max(selectedItem.TargetItem.MaxStackSize / selectedItem.Amount, 1); + int max = Math.Max(selectedItem.TargetItem.GetMaxStackSize(outputContainer.Inventory) / selectedItem.Amount, 1); if (amountInput != null) { @@ -924,7 +994,7 @@ namespace Barotrauma.Items.Components return true; } - public override void UpdateHUD(Character character, float deltaTime, Camera cam) + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { activateButton.Enabled = false; inSufficientPowerWarning.Visible = IsActive && !hasPower; @@ -933,6 +1003,12 @@ namespace Barotrauma.Items.Components if (!IsActive) { + if (selectedItem != null && displayingForCharacter != character) + { + //reselect to recreate the info based on the new user's skills + SelectItem(character, selectedItem); + } + //only check ingredients if the fabricator isn't active (if it is, this is done in Update) if (refreshIngredientsTimer <= 0.0f) { @@ -998,9 +1074,13 @@ namespace Barotrauma.Items.Components { fabricationLimits[msg.ReadUInt32()] = 0; } - State = newState; - this.amountToFabricate = amountToFabricate; + //don't touch the amount unless another character changed it or the fabricator is running + //otherwise we may end up reverting the changes the client just did to the amount + if ((user != null && user != Character.Controlled) || State != FabricatorState.Stopped) + { + this.amountToFabricate = amountToFabricate; + } this.amountRemaining = amountRemaining; if (newState == FabricatorState.Stopped || recipeHash == 0) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index 6759ec985..b81560ca1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -295,7 +295,7 @@ namespace Barotrauma.Items.Components } } - OrderPrefab[] reports = OrderPrefab.Prefabs.Where(o => o.IsReport && o.SymbolSprite != null && !o.Hidden).OrderBy(o => o.Identifier).ToArray(); + OrderPrefab[] reports = OrderPrefab.Prefabs.Where(o => o.IsVisibleAsReportButton).OrderBy(o => o.Identifier).ToArray(); GUIFrame bottomFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.15f), paddedContainer.RectTransform, Anchor.BottomCenter) { MaxSize = new Point(int.MaxValue, GUI.IntScale(40)) }, style: null) { @@ -350,10 +350,16 @@ namespace Barotrauma.Items.Components } }; + List shownItemPrefabs = new List(); foreach (ItemPrefab prefab in ItemPrefab.Prefabs.OrderBy(prefab => prefab.Name)) { if (prefab.HideInMenus) { continue; } + if (shownItemPrefabs.Any(ip => DisplayAsSameItem(ip, prefab))) + { + continue; + } CreateItemFrame(prefab, listBox.Content.RectTransform); + shownItemPrefabs.Add(prefab); } searchBar.OnDeselected += (sender, key) => @@ -398,6 +404,27 @@ namespace Barotrauma.Items.Components new Point(int.MaxValue, paddedContainer.Rect.Height - bottomFrame.Rect.Height - buttonLayout.Rect.Height); } + private static Sprite GetPreviewSprite(ItemPrefab prefab) + { + return prefab.InventoryIcon ?? prefab.Sprite; + } + + /// + /// If the items have an identical name and icon (e.g. a variant with an alternative fabrication/deconstruction recipe), + /// they're displayed as if they were the same item, not as two separate entries. + /// + private static bool DisplayAsSameItem(ItemPrefab prefab1, ItemPrefab prefab2) + { + if (prefab1 == prefab2) { return true; } + if (prefab1.Name == prefab2.Name) + { + var sprite1 = GetPreviewSprite(prefab1); + var sprite2 = GetPreviewSprite(prefab2); + return sprite1?.FullPath == sprite2?.FullPath && sprite1?.SourceRect == sprite2?.SourceRect; + } + return false; + } + private bool VisibleOnItemFinder(Item it) { if (it?.Submarine == null) { return false; } @@ -413,7 +440,7 @@ namespace Barotrauma.Items.Components if (it.Container?.GetComponent() is { DrawInventory: false } or { AllowAccess: false }) { return false; } - if (it.HasTag("traitormissionitem")) { return false; } + if (it.HasTag(Tags.TraitorMissionItem)) { return false; } return true; } @@ -539,13 +566,13 @@ namespace Barotrauma.Items.Components displayedSubs.Add(item.Submarine); displayedSubs.AddRange(item.Submarine.DockedTo.Where(s => s.TeamID == item.Submarine.TeamID)); - subEntities = MapEntity.mapEntityList.Where(me => (item.Submarine is { } sub && sub.IsEntityFoundOnThisSub(me, includingConnectedSubs: true, allowDifferentType: false)) && !me.HiddenInGame).OrderByDescending(w => w.SpriteDepth).ToList(); + subEntities = MapEntity.MapEntityList.Where(me => (item.Submarine is { } sub && sub.IsEntityFoundOnThisSub(me, includingConnectedSubs: true, allowDifferentType: false)) && !me.HiddenInGame).OrderByDescending(w => w.SpriteDepth).ToList(); BakeSubmarine(item.Submarine, parentRect); elementSize = GuiFrame.Rect.Size; } - public override void UpdateHUD(Character character, float deltaTime, Camera cam) + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { //recreate HUD if the subs we should display have changed if (item.Submarine == null && displayedSubs.Count > 0 || // item not inside a sub anymore, but display is still showing subs @@ -824,7 +851,8 @@ namespace Barotrauma.Items.Components foreach (GUIComponent component in listBox.Content.Children) { component.Visible = false; - if (component.UserData is ItemPrefab { Name: { } prefabName} prefab && itemsFoundOnSub.Contains(prefab)) + if (component.UserData is ItemPrefab { Name: { } prefabName} prefab && + (itemsFoundOnSub.Contains(prefab) || itemsFoundOnSub.Any(ip => DisplayAsSameItem(ip, prefab)))) { component.Visible = prefabName.ToLower().Contains(text.ToLower()); @@ -851,9 +879,9 @@ namespace Barotrauma.Items.Components tooltip.RectTransform.ScreenSpaceOffset = new Point(box.Rect.X, box.Rect.Y - height); } - private void CreateItemFrame(ItemPrefab prefab, RectTransform parent) + private static void CreateItemFrame(ItemPrefab prefab, RectTransform parent) { - Sprite sprite = prefab.InventoryIcon ?? prefab.Sprite; + Sprite sprite = GetPreviewSprite(prefab); if (sprite is null) { return; } GUIFrame frame = new GUIFrame(new RectTransform(new Vector2(1f, 0.25f), parent), style: "ListBoxElement") { @@ -899,7 +927,7 @@ namespace Barotrauma.Items.Components { if (!VisibleOnItemFinder(it)) { continue; } - if (it.Prefab == searchedPrefab) + if (DisplayAsSameItem(it.Prefab, searchedPrefab)) { // ignore items on players and hidden inventories if (it.FindParentInventory(inv => inv is CharacterInventory || inv is ItemInventory { Owner: Item { HiddenInGame: true }}) is { }) { continue; } @@ -1079,7 +1107,7 @@ namespace Barotrauma.Items.Components if (ShowHullIntegrity) { float amount = 1f + hullData.LinkedHulls.Count; - gapOpenSum = hull.ConnectedGaps.Concat(hullData.LinkedHulls.SelectMany(h => h.ConnectedGaps)).Where(g => !g.IsRoomToRoom && !g.HiddenInGame).Sum(g => g.Open) / amount; + gapOpenSum = hull.ConnectedGaps.Concat(hullData.LinkedHulls.SelectMany(h => h.ConnectedGaps)).Where(g => g.linkedTo.Count == 1 && !g.HiddenInGame).Sum(g => g.Open) / amount; borderColor = Color.Lerp(neutralColor, GUIStyle.Red, Math.Min(gapOpenSum, 1.0f)); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs index d3076f105..2c0adbcb6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs @@ -181,7 +181,7 @@ namespace Barotrauma.Items.Components private float flickerTimer; private readonly float flickerFrequency = 1; - public override void UpdateHUD(Character character, float deltaTime, Camera cam) + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { autoControlIndicator.Selected = IsAutoControlled; PowerButton.Enabled = isActiveLockTimer <= 0.0f; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs index 56194db32..43ec63ae5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs @@ -615,7 +615,7 @@ namespace Barotrauma.Items.Components turbineOutputMeter, TurbineOutput, new Vector2(0.0f, 100.0f), clampedOptimalTurbineOutput, clampedAllowedTurbineOutput); } - public override void UpdateHUD(Character character, float deltaTime, Camera cam) + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { IsActive = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index 0f61141c9..e06de7a92 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -109,7 +109,7 @@ namespace Barotrauma.Items.Components }, { BlipType.Destructible, - new Color[] { Color.TransparentBlack, new Color(74, 113, 75) * 0.8f, new Color(151, 236, 172) * 0.8f, new Color(153, 217, 234) * 0.8f } + new Color[] { Color.TransparentBlack, new Color(94, 114, 73) * 0.8f, new Color(255, 236, 151) * 0.8f, new Color(242, 243, 194) * 0.8f } }, { BlipType.Door, @@ -358,11 +358,6 @@ namespace Barotrauma.Items.Components } } - protected override void TryCreateDragHandle() - { - base.TryCreateDragHandle(); - } - private void SetPingDirection(Vector2 direction) { pingDirection = direction; @@ -471,7 +466,7 @@ namespace Barotrauma.Items.Components } } - public override void UpdateHUD(Character character, float deltaTime, Camera cam) + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { showDirectionalIndicatorTimer -= deltaTime; if (GameMain.Client != null) @@ -981,38 +976,41 @@ namespace Barotrauma.Items.Components } } - if (GameMain.GameSession == null || Level.Loaded == null) { return; } + if (GameMain.GameSession == null) { return; } - if (Level.Loaded.StartLocation?.Type is { ShowSonarMarker: true }) + if (Level.Loaded != null) { - DrawMarker(spriteBatch, - Level.Loaded.StartLocation.Name, - (Level.Loaded.StartOutpost != null ? "outpost" : "location").ToIdentifier(), - "startlocation", - Level.Loaded.StartExitPosition, transducerCenter, - displayScale, center, DisplayRadius); - } + if (Level.Loaded.StartLocation?.Type is { ShowSonarMarker: true }) + { + DrawMarker(spriteBatch, + Level.Loaded.StartLocation.Name, + (Level.Loaded.StartOutpost != null ? "outpost" : "location").ToIdentifier(), + "startlocation", + Level.Loaded.StartExitPosition, transducerCenter, + displayScale, center, DisplayRadius); + } - if (Level.Loaded is { EndLocation.Type.ShowSonarMarker: true, Type: LevelData.LevelType.LocationConnection }) - { - DrawMarker(spriteBatch, - Level.Loaded.EndLocation.Name, - (Level.Loaded.EndOutpost != null ? "outpost" : "location").ToIdentifier(), - "endlocation", - Level.Loaded.EndExitPosition, transducerCenter, - displayScale, center, DisplayRadius); - } + if (Level.Loaded is { EndLocation.Type.ShowSonarMarker: true, Type: LevelData.LevelType.LocationConnection }) + { + DrawMarker(spriteBatch, + Level.Loaded.EndLocation.Name, + (Level.Loaded.EndOutpost != null ? "outpost" : "location").ToIdentifier(), + "endlocation", + Level.Loaded.EndExitPosition, transducerCenter, + displayScale, center, DisplayRadius); + } - for (int i = 0; i < Level.Loaded.Caves.Count; i++) - { - var cave = Level.Loaded.Caves[i]; - if (!cave.DisplayOnSonar) { continue; } - DrawMarker(spriteBatch, - caveLabel.Value, - "cave".ToIdentifier(), - "cave" + i, - cave.StartPos.ToVector2(), transducerCenter, - displayScale, center, DisplayRadius); + for (int i = 0; i < Level.Loaded.Caves.Count; i++) + { + var cave = Level.Loaded.Caves[i]; + if (cave.MissionsToDisplayOnSonar.None()) { continue; } + DrawMarker(spriteBatch, + caveLabel.Value, + "cave".ToIdentifier(), + "cave" + i, + cave.StartPos.ToVector2(), transducerCenter, + displayScale, center, DisplayRadius); + } } int missionIndex = 0; @@ -1070,7 +1068,7 @@ namespace Barotrauma.Items.Components { if (!sub.ShowSonarMarker) { continue; } if (connectedSubs.Contains(sub)) { continue; } - if (sub.WorldPosition.Y > Level.Loaded.Size.Y) { continue; } + if (Level.Loaded != null && sub.WorldPosition.Y > Level.Loaded.Size.Y) { continue; } if (item.Submarine != null || Character.Controlled != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index 6a5d4a44b..fd5be2193 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -532,6 +532,22 @@ namespace Barotrauma.Items.Components }; } + /// + /// Map the rectangular steering vector to a circular area using FG-Squircular Mapping which preserves the angle of the vector. + /// + private static Vector2 MapSquareToCircle(Vector2 steeringVector) + { + float xSqr = steeringVector.X * steeringVector.X; + float ySqr = steeringVector.Y * steeringVector.Y; + float length = MathF.Sqrt(ySqr + xSqr); + if (MathUtils.NearlyEqual(length, 0.0f)) { return Vector2.Zero; } + + //FG-Squircular mapping formula from https://arxiv.org/ftp/arxiv/papers/1509/1509.06344.pdf + float x = steeringVector.X * MathF.Sqrt(xSqr + ySqr - xSqr * ySqr) / length; + float y = steeringVector.Y * MathF.Sqrt(xSqr + ySqr - xSqr * ySqr) / length; + return new Vector2(x, y); + } + public void DrawHUD(SpriteBatch spriteBatch, Rectangle rect) { int width = rect.Width, height = rect.Height; @@ -545,11 +561,9 @@ namespace Barotrauma.Items.Components if (!AutoPilot) { - Vector2 unitSteeringInput = steeringInput / 100.0f; //map input from rectangle to circle - Vector2 steeringInputPos = new Vector2( - steeringInput.X * (float)Math.Sqrt(1.0f - 0.5f * unitSteeringInput.Y * unitSteeringInput.Y), - -steeringInput.Y * (float)Math.Sqrt(1.0f - 0.5f * unitSteeringInput.X * unitSteeringInput.X)); + Vector2 steeringInputPos = MapSquareToCircle(steeringInput / 100f) * 100.0f; + steeringInputPos.Y = -steeringInputPos.Y; steeringInputPos += steeringOrigin; if (steeringIndicator != null) @@ -604,10 +618,8 @@ namespace Barotrauma.Items.Components } //map velocity from rectangle to circle - Vector2 unitTargetVel = targetVelocity / 100.0f; - Vector2 steeringPos = new Vector2( - targetVelocity.X * 0.9f * (float)Math.Sqrt(1.0f - 0.5f * unitTargetVel.Y * unitTargetVel.Y), - -targetVelocity.Y * 0.9f * (float)Math.Sqrt(1.0f - 0.5f * unitTargetVel.X * unitTargetVel.X)); + Vector2 steeringPos = MapSquareToCircle(targetVelocity / 100f) * 90.0f; + steeringPos.Y = -steeringPos.Y; steeringPos += steeringOrigin; if (steeringIndicator != null) @@ -682,7 +694,7 @@ namespace Barotrauma.Items.Components } } - public override void UpdateHUD(Character character, float deltaTime, Camera cam) + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { if (swapDestinationOrder == null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Planter.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Planter.cs index c0d347358..d91e5966b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Planter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Planter.cs @@ -40,7 +40,7 @@ namespace Barotrauma.Items.Components } } - public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) { for (var i = 0; i < GrowableSeeds.Length; i++) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs index 13e9d9ffc..d2328aac7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs @@ -122,7 +122,7 @@ namespace Barotrauma.Items.Components } } - public override void UpdateHUD(Character character, float deltaTime, Camera cam) + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { if (chargeIndicator != null) { @@ -131,7 +131,7 @@ namespace Barotrauma.Items.Components } } - public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1, Color? overrideColor = null) { Vector2 scaledIndicatorSize = indicatorSize * item.Scale; if (scaledIndicatorSize.X <= 2.0f || scaledIndicatorSize.Y <= 2.0f) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerTransfer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerTransfer.cs index 0a1cbd605..cd4053924 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerTransfer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerTransfer.cs @@ -10,6 +10,10 @@ namespace Barotrauma.Items.Components private GUITickBox highVoltageIndicator; private GUITickBox lowVoltageIndicator; + private GUITextBlock powerLabel, loadLabel; + + private LanguageIdentifier prevLanguage; + partial void InitProjectSpecific(XElement element) { if (GuiFrame == null) { return; } @@ -56,12 +60,12 @@ namespace Barotrauma.Items.Components Stretch = true }; - var powerLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1), upperTextArea.RectTransform), + powerLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1), upperTextArea.RectTransform), TextManager.Get("PowerTransferPowerLabel"), textColor: GUIStyle.TextColorBright, font: GUIStyle.LargeFont, textAlignment: Alignment.CenterRight) { ToolTip = TextManager.Get("PowerTransferTipPower") }; - var loadLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1), lowerTextArea.RectTransform), + loadLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1), lowerTextArea.RectTransform), TextManager.Get("PowerTransferLoadLabel"), textColor: GUIStyle.TextColorBright, font: GUIStyle.LargeFont, textAlignment: Alignment.CenterRight) { ToolTip = TextManager.Get("PowerTransferTipLoad") @@ -75,7 +79,7 @@ namespace Barotrauma.Items.Components ToolTip = TextManager.Get("PowerTransferTipPower"), TextGetter = () => { float currPower = powerLoad < 0 ? -powerLoad: 0; - if (!(this is RelayComponent) && PowerConnections != null && PowerConnections.Count > 0 && PowerConnections[0].Grid != null) + if (this is not RelayComponent && PowerConnections != null && PowerConnections.Count > 0 && PowerConnections[0].Grid != null) { currPower = PowerConnections[0].Grid.Power; } @@ -119,9 +123,11 @@ namespace Barotrauma.Items.Components GUITextBlock.AutoScaleAndNormalize(powerLabel, loadLabel); GUITextBlock.AutoScaleAndNormalize(true, true, powerText, loadText); GUITextBlock.AutoScaleAndNormalize(kw1, kw2); + + prevLanguage = GameSettings.CurrentConfig.Language; } - public override void UpdateHUD(Character character, float deltaTime, Camera cam) + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { if (GuiFrame == null) return; @@ -129,6 +135,13 @@ namespace Barotrauma.Items.Components powerIndicator.Selected = IsActive && voltage > 0; highVoltageIndicator.Selected = Timing.TotalTime % 0.5f < 0.25f && powerIndicator.Selected && voltage > 1.2f; lowVoltageIndicator.Selected = Timing.TotalTime % 0.5f < 0.25f && powerIndicator.Selected && voltage < 0.8f; + + if (prevLanguage != GameSettings.CurrentConfig.Language) + { + GUITextBlock.AutoScaleAndNormalize(powerIndicator.TextBlock, highVoltageIndicator.TextBlock, lowVoltageIndicator.TextBlock); + GUITextBlock.AutoScaleAndNormalize(powerLabel, loadLabel); + prevLanguage = GameSettings.CurrentConfig.Language; + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs index aaca8fda0..7993a8a97 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs @@ -42,59 +42,98 @@ namespace Barotrauma.Items.Components Vector2 axis = new Vector2( msg.ReadSingle(), msg.ReadSingle()); - UInt16 entityID = msg.ReadUInt16(); + StickTargetType targetType = (StickTargetType)msg.ReadByte(); - Entity entity = Entity.FindEntityByID(entityID); Submarine submarine = Entity.FindEntityByID(submarineID) as Submarine; - Hull hull = Entity.FindEntityByID(hullID) as Hull; + Hull hull = Entity.FindEntityByID(hullID) as Hull; item.Submarine = submarine; item.CurrentHull = hull; item.body.SetTransform(simPosition, item.body.Rotation); - if (entity is Character character) + + switch (targetType) { - byte limbIndex = msg.ReadByte(); - if (limbIndex >= character.AnimController.Limbs.Length) - { - DebugConsole.ThrowError($"Failed to read a projectile update from the server. Limb index out of bounds ({limbIndex}, character: {character.ToString()})"); - return; - } - if (character.Removed) { return; } - var limb = character.AnimController.Limbs[limbIndex]; - StickToTarget(limb.body.FarseerBody, axis); - } - else if (entity is Structure structure) - { - byte bodyIndex = msg.ReadByte(); - if (bodyIndex == 255) { bodyIndex = 0; } - if (bodyIndex >= structure.Bodies.Count) - { - DebugConsole.ThrowError($"Failed to read a projectile update from the server. Structure body index out of bounds ({bodyIndex}, structure: {structure.ToString()})"); - return; - } - var body = structure.Bodies[bodyIndex]; - StickToTarget(body, axis); - } - else if (entity is Item item) - { - if (item.Removed) { return; } - var door = item.GetComponent(); - if (door != null) - { - StickToTarget(door.Body.FarseerBody, axis); - } - else if (item.body != null) - { - StickToTarget(item.body.FarseerBody, axis); - } - } - else if (entity is Submarine sub) - { - StickToTarget(sub.PhysicsBody.FarseerBody, axis); - } - else - { - DebugConsole.ThrowError($"Failed to read a projectile update from the server. Invalid stick target ({entity?.ToString() ?? "null"}, {entityID})"); - } + case StickTargetType.Structure: + UInt16 structureId = msg.ReadUInt16(); + byte bodyIndex = msg.ReadByte(); + if (Entity.FindEntityByID(structureId) is Structure structure) + { + if (bodyIndex == 255) { bodyIndex = 0; } + if (bodyIndex >= structure.Bodies.Count) + { + DebugConsole.ThrowError($"Failed to read a projectile update from the server. Structure body index out of bounds ({bodyIndex}, structure: {structure})"); + return; + } + var body = structure.Bodies[bodyIndex]; + StickToTarget(body, axis); + } + else + { + DebugConsole.AddWarning($"\"{item.Prefab.Identifier}\" failed to stick to a structure. Could not find a structure with the ID {structureId}"); + } + break; + case StickTargetType.Limb: + UInt16 characterId = msg.ReadUInt16(); + byte limbIndex = msg.ReadByte(); + if (Entity.FindEntityByID(characterId) is Character character) + { + if (limbIndex >= character.AnimController.Limbs.Length) + { + DebugConsole.ThrowError($"Failed to read a projectile update from the server. Limb index out of bounds ({limbIndex}, character: {character})"); + return; + } + if (character.Removed) { return; } + var limb = character.AnimController.Limbs[limbIndex]; + StickToTarget(limb.body.FarseerBody, axis); + } + else + { + DebugConsole.AddWarning($"\"{this.item.Prefab.Identifier}\" failed to stick to a limb. Could not find a character with the ID {characterId}"); + } + break; + case StickTargetType.Item: + UInt16 itemID = msg.ReadUInt16(); + if (Entity.FindEntityByID(itemID) is Item targetItem) + { + if (targetItem.Removed) { return; } + var door = targetItem.GetComponent(); + if (door != null) + { + StickToTarget(door.Body.FarseerBody, axis); + } + else if (targetItem.body != null) + { + StickToTarget(targetItem.body.FarseerBody, axis); + } + } + else + { + DebugConsole.AddWarning($"\"{this.item.Prefab.Identifier}\" failed to stick to an item. Could not find n item with the ID {itemID}"); + } + break; + case StickTargetType.Submarine: + UInt16 targetSubmarineId = msg.ReadUInt16(); + if (Entity.FindEntityByID(targetSubmarineId) is Submarine targetSub) + { + StickToTarget(targetSub.PhysicsBody.FarseerBody, axis); + } + else + { + DebugConsole.AddWarning($"\"{item.Prefab.Identifier}\" failed to stick to a submarine. Could not find a structure with the ID {targetSubmarineId}"); + } + break; + case StickTargetType.LevelWall: + int levelWallIndex = msg.ReadInt32(); + var allCells = Level.Loaded.GetAllCells(); + if (levelWallIndex >= 0 && levelWallIndex < allCells.Count) + { + StickToTarget(allCells[levelWallIndex].Body, axis); + } + else + { + DebugConsole.ThrowError($"Failed to read a projectile update from the server. Level wall index out of bounds ({levelWallIndex}, wall count: {allCells.Count})"); + } + break; + } } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RemoteController.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RemoteController.cs index 6f6d3d740..31f951899 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RemoteController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RemoteController.cs @@ -9,7 +9,7 @@ namespace Barotrauma.Items.Components currentTarget?.DrawHUD(spriteBatch, Screen.Selected.Cam, character); } - public override void UpdateHUD(Character character, float deltaTime, Camera cam) + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { currentTarget?.UpdateHUD(cam, character,deltaTime); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs index a449dc00f..8f58322ae 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs @@ -144,7 +144,7 @@ namespace Barotrauma.Items.Components } } #if DEBUG - public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) { if (GameMain.DebugDraw && IsActive) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index c25a7c6cc..59010730c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -373,12 +373,19 @@ namespace Barotrauma.Items.Components } } - public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) { if (GameMain.DebugDraw && Character.Controlled?.FocusedItem == item) { - bool paused = !ShouldDeteriorate(); - if (deteriorationTimer > 0.0f) + bool paused = !ShouldDeteriorate() && ForceDeteriorationTimer <= 0.0f; + if (ForceDeteriorationTimer > 0.0f) + { + GUI.DrawString(spriteBatch, + new Vector2(item.DrawPosition.X, -item.DrawPosition.Y), "Forced deterioration for " + ((int)ForceDeteriorationTimer) + " s", + Color.Red, Color.Black * 0.5f); + + } + else if (deteriorationTimer > 0.0f) { GUI.DrawString(spriteBatch, new Vector2(item.DrawPosition.X, -item.DrawPosition.Y), "Deterioration delay " + ((int)deteriorationTimer) + (paused ? " [PAUSED]" : ""), @@ -430,8 +437,7 @@ namespace Barotrauma.Items.Components public void ClientEventRead(IReadMessage msg, float sendingTime) { deteriorationTimer = msg.ReadSingle(); - deteriorateAlwaysResetTimer = msg.ReadSingle(); - DeteriorateAlways = msg.ReadBoolean(); + ForceDeteriorationTimer = msg.ReadSingle(); tinkeringDuration = msg.ReadSingle(); tinkeringStrength = msg.ReadSingle(); tinkeringPowersDevices = msg.ReadBoolean(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs index 5bee8bba0..79adeb549 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs @@ -55,20 +55,6 @@ namespace Barotrauma.Items.Components } } - private Vector2 GetSourcePos() - { - Vector2 sourcePos = source.WorldPosition; - if (source is Item sourceItem) - { - sourcePos = sourceItem.DrawPosition; - } - else if (source is Limb sourceLimb && sourceLimb.body != null) - { - sourcePos = sourceLimb.body.DrawPosition; - } - return sourcePos; - } - partial void InitProjSpecific(ContentXElement element) { foreach (var subElement in element.Elements()) @@ -88,34 +74,22 @@ namespace Barotrauma.Items.Components } } - public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) { if (target == null || target.Removed) { return; } if (target.ParentInventory != null) { return; } if (source is Limb limb && limb.Removed) { return; } if (source is Entity e && e.Removed) { return; } - Vector2 startPos = GetSourcePos(); + Vector2 startPos = GetSourcePos(useDrawPosition: true); startPos.Y = -startPos.Y; - if (source is Item sourceItem && !sourceItem.Removed) + if ((source as Item)?.GetComponent() is { } turret) { - var turret = sourceItem.GetComponent(); - var weapon = sourceItem.GetComponent(); - if (turret != null) + if (turret.BarrelSprite != null) { - startPos = new Vector2(sourceItem.WorldRect.X + turret.TransformedBarrelPos.X, -(sourceItem.WorldRect.Y - turret.TransformedBarrelPos.Y)); - if (turret.BarrelSprite != null) - { - startPos += new Vector2((float)Math.Cos(turret.Rotation), (float)Math.Sin(turret.Rotation)) * turret.BarrelSprite.size.Y * turret.BarrelSprite.RelativeOrigin.Y * item.Scale * 0.9f; - } - startPos -= turret.GetRecoilOffset(); - } - else if (weapon != null) - { - Vector2 barrelPos = FarseerPhysics.ConvertUnits.ToDisplayUnits(weapon.TransformedBarrelPos); - barrelPos.Y = -barrelPos.Y; - startPos += barrelPos; + startPos += new Vector2((float)Math.Cos(turret.Rotation), (float)Math.Sin(turret.Rotation)) * turret.BarrelSprite.size.Y * turret.BarrelSprite.RelativeOrigin.Y * item.Scale * 0.9f; } + startPos -= turret.GetRecoilOffset(); } Vector2 endPos = new Vector2(target.DrawPosition.X, target.DrawPosition.Y); Vector2 flippedPos = target.Sprite.size * target.Scale * (Origin - new Vector2(0.5f)); @@ -156,28 +130,28 @@ namespace Barotrauma.Items.Components if (startSprite != null) { float depth = Math.Min(item.GetDrawDepth() + (startSprite.Depth - item.Sprite.Depth), 0.999f); - startSprite?.Draw(spriteBatch, startPos, SpriteColor, angle, depth: depth); + startSprite?.Draw(spriteBatch, startPos, overrideColor ?? SpriteColor, angle, depth: depth); } if (endSprite != null && (!Snapped || BreakFromMiddle)) { float depth = Math.Min(item.GetDrawDepth() + (endSprite.Depth - item.Sprite.Depth), 0.999f); - endSprite?.Draw(spriteBatch, endPos, SpriteColor, angle, depth: depth); + endSprite?.Draw(spriteBatch, endPos, overrideColor ?? SpriteColor, angle, depth: depth); } } } - private void DrawRope(SpriteBatch spriteBatch, Vector2 startPos, Vector2 endPos, int width) + private void DrawRope(SpriteBatch spriteBatch, Vector2 startPos, Vector2 endPos, int width, Color? overrideColor = null) { float depth = sprite == null ? item.Sprite.Depth + 0.001f : Math.Min(item.GetDrawDepth() + (sprite.Depth - item.Sprite.Depth), 0.999f); - + if (sprite?.Texture == null) { GUI.DrawLine(spriteBatch, startPos, endPos, - SpriteColor, depth: depth, width: width); + overrideColor ?? SpriteColor, depth: depth, width: width); return; } @@ -191,7 +165,7 @@ namespace Barotrauma.Items.Components GUI.DrawLine(spriteBatch, sprite, startPos + dir * (x - 5.0f), startPos + dir * (x + sprite.size.X), - SpriteColor, depth: depth, width: width); + overrideColor ?? SpriteColor, depth: depth, width: width); } float leftOver = length - x; if (leftOver > 0.0f) @@ -199,7 +173,7 @@ namespace Barotrauma.Items.Components GUI.DrawLine(spriteBatch, sprite, startPos + dir * (x - 5.0f), endPos, - SpriteColor, depth: depth, width: width); + overrideColor ?? SpriteColor, depth: depth, width: width); } } else @@ -207,7 +181,7 @@ namespace Barotrauma.Items.Components GUI.DrawLine(spriteBatch, sprite, startPos, endPos, - SpriteColor, depth: depth, width: width); + overrideColor ?? SpriteColor, depth: depth, width: width); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs new file mode 100644 index 000000000..7dafcf617 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs @@ -0,0 +1,499 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; + +namespace Barotrauma.Items.Components +{ + internal sealed partial class CircuitBox + { + public CircuitBoxUI? UI; + public readonly Dictionary ActiveCursors = new Dictionary(); + public Option HeldComponent = Option.None; + + private const float CursorUpdateInterval = 1f; + private float cursorUpdateTimer; + + private readonly Vector2[] recordedCursorPositions = new Vector2[10]; + private Option recordedDragStart = Option.None; + private Option recordedHeldPrefab = Option.None; + + /// + /// If the circuit box was initialized by the server instead of from the save file. + /// Used to ensure the wires the server sends are properly connected up when we load in. + /// + private bool wasInitializedByServer; + + public Sprite? WireSprite { get; private set; } + public Sprite? ConnectionSprite { get; private set; } + public Sprite? WireConnectorSprite { get; private set; } + public Sprite? ConnectionScrewSprite { get; private set; } + public UISprite? NodeFrameSprite { get; private set; } + public UISprite? NodeTopSprite { get; private set; } + + protected override void CreateGUI() + { + base.CreateGUI(); + GuiFrame.ClearChildren(); + UI?.CreateGUI(GuiFrame); + } + + partial void InitProjSpecific(ContentXElement element) + { + UI = new CircuitBoxUI(this); + IsActive = true; + CreateGUI(); + + foreach (var subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "wiresprite": + WireSprite = new Sprite(subElement); + break; + case "connectionsprite": + ConnectionSprite = new Sprite(subElement); + break; + case "wireconnectorsprite": + WireConnectorSprite = new Sprite(subElement); + break; + case "connectionscrewsprite": + ConnectionScrewSprite = new Sprite(subElement); + break; + } + } + + if (GUIStyle.GetComponentStyle("CircuitBoxTop") is { } topStyle) + { + NodeTopSprite = topStyle.Sprites[GUIComponent.ComponentState.None][0]; + } + + if (GUIStyle.GetComponentStyle("CircuitBoxFrame") is { } compStyle) + { + NodeFrameSprite = compStyle.Sprites[GUIComponent.ComponentState.None][0]; + } + } + + public override bool ShouldDrawHUD(Character character) + => character == Character.Controlled && (character.SelectedItem == item || character.SelectedSecondaryItem == item); + + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) + { + if (UI is null) { return; } + + UI.Update(deltaTime); + + if (GameMain.NetworkMember is null) { return; } + + foreach (var (cursorChar, cursor) in ActiveCursors) + { + if (!cursor.IsActive) { continue; } + + ActiveCursors[cursorChar].Update(deltaTime); + } + + Vector2 cursorPos = UI.GetCursorPosition(); + int lastCursorPosIndex = recordedCursorPositions.Length - 1; + + if (cursorUpdateTimer < CursorUpdateInterval) + { + cursorUpdateTimer += deltaTime; + int cursorIndex = (int)MathF.Floor(cursorUpdateTimer * lastCursorPosIndex); + RecordCursorPosition(cursorIndex); + } + else + { + RecordCursorPosition(lastCursorPosIndex); + SendCursorState(recordedCursorPositions, recordedDragStart, recordedHeldPrefab.Select(static c => c.Identifier)); + + recordedDragStart = Option.None; + recordedHeldPrefab = Option.None; + cursorUpdateTimer = 0f; + } + + void RecordCursorPosition(int index) + { + var dragStart = UI.GetDragStart(); + if (dragStart.IsSome()) { recordedDragStart = dragStart; } + + var heldComponent = HeldComponent; + if (heldComponent.IsSome()) { recordedHeldPrefab = heldComponent; } + + if (index >= 0 && index < recordedCursorPositions.Length) { recordedCursorPositions[index] = cursorPos; } + } + } + + public void RemoveComponents(IReadOnlyCollection node) + { + var ids = node.Select(static n => n.ID).ToImmutableArray(); + + if (GameMain.NetworkMember is null) + { + CreateRefundItemsForUsedResources(ids, Character.Controlled); + RemoveComponentInternal(ids); + return; + } + + if (!node.Any()) { return; } + + CreateClientEvent(new CircuitBoxRemoveComponentEvent(ids)); + } + + public void AddWire(CircuitBoxConnection one, CircuitBoxConnection two) + { + if (GameMain.NetworkMember is null) + { + Connect(one, two, static delegate { }, CircuitBoxWire.SelectedWirePrefab); + return; + } + + if (!VerifyConnection(one, two)) { return; } + + CreateClientEvent(new CircuitBoxClientAddWireEvent(Color.White, CircuitBoxConnectorIdentifier.FromConnection(one), CircuitBoxConnectorIdentifier.FromConnection(two), CircuitBoxWire.SelectedWirePrefab.UintIdentifier)); + } + + public void RemoveWires(IReadOnlyCollection wires) + { + var ids = wires.Select(static w => w.ID).ToImmutableArray(); + if (GameMain.NetworkMember is null) + { + RemoveWireInternal(ids); + return; + } + + if (!ids.Any()) { return; } + CreateClientEvent(new CircuitBoxRemoveWireEvent(ids)); + } + + public void SelectComponents(IReadOnlyCollection moveables, bool overwrite) + { + if (Character.Controlled is not { ID: var controlledId }) { return; } + + var ids = ImmutableArray.CreateBuilder(); + var ios = ImmutableArray.CreateBuilder(); + + foreach (var moveable in moveables) + { + if (moveable is { IsSelected: true, IsSelectedByMe: false }) { continue; } + + switch (moveable) + { + case CircuitBoxComponent node: + ids.Add(node.ID); + break; + case CircuitBoxInputOutputNode io: + ios.Add(io.NodeType); + break; + } + } + + if (GameMain.NetworkMember is null) + { + SelectComponentsInternal(ids, controlledId, overwrite); + SelectInputOutputInternal(ios, controlledId, overwrite); + return; + } + + if ((!ids.Any() && !ios.Any()) && !overwrite) { return; } + + CreateClientEvent(new CircuitBoxSelectNodesEvent(ids.ToImmutable(), ios.ToImmutable(), overwrite, controlledId)); + } + + public void SelectWires(IReadOnlyCollection wires, bool overwrite) + { + if (Character.Controlled is not { ID: var controlledId }) { return; } + + var ids = (from wire in wires where !wire.IsSelected || wire.IsSelectedByMe select wire.ID).ToImmutableArray(); + + if (GameMain.NetworkMember is null) + { + SelectWiresInternal(ids, controlledId, overwrite); + return; + } + + if (!ids.Any() && !overwrite) { return; } + + CreateClientEvent(new CircuitBoxSelectWiresEvent(ids, overwrite, Character.Controlled.ID)); + } + + public void MoveComponent(Vector2 moveAmount, IReadOnlyCollection moveables) + { + var ids = ImmutableArray.CreateBuilder(); + var ios = ImmutableArray.CreateBuilder(); + + foreach (CircuitBoxNode move in moveables) + { + switch (move) + { + case CircuitBoxComponent node: + ids.Add(node.ID); + break; + case CircuitBoxInputOutputNode io: + ios.Add(io.NodeType); + break; + } + } + + if (GameMain.NetworkMember is null) + { + MoveNodesInternal(ids, ios, moveAmount); + return; + } + + if (!ids.Any() && !ios.Any()) { return; } + + + CreateClientEvent(new CircuitBoxMoveComponentEvent(ids.ToImmutable(), ios.ToImmutable(), moveAmount)); + } + + public void AddComponent(ItemPrefab prefab, Vector2 pos) + { + if (GameMain.NetworkMember is null) + { + ItemPrefab resource; + + if (IsFull) { return; } + + if (IsInGame()) + { + if (!GetApplicableResourcePlayerHas(prefab, Character.Controlled).TryUnwrap(out var r)) { return; } + resource = r.Prefab; + RemoveItem(r); + } + else + { + resource = ItemPrefab.Prefabs[Tags.FPGACircuit]; + } + + AddComponentInternal(ICircuitBoxIdentifiable.FindFreeID(Components), prefab, resource, pos, static delegate { }); + return; + } + + CreateClientEvent(new CircuitBoxAddComponentEvent(prefab.UintIdentifier, pos)); + } + + public partial void OnViewUpdateProjSpecific() + { + UI?.MouseSnapshotHandler.UpdateConnections(); + UI?.UpdateComponentList(); + } + + protected override void OnResolutionChanged() + { + base.OnResolutionChanged(); + CreateGUI(); + } + + // Remove selection when the circuit box is deselected + public partial void OnDeselected(Character c) + { + cursorUpdateTimer = 0f; + + // Server will broadcast the deselection, we don't need to do it ourselves + if (GameMain.NetworkMember is not null) { return; } + ClearAllSelectionsInternal(c.ID); + } + + public void ClientRead(INetSerializableStruct data) + { + switch (data) + { + case NetCircuitBoxCursorInfo cursorInfo: + { + ClientReadCursor(cursorInfo); + break; + } + case CircuitBoxErrorEvent errorData: + { + DebugConsole.ThrowError($"The server responded with an error: {errorData.Message}"); + break; + } + default: + throw new ArgumentOutOfRangeException(nameof(data), data, "This data cannot be handled using direct network messages."); + } + } + + public void SendMessage(CircuitBoxOpcode opcode, INetSerializableStruct data) + { + IWriteMessage msg = new WriteOnlyMessage().WithHeader(ClientPacketHeader.CIRCUITBOX); + + msg.WriteNetSerializableStruct(new NetCircuitBoxHeader( + Opcode: opcode, + ItemID: item.ID, + ComponentIndex: (byte)item.GetComponentIndex(this))); + + msg.WriteNetSerializableStruct(data); + + DeliveryMethod deliveryMethod = + UnrealiableOpcodes.Contains(opcode) + ? DeliveryMethod.Unreliable + : DeliveryMethod.Reliable; + + GameMain.Client?.ClientPeer?.Send(msg, deliveryMethod); + } + + private void SendCursorState(Vector2[] cursorPositions, Option dragStart, Option heldComponent) + { + if (!IsRoundRunning()) { return; } + + var msg = new NetCircuitBoxCursorInfo( + RecordedPositions: cursorPositions, + DragStart: dragStart, + HeldItem: heldComponent); + + SendMessage(CircuitBoxOpcode.Cursor, msg); + } + + public void ClientReadCursor(NetCircuitBoxCursorInfo info) + { + if (Entity.FindEntityByID(info.CharacterID) is not Character character) { return; } + + if (!ActiveCursors.ContainsKey(character)) + { + var newCursor = new CircuitBoxCursor(info); + ActiveCursors.Add(character, newCursor); + return; + } + + var activeCursor = ActiveCursors[character]; + activeCursor.UpdateInfo(info); + activeCursor.ResetTimers(); + } + + public void CreateClientEvent(INetSerializableStruct data) + => item.CreateClientEvent(this, new CircuitBoxEventData(data)); + + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData? extraData = null) + { + if (extraData is null) { return; } + var eventData = ExtractEventData(extraData); + msg.WriteByte((byte)eventData.Opcode); + msg.WriteNetSerializableStruct(eventData.Data); + } + + public void ClientEventRead(IReadMessage msg, float sendingTime) + { + var header = (CircuitBoxOpcode)msg.ReadByte(); + + switch (header) + { + case CircuitBoxOpcode.AddComponent: + { + var data = INetSerializableStruct.Read(msg); + AddComponentFromData(data); + break; + } + case CircuitBoxOpcode.DeleteComponent: + { + var data = INetSerializableStruct.Read(msg); + RemoveComponentInternal(data.TargetIDs); + break; + } + case CircuitBoxOpcode.MoveComponent: + { + var data = INetSerializableStruct.Read(msg); + MoveNodesInternal(data.TargetIDs, data.IOs, data.MoveAmount); + break; + } + case CircuitBoxOpcode.UpdateSelection: + { + var data = INetSerializableStruct.Read(msg); + + var nodeDict = data.ComponentIds.ToImmutableDictionary(static s => s.ID, static s => s.SelectedBy); + var wireDict = data.WireIds.ToImmutableDictionary(static s => s.ID, static s => s.SelectedBy); + var ioDict = data.InputOutputs.ToImmutableDictionary(static s => s.Type, static s => s.SelectedBy); + + UpdateSelections(nodeDict, wireDict, ioDict); + break; + } + case CircuitBoxOpcode.AddWire: + { + var data = INetSerializableStruct.Read(msg); + AddWireFromData(data); + break; + } + case CircuitBoxOpcode.RemoveWire: + { + var data = INetSerializableStruct.Read(msg); + RemoveWireInternal(data.TargetIDs); + break; + } + case CircuitBoxOpcode.ServerInitialize: + { + Components.Clear(); + Wires.Clear(); + + var data = INetSerializableStruct.Read(msg); + foreach (var compData in data.Components) { AddComponentFromData(compData); } + foreach (var wireData in data.Wires) { AddWireFromData(wireData); } + + foreach (var node in InputOutputNodes) + { + node.Position = node.NodeType switch + { + CircuitBoxInputOutputNode.Type.Input => data.InputPos, + CircuitBoxInputOutputNode.Type.Output => data.OutputPos, + _ => node.Position + }; + } + wasInitializedByServer = true; + break; + } + default: + throw new ArgumentOutOfRangeException(nameof(header), header, "This opcode cannot be handled using entity events"); + } + } + + public void AddComponentFromData(CircuitBoxServerCreateComponentEvent data) + { + if (ItemPrefab.Prefabs.Find(p => p.UintIdentifier == data.UsedResource) is not { } prefab) + { + throw new Exception($"No item prefab found for \"{data.UsedResource}\""); + } + + AddComponentInternalUnsafe(data.ComponentId, FindItemByID(data.BackingItemId), prefab, data.Position); + } + + public void AddWireFromData(CircuitBoxServerCreateWireEvent data) + { + var (req, wireId, possibleItemId) = data; + var prefab = ItemPrefab.Prefabs.Find(p => p.UintIdentifier == req.SelectedWirePrefabIdentifier); + if (prefab is null) + { + throw new Exception($"No prefab found for \"{req.SelectedWirePrefabIdentifier}\""); + } + + if (!req.Start.FindConnection(this).TryUnwrap(out var start)) + { + throw new Exception($"No connection found for ({req.Start})"); + } + + if (!req.End.FindConnection(this).TryUnwrap(out var end)) + { + throw new Exception($"No connection found for ({req.Start})"); + } + + if (possibleItemId.TryUnwrap(out var backingItem)) + { + CreateWireWithItem(start, end, wireId, FindItemByID(backingItem)); + } + else + { + CreateWireWithoutItem(start, end, wireId, prefab); + } + } + + public static Item FindItemByID(ushort id) + => Entity.FindEntityByID(id) as Item ?? throw new Exception($"No item with ID {id} exists."); + + public override void AddToGUIUpdateList(int order = 0) + { + base.AddToGUIUpdateList(order); + UI?.AddToGUIUpdateList(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs index 35ed6a50a..8b7255a32 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs @@ -21,6 +21,8 @@ namespace Barotrauma.Items.Components public float FlashTimer { get; private set; } public static Wire DraggingConnected { get; private set; } + private static float ConnectionSpriteSize => 35.0f * GUI.Scale; + public static void DrawConnections(SpriteBatch spriteBatch, ConnectionPanel panel, Rectangle dragArea, Character character, out (Vector2 tooltipPos, LocalizedString text) tooltip) { @@ -33,8 +35,6 @@ namespace Barotrauma.Items.Components int x = panelRect.X, y = panelRect.Y; int width = panelRect.Width, height = panelRect.Height; - Vector2 scale = GetScale(panel.GuiFrame.RectTransform.MaxSize, panel.GuiFrame.Rect.Size); - bool mouseInRect = panelRect.Contains(PlayerInput.MousePosition); int totalWireCount = 0; @@ -70,15 +70,15 @@ namespace Barotrauma.Items.Components //two passes: first the connector, then the wires to get the wires to render in front for (int i = 0; i < 2; i++) { - Vector2 rightPos = GetRightPos(x, y, width, scale); - Vector2 leftPos = GetLeftPos(x, y, scale); + Vector2 rightPos = GetRightPos(x, y, width); + Vector2 leftPos = GetLeftPos(x, y); - Vector2 rightWirePos = new Vector2(x + width - 5 * scale.X, y + 30 * scale.Y); - Vector2 leftWirePos = new Vector2(x + 5 * scale.X, y + 30 * scale.Y); + Vector2 rightWirePos = new Vector2(x + width - 5 * GUI.Scale, y + 30 * GUI.Scale); + Vector2 leftWirePos = new Vector2(x + 5 * GUI.Scale, y + 30 * GUI.Scale); - int wireInterval = (height - (int)(20 * scale.Y)) / Math.Max(totalWireCount, 1); - int connectorIntervalLeft = GetConnectorIntervalLeft(height, scale, panel); - int connectorIntervalRight = GetConnectorIntervalRight(height, scale, panel); + int wireInterval = (height - (int)(20 * GUI.Scale)) / Math.Max(totalWireCount, 1); + int connectorIntervalLeft = GetConnectorIntervalLeft(height, panel); + int connectorIntervalRight = GetConnectorIntervalRight(height, panel); foreach (Connection c in panel.Connections) { @@ -101,47 +101,26 @@ namespace Barotrauma.Items.Components } Vector2 position = c.IsOutput ? rightPos : leftPos; - Color highlightColor = Color.Transparent; if (ConnectionPanel.ShouldDebugDrawWiring) { - if (c.IsPower) - { - highlightColor = VisualizeSignal(0.0f, highlightColor, Color.Red); - } - else - { - highlightColor = VisualizeSignal(c.LastReceivedSignal.TimeSinceCreated, highlightColor, Color.LightGreen); - highlightColor = VisualizeSignal(c.LastSentSignal.TimeSinceCreated, highlightColor, Color.Orange); - } - bool mouseOn = Vector2.DistanceSquared(position, PlayerInput.MousePosition) < MathUtils.Pow2(35 * GUI.Scale); + DrawConnectionDebugInfo(spriteBatch, c, position, GUI.Scale, out var tooltipText); - LocalizedString toolTipText = c.GetToolTip(); - if (mouseOn) { tooltip = (position, toolTipText); } - if (!toolTipText.IsNullOrEmpty()) + if (!tooltipText.IsNullOrEmpty()) { - var glowSprite = GUIStyle.UIGlowCircular.Value.Sprite; - glowSprite.Draw(spriteBatch, position, highlightColor, glowSprite.size / 2, - scale: 45.0f / glowSprite.size.X * panel.Scale); + bool mouseOn = Vector2.DistanceSquared(position, PlayerInput.MousePosition) < MathUtils.Pow2(35 * GUI.Scale); + if (mouseOn) + { + tooltip = (position, tooltipText); + } } } - static Color VisualizeSignal(double timeSinceCreated, Color defaultColor, Color color) - { - if (timeSinceCreated < 1.0f) - { - float pulseAmount = (MathF.Sin((float)Timing.TotalTimeUnpaused * 10.0f) + 3.0f) / 4.0f; - Color targetColor = Color.Lerp(defaultColor, color, pulseAmount); - return Color.Lerp(targetColor, defaultColor, (float)timeSinceCreated); - } - return defaultColor; - } - //outputs are drawn at the right side of the panel, inputs at the left if (c.IsOutput) { if (i == 0) { - c.DrawConnection(spriteBatch, panel, rightPos, GetOutputLabelPosition(rightPos, panel, c), scale); + c.DrawConnection(spriteBatch, panel, rightPos, GetOutputLabelPosition(rightPos, panel, c)); } else { @@ -154,7 +133,7 @@ namespace Barotrauma.Items.Components { if (i == 0) { - c.DrawConnection(spriteBatch, panel, leftPos, GetInputLabelPosition(leftPos, panel, c), scale); + c.DrawConnection(spriteBatch, panel, leftPos, GetInputLabelPosition(leftPos, panel, c)); } else { @@ -224,8 +203,8 @@ namespace Barotrauma.Items.Components } - float step = (width * 0.75f) / panel.DisconnectedWires.Count(); - x = (int)(x + width / 2 - step * (panel.DisconnectedWires.Count() - 1) / 2); + float step = (width * 0.75f) / panel.DisconnectedWires.Count; + x = (int)(x + width / 2 - step * (panel.DisconnectedWires.Count - 1) / 2); foreach (Wire wire in panel.DisconnectedWires) { if (wire == DraggingConnected && mouseInRect) { continue; } @@ -243,26 +222,61 @@ namespace Barotrauma.Items.Components //stop dragging a wire item if the cursor is within any connection panel //(so we don't drop the item when dropping the wire on a connection) if (mouseInRect || (GUI.MouseOn?.UserData is ConnectionPanel && GUI.MouseOn.MouseRect.Contains(PlayerInput.MousePosition))) - { - Inventory.DraggingItems.Clear(); - } + { + Inventory.DraggingItems.Clear(); + } } - private void DrawConnection(SpriteBatch spriteBatch, ConnectionPanel panel, Vector2 position, Vector2 labelPos, Vector2 scale) + public static void DrawConnectionDebugInfo(SpriteBatch spriteBatch, Connection c, Vector2 position, float scale, out LocalizedString tooltip) + { + Color highlightColor = Color.Transparent; + if (c.IsPower) + { + highlightColor = VisualizeSignal(0.0f, highlightColor, Color.Red); + } + else + { + highlightColor = VisualizeSignal(c.LastReceivedSignal.TimeSinceCreated, highlightColor, Color.LightGreen); + highlightColor = VisualizeSignal(c.LastSentSignal.TimeSinceCreated, highlightColor, Color.Orange); + } + + LocalizedString toolTipText = c.GetToolTip(); + if (!toolTipText.IsNullOrEmpty()) + { + var glowSprite = GUIStyle.UIGlowCircular.Value.Sprite; + glowSprite.Draw(spriteBatch, position, highlightColor, glowSprite.size / 2, + scale: 45.0f / glowSprite.size.X * scale); + } + + tooltip = toolTipText; + + static Color VisualizeSignal(double timeSinceCreated, Color defaultColor, Color color) + { + if (timeSinceCreated < 1.0f) + { + float pulseAmount = (MathF.Sin((float)Timing.TotalTimeUnpaused * 10.0f) + 3.0f) / 4.0f; + Color targetColor = Color.Lerp(defaultColor, color, pulseAmount); + return Color.Lerp(targetColor, defaultColor, (float)timeSinceCreated); + } + return defaultColor; + } + } + + private void DrawConnection(SpriteBatch spriteBatch, ConnectionPanel panel, Vector2 position, Vector2 labelPos) { string text = DisplayName.Value.ToUpperInvariant(); //nasty if (GUIStyle.GetComponentStyle("ConnectionPanelLabel")?.Sprites.Values.First().First() is UISprite labelSprite) { - Rectangle labelArea = GetLabelArea(labelPos, text, scale); + Rectangle labelArea = GetLabelArea(labelPos, text); labelSprite.Draw(spriteBatch, labelArea, IsPower ? GUIStyle.Red : Color.SteelBlue); } GUI.DrawString(spriteBatch, labelPos + Vector2.UnitY, text, Color.Black * 0.8f, font: GUIStyle.SmallFont); GUI.DrawString(spriteBatch, labelPos, text, GUIStyle.TextColorBright, font: GUIStyle.SmallFont); - float connectorSpriteScale = (35.0f / connectionSprite.SourceRect.Width) * panel.Scale; + float connectorSpriteScale = ConnectionSpriteSize / connectionSprite.SourceRect.Width; connectionSprite.Draw(spriteBatch, position, scale: connectorSpriteScale); @@ -270,7 +284,7 @@ namespace Barotrauma.Items.Components private void DrawWires(SpriteBatch spriteBatch, ConnectionPanel panel, Vector2 position, Vector2 wirePosition, bool mouseIn, Wire equippedWire, float wireInterval) { - float connectorSpriteScale = (35.0f / connectionSprite.SourceRect.Width) * panel.Scale; + float connectorSpriteScale = ConnectionSpriteSize / connectionSprite.SourceRect.Width; foreach (var wire in wires) { @@ -285,9 +299,14 @@ namespace Barotrauma.Items.Components wirePosition.Y += wireInterval; } - if (DraggingConnected != null && Vector2.Distance(position, PlayerInput.MousePosition) < (20.0f * GUI.Scale)) + bool isMouseOn = Vector2.Distance(position, PlayerInput.MousePosition) < (20.0f * GUI.Scale); + if (isMouseOn) { connectionSpriteHighlight.Draw(spriteBatch, position, scale: connectorSpriteScale); + } + + if (DraggingConnected != null && isMouseOn) + { if (!PlayerInput.PrimaryMouseButtonHeld()) { @@ -418,7 +437,7 @@ namespace Barotrauma.Items.Components bool mouseOn = canDrag && - !(GUI.MouseOn is GUIDragHandle) && + GUI.MouseOn is not GUIDragHandle && ((PlayerInput.MousePosition.X > Math.Min(start.X, end.X) && PlayerInput.MousePosition.X < Math.Max(start.X, end.X) && MathUtils.LineToPointDistanceSquared(start, end, PlayerInput.MousePosition) < 36) || @@ -442,12 +461,12 @@ namespace Barotrauma.Items.Components } } - var wireEnd = end + Vector2.Normalize(start - end) * 30.0f * panel.Scale; + var wireEnd = end + Vector2.Normalize(start - end) * 30.0f * GUI.Scale; float dist = Vector2.Distance(start, wireEnd); - float wireWidth = 12 * panel.Scale; - float highlight = 5 * panel.Scale; + float wireWidth = 12 * GUI.Scale; + float highlight = 5 * GUI.Scale; if (mouseOn) { spriteBatch.Draw(wireVertical.Texture, new Rectangle(wireEnd.ToPoint(), new Point((int)(wireWidth + highlight), (int)dist)), wireVertical.SourceRect, @@ -488,110 +507,84 @@ namespace Barotrauma.Items.Components { Rectangle panelRect = panel.GuiFrame.Rect; int x = panelRect.X, y = panelRect.Y; - Vector2 scale = GetScale(panel.GuiFrame.RectTransform.MaxSize, panel.GuiFrame.Rect.Size); - Vector2 rightPos = GetRightPos(x, y, panelRect.Width, scale); - Vector2 leftPos = GetLeftPos(x, y, scale); - int connectorIntervalLeft = GetConnectorIntervalLeft(panelRect.Height, scale, panel); - int connectorIntervalRight = GetConnectorIntervalRight(panelRect.Height, scale, panel); + Vector2 rightPos = GetRightPos(x, y, panelRect.Width); + Vector2 leftPos = GetLeftPos(x, y); + int connectorIntervalLeft = GetConnectorIntervalLeft(panelRect.Height, panel); + int connectorIntervalRight = GetConnectorIntervalRight(panelRect.Height, panel); newRectSize = panelRect.Size; - var labelAreas = new List(); - for (int i = 0; i < 100; i++) + + //make sure the connection labels don't overlap horizontally + float rightMostInput = panelRect.Center.X; + float leftMostOutput = panelRect.Center.X; + foreach (var c in panel.Connections) { - labelAreas.Clear(); - foreach (var c in panel.Connections) + if (c.IsOutput) { - if (c.IsOutput) - { - var labelArea = GetLabelArea(GetOutputLabelPosition(rightPos, panel, c), c.DisplayName.Value.ToUpperInvariant(), scale); - labelAreas.Add(labelArea); - rightPos.Y += connectorIntervalLeft; - } - else - { - var labelArea = GetLabelArea(GetInputLabelPosition(leftPos, panel, c), c.DisplayName.Value.ToUpperInvariant(), scale); - labelAreas.Add(labelArea); - leftPos.Y += connectorIntervalRight; - } + var labelArea = GetLabelArea(GetOutputLabelPosition(rightPos, panel, c), c.DisplayName.Value.ToUpperInvariant()); + leftMostOutput = Math.Min(leftMostOutput, labelArea.X); + rightPos.Y += connectorIntervalLeft; } - bool foundOverlap = false; - for (int j = 0; j < labelAreas.Count; j++) + else { - for (int k = 0; k < labelAreas.Count; k++) - { - if (k == j) { continue; } - if (!labelAreas[j].Intersects(labelAreas[k])) { continue; } - newRectSize += new Point(10); - Point maxSize = new Point( - Math.Max(panel.GuiFrame.RectTransform.MaxSize.X, newRectSize.X), - Math.Max(panel.GuiFrame.RectTransform.MaxSize.Y, newRectSize.Y)); - scale = GetScale(maxSize, newRectSize); - rightPos = GetRightPos(x, y, newRectSize.X, scale); - leftPos = GetLeftPos(x, y, scale); - connectorIntervalLeft = GetConnectorIntervalLeft(newRectSize.Y, scale, panel); - connectorIntervalRight = GetConnectorIntervalRight(newRectSize.Y, scale, panel); - foundOverlap = true; - break; - } + var labelArea = GetLabelArea(GetInputLabelPosition(leftPos, panel, c), c.DisplayName.Value.ToUpperInvariant()); + rightMostInput = Math.Max(rightMostInput, labelArea.Right); + leftPos.Y += connectorIntervalRight; } - if (!foundOverlap) { break; } + } + if (leftMostOutput < rightMostInput) + { + newRectSize += new Point((int)(rightMostInput - leftMostOutput) + GUI.IntScale(15), 0); + } + + //make sure connection sprites don't overlap vertically + while (GetConnectorIntervalLeft(newRectSize.Y, panel) < ConnectionSpriteSize || + GetConnectorIntervalRight(newRectSize.Y, panel) < ConnectionSpriteSize) + { + newRectSize.Y += 10; } return newRectSize.X != panel.GuiFrame.Rect.Width || newRectSize.Y > panel.GuiFrame.Rect.Height; } - private static Vector2 GetScale(Point maxSize, Point size) - { - Vector2 scale = new Vector2(GUI.Scale); - if (maxSize.X < int.MaxValue) - { - scale.X = maxSize.X / size.X; - } - if (maxSize.Y < int.MaxValue) - { - scale.Y = maxSize.Y / size.Y; - } - return scale; - } - private static Vector2 GetInputLabelPosition(Vector2 connectorPosition, ConnectionPanel panel, Connection connection) { return new Vector2( - connectorPosition.X + 25 * panel.Scale, - connectorPosition.Y - 5 * panel.Scale - GUIStyle.SmallFont.MeasureString(connection.DisplayName.ToUpper()).Y); + connectorPosition.X + 25 * GUI.Scale, + connectorPosition.Y - GUIStyle.SmallFont.MeasureString(connection.DisplayName.ToUpper()).Y / 2); } private static Vector2 GetOutputLabelPosition(Vector2 connectorPosition, ConnectionPanel panel, Connection connection) { return new Vector2( - connectorPosition.X - 25 * panel.Scale - GUIStyle.SmallFont.MeasureString(connection.DisplayName.ToUpper()).X, - connectorPosition.Y + 5 * panel.Scale); + connectorPosition.X - 25 * GUI.Scale - GUIStyle.SmallFont.MeasureString(connection.DisplayName.ToUpper()).X, + connectorPosition.Y - GUIStyle.SmallFont.MeasureString(connection.DisplayName.ToUpper()).Y / 2); } - private static Rectangle GetLabelArea(Vector2 labelPos, string text, Vector2 scale) + private static Rectangle GetLabelArea(Vector2 labelPos, string text) { Vector2 textSize = GUIStyle.SmallFont.MeasureString(text); Rectangle labelArea = new Rectangle(labelPos.ToPoint(), textSize.ToPoint()); - labelArea.Inflate(10 * scale.X, 3 * scale.Y); + labelArea.Inflate(GUI.IntScale(10), GUI.IntScale(3)); return labelArea; } - private static Vector2 GetLeftPos(int x, int y, Vector2 scale) + private static Vector2 GetLeftPos(int x, int y) { - return new Vector2(x + 80 * scale.X, y + 60 * scale.Y); + return new Vector2(x + 80 * GUI.Scale, y + 60 * GUI.Scale); } - private static Vector2 GetRightPos(int x, int y, int width, Vector2 scale) + private static Vector2 GetRightPos(int x, int y, int width) { - return new Vector2(x + width - 80 * scale.X, y + 60 * scale.Y); + return new Vector2(x + width - 80 * GUI.Scale, y + 60 * GUI.Scale); } - private static int GetConnectorIntervalLeft(int height, Vector2 scale, ConnectionPanel panel) + private static int GetConnectorIntervalLeft(int height, ConnectionPanel panel) { - return (height - (int)(100 * scale.Y)) / Math.Max(panel.Connections.Count(c => c.IsOutput), 1); + return (height - GUI.IntScale(60)) / Math.Max(panel.Connections.Count(c => c.IsOutput), 1); } - private static int GetConnectorIntervalRight(int height, Vector2 scale, ConnectionPanel panel) + private static int GetConnectorIntervalRight(int height, ConnectionPanel panel) { - return (height - (int)(100 * scale.Y)) / Math.Max(panel.Connections.Count(c => !c.IsOutput), 1); + return (height - GUI.IntScale(60)) / Math.Max(panel.Connections.Count(c => !c.IsOutput), 1); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs index d0c3dfda4..50ee85985 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs @@ -22,11 +22,6 @@ namespace Barotrauma.Items.Components private SoundChannel rewireSoundChannel; private float rewireSoundTimer; - public float Scale - { - get { return GuiFrame.Rect.Width / 400.0f; } - } - private Point originalMaxSize; private Vector2 originalRelativeSize; @@ -104,10 +99,10 @@ namespace Barotrauma.Items.Components public override bool ShouldDrawHUD(Character character) { - return character == Character.Controlled && character == user && character.SelectedItem == item; + return character == Character.Controlled && character == user && (character.SelectedItem == item || character.SelectedSecondaryItem == item); } - public override void UpdateHUD(Character character, float deltaTime, Camera cam) + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { if (character != Character.Controlled || character != user || character.SelectedItem != item) { return; } @@ -162,10 +157,11 @@ namespace Barotrauma.Items.Components //because some of the wires connected to the panel may not exist yet long msgStartPos = msg.BitPosition; msg.ReadUInt16(); //user ID - foreach (Connection _ in Connections) + byte connectionCount = msg.ReadByte(); + for (int i = 0; i < connectionCount; i++) { uint wireCount = msg.ReadVariableUInt32(); - for (int i = 0; i < wireCount; i++) + for (int j = 0; j < wireCount; j++) { msg.ReadUInt16(); } @@ -208,21 +204,25 @@ namespace Barotrauma.Items.Components connection.ClearConnections(); } - foreach (Connection connection in Connections) + byte connectionCount = msg.ReadByte(); + for (int i = 0; i < connectionCount; i++) { HashSet newWires = new HashSet(); uint wireCount = msg.ReadVariableUInt32(); - for (int i = 0; i < wireCount; i++) + for (int j = 0; j < wireCount; j++) { ushort wireId = msg.ReadUInt16(); - - if (!(Entity.FindEntityByID(wireId) is Item wireItem)) { continue; } + if (Entity.FindEntityByID(wireId) is not Item wireItem) { continue; } Wire wireComponent = wireItem.GetComponent(); if (wireComponent == null) { continue; } newWires.Add(wireComponent); } + //this may happen if the item has been deleted server-side at the point the server is writing this event to the client + if (i >= Connections.Count) { continue; } + + var connection = Connections[i]; Wire[] oldWires = connection.Wires.Where(w => !newWires.Contains(w)).ToArray(); foreach (var wire in oldWires) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs index 2f4c6a63c..c821da993 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs @@ -244,7 +244,7 @@ namespace Barotrauma.Items.Components } } - public override void UpdateHUD(Character character, float deltaTime, Camera cam) + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { bool elementVisibilityChanged = false; int visibleElementCount = 0; @@ -335,17 +335,18 @@ namespace Barotrauma.Items.Components if (signals == null) { return; } for (int i = 0; i < signals.Length && i < uiElements.Count; i++) { + string signal = customInterfaceElementList[i].Signal; if (uiElements[i] is GUITextBox tb) { tb.Text = Screen.Selected is { IsEditor: true } ? - customInterfaceElementList[i].Signal : - TextManager.Get(customInterfaceElementList[i].Signal).Value; + signal : + TextManager.Get(signal).Fallback(signal).Value; } else if (uiElements[i] is GUINumberInput ni) { if (ni.InputType == NumberType.Int) { - int.TryParse(customInterfaceElementList[i].Signal, out int value); + int.TryParse(signal, out int value); ni.IntValue = value; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/MotionSensor.cs index 8d7ec8a22..88dde557b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/MotionSensor.cs @@ -10,7 +10,7 @@ namespace Barotrauma.Items.Components get { return new Vector2(rangeX, rangeY) * 2.0f; } } - public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) { if (!editing || !MapEntity.SelectedList.Contains(item)) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs index c879cb159..21cad1ace 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs @@ -86,15 +86,15 @@ namespace Barotrauma.Items.Components } OutputValue = input; - ShowOnDisplay(input, addToHistory: true, TextColor); + ShowOnDisplay(input, addToHistory: true, TextColor, isWelcomeMessage: false); item.SendSignal(input, "signal_out"); } - partial void ShowOnDisplay(string input, bool addToHistory, Color color) + partial void ShowOnDisplay(string input, bool addToHistory, Color color, bool isWelcomeMessage) { if (addToHistory) { - messageHistory.Add(new TerminalMessage(input, color)); + messageHistory.Add(new TerminalMessage(input, color, isWelcomeMessage)); while (messageHistory.Count > MaxMessages) { messageHistory.RemoveAt(0); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/WifiComponent.cs index 64793fa05..e0e8ebd4a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/WifiComponent.cs @@ -11,9 +11,9 @@ namespace Barotrauma.Items.Components get { return new Vector2(range * 2); } } - public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) { - if (!editing || !MapEntity.SelectedList.Contains(item)) return; + if (!editing || !MapEntity.SelectedList.Contains(item)) { return; } Vector2 pos = new Vector2(item.DrawPosition.X, -item.DrawPosition.Y); ShapeExtensions.DrawLine(spriteBatch, pos + Vector2.UnitY * range, pos - Vector2.UnitY * range, Color.Cyan * 0.5f, 2); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs index 81d3fa3ee..9bafa3d1a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs @@ -186,12 +186,12 @@ namespace Barotrauma.Items.Components return Color.LightBlue; } - public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) { - Draw(spriteBatch, editing, Vector2.Zero, itemDepth); + Draw(spriteBatch, editing, Vector2.Zero, itemDepth, overrideColor); } - public void Draw(SpriteBatch spriteBatch, bool editing, Vector2 offset, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing, Vector2 offset, float itemDepth = -1, Color? overrideColor = null) { if (sections.Count == 0 && !IsActive || Hidden) { @@ -223,7 +223,7 @@ namespace Barotrauma.Items.Components foreach (WireSection section in sections) { - section.Draw(spriteBatch, wireSprite, item.Color, drawOffset, depth, Width); + section.Draw(spriteBatch, wireSprite, overrideColor ?? item.Color, drawOffset, depth, Width); } if (nodes.Count > 0) @@ -271,13 +271,13 @@ namespace Barotrauma.Items.Components spriteBatch, wireSprite, nodes[^1] + drawOffset, new Vector2(newNodePos.X, newNodePos.Y) + drawOffset, - item.Color, 0.0f, Width); + overrideColor ?? item.Color, 0.0f, Width); WireSection.Draw( spriteBatch, wireSprite, new Vector2(newNodePos.X, newNodePos.Y) + drawOffset, item.DrawPosition, - item.Color, itemDepth, Width); + overrideColor ?? item.Color, itemDepth, Width); GUI.DrawRectangle(spriteBatch, new Vector2(newNodePos.X + drawOffset.X, -(newNodePos.Y + drawOffset.Y)) - Vector2.One * 3, Vector2.One * 6, item.Color); } @@ -287,7 +287,7 @@ namespace Barotrauma.Items.Components spriteBatch, wireSprite, nodes[^1] + drawOffset, item.DrawPosition, - item.Color, 0.0f, Width); + overrideColor ?? item.Color, 0.0f, Width); } } } @@ -594,7 +594,7 @@ namespace Barotrauma.Items.Components selectedWire.shouldClearConnections = false; Character.Controlled.Inventory.TryPutItem(selectedWire.item, Character.Controlled, new List { InvSlotType.LeftHand, InvSlotType.RightHand }); - foreach (var entity in MapEntity.mapEntityList) + foreach (var entity in MapEntity.MapEntityList) { if (entity is Item item) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index 402805f70..5e5cc982f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -117,6 +117,13 @@ namespace Barotrauma.Items.Components get { return barrelSprite; } } + [Serialize(false, IsPropertySaveable.No)] + public bool HideBarrelWhenBroken + { + get; + private set; + } + partial void InitProjSpecific(ContentXElement element) { foreach (var subElement in element.Elements()) @@ -232,7 +239,7 @@ namespace Barotrauma.Items.Components { moveSoundChannel.FadeOutAndDispose(); moveSoundChannel = SoundPlayer.PlaySound(endMoveSound.Sound, item.WorldPosition, endMoveSound.Volume, endMoveSound.Range, ignoreMuffling: endMoveSound.IgnoreMuffling, freqMult: endMoveSound.GetRandomFrequencyMultiplier()); - if (moveSoundChannel != null) moveSoundChannel.Looping = false; + if (moveSoundChannel != null) { moveSoundChannel.Looping = false; } } else if (!moveSoundChannel.IsPlaying) { @@ -261,12 +268,13 @@ namespace Barotrauma.Items.Components if (chargeSound != null) { chargeSoundChannel = SoundPlayer.PlaySound(chargeSound.Sound, item.WorldPosition, chargeSound.Volume, chargeSound.Range, ignoreMuffling: chargeSound.IgnoreMuffling, freqMult: chargeSound.GetRandomFrequencyMultiplier()); - if (chargeSoundChannel != null) chargeSoundChannel.Looping = true; + if (chargeSoundChannel != null) { chargeSoundChannel.Looping = true; } } } else if (chargeSoundChannel != null) { chargeSoundChannel.FrequencyMultiplier = MathHelper.Lerp(0.5f, 1.5f, chargeRatio); + chargeSoundChannel.Position = new Vector3(item.WorldPosition, 0.0f); } break; default: @@ -318,7 +326,7 @@ namespace Barotrauma.Items.Components } } - public override void UpdateHUD(Character character, float deltaTime, Camera cam) + public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { if (crosshairSprite != null) { @@ -359,7 +367,7 @@ namespace Barotrauma.Items.Components return new Vector2((float)Math.Cos(rotation), (float)Math.Sin(rotation)) * recoilOffset; } - public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1, Color? overrideColor = null) { if (!MathUtils.NearlyEqual(item.Rotation, prevBaseRotation) || !MathUtils.NearlyEqual(item.Scale, prevScale)) { @@ -367,53 +375,55 @@ namespace Barotrauma.Items.Components } Vector2 drawPos = GetDrawPos(); - - railSprite?.Draw(spriteBatch, - drawPos, - item.SpriteColor, - rotation + MathHelper.PiOver2, item.Scale, - SpriteEffects.None, item.SpriteDepth + (railSprite.Depth - item.Sprite.Depth)); - - barrelSprite?.Draw(spriteBatch, - drawPos - GetRecoilOffset() * item.Scale, - item.SpriteColor, - rotation + MathHelper.PiOver2, item.Scale, - SpriteEffects.None, item.SpriteDepth + (barrelSprite.Depth - item.Sprite.Depth)); - - float chargeRatio = currentChargeTime / MaxChargeTime; - - foreach ((Sprite chargeSprite, Vector2 position) in chargeSprites) + if (item.Condition > 0.0f || !HideBarrelWhenBroken) { - chargeSprite?.Draw(spriteBatch, - drawPos - MathUtils.RotatePoint(new Vector2(position.X * chargeRatio, position.Y * chargeRatio) * item.Scale, rotation + MathHelper.PiOver2), - item.SpriteColor, + railSprite?.Draw(spriteBatch, + drawPos, + overrideColor ?? item.SpriteColor, rotation + MathHelper.PiOver2, item.Scale, - SpriteEffects.None, item.SpriteDepth + (chargeSprite.Depth - item.Sprite.Depth)); - } + SpriteEffects.None, item.SpriteDepth + (railSprite.Depth - item.Sprite.Depth)); - int spinningBarrelCount = spinningBarrelSprites.Count; - - for (int i = 0; i < spinningBarrelCount; i++) - { - // this block is messy since I was debugging it with a bunch of values, should be cleaned up / optimized if prototype is accepted - Sprite spinningBarrel = spinningBarrelSprites[i]; - float barrelCirclePosition = (MaxCircle * i / spinningBarrelCount + currentBarrelSpin) % MaxCircle; - - float newDepth = item.SpriteDepth + (spinningBarrel.Depth - item.Sprite.Depth) + (barrelCirclePosition > HalfCircle ? 0.0f : 0.001f); - - float barrelColorPosition = (barrelCirclePosition + QuarterCircle) % MaxCircle; - float colorOffset = Math.Abs(barrelColorPosition - HalfCircle) / HalfCircle; - Color newColorModifier = Color.Lerp(Color.Black, Color.Gray, colorOffset); - - float barrelHalfCirclePosition = Math.Abs(barrelCirclePosition - HalfCircle); - float barrelPositionModifier = MathUtils.SmoothStep(barrelHalfCirclePosition / HalfCircle); - float newPositionOffset = barrelPositionModifier * SpinningBarrelDistance; - - spinningBarrel.Draw(spriteBatch, - drawPos - MathUtils.RotatePoint(new Vector2(newPositionOffset, 0f) * item.Scale, rotation + MathHelper.PiOver2), - Color.Lerp(item.SpriteColor, newColorModifier, 0.8f), + barrelSprite?.Draw(spriteBatch, + drawPos - GetRecoilOffset() * item.Scale, + overrideColor ?? item.SpriteColor, rotation + MathHelper.PiOver2, item.Scale, - SpriteEffects.None, newDepth); + SpriteEffects.None, item.SpriteDepth + (barrelSprite.Depth - item.Sprite.Depth)); + + float chargeRatio = currentChargeTime / MaxChargeTime; + + foreach ((Sprite chargeSprite, Vector2 position) in chargeSprites) + { + chargeSprite?.Draw(spriteBatch, + drawPos - MathUtils.RotatePoint(new Vector2(position.X * chargeRatio, position.Y * chargeRatio) * item.Scale, rotation + MathHelper.PiOver2), + item.SpriteColor, + rotation + MathHelper.PiOver2, item.Scale, + SpriteEffects.None, item.SpriteDepth + (chargeSprite.Depth - item.Sprite.Depth)); + } + + int spinningBarrelCount = spinningBarrelSprites.Count; + + for (int i = 0; i < spinningBarrelCount; i++) + { + // this block is messy since I was debugging it with a bunch of values, should be cleaned up / optimized if prototype is accepted + Sprite spinningBarrel = spinningBarrelSprites[i]; + float barrelCirclePosition = (MaxCircle * i / spinningBarrelCount + currentBarrelSpin) % MaxCircle; + + float newDepth = item.SpriteDepth + (spinningBarrel.Depth - item.Sprite.Depth) + (barrelCirclePosition > HalfCircle ? 0.0f : 0.001f); + + float barrelColorPosition = (barrelCirclePosition + QuarterCircle) % MaxCircle; + float colorOffset = Math.Abs(barrelColorPosition - HalfCircle) / HalfCircle; + Color newColorModifier = Color.Lerp(Color.Black, Color.Gray, colorOffset); + + float barrelHalfCirclePosition = Math.Abs(barrelCirclePosition - HalfCircle); + float barrelPositionModifier = MathUtils.SmoothStep(barrelHalfCirclePosition / HalfCircle); + float newPositionOffset = barrelPositionModifier * SpinningBarrelDistance; + + spinningBarrel.Draw(spriteBatch, + drawPos - MathUtils.RotatePoint(new Vector2(newPositionOffset, 0f) * item.Scale, rotation + MathHelper.PiOver2), + Color.Lerp(overrideColor ?? item.SpriteColor, newColorModifier, 0.8f), + rotation + MathHelper.PiOver2, item.Scale, + SpriteEffects.None, newDepth); + } } if (GameMain.DebugDraw) @@ -593,6 +603,7 @@ namespace Barotrauma.Items.Components if (!recipient.IsPower || !recipient.IsOutput) { continue; } var battery = recipient.Item?.GetComponent(); if (battery == null || battery.Item.Condition <= 0.0f) { continue; } + if (battery.OutputDisabled) { continue; } availableCharge += battery.Charge; availableCapacity += battery.GetCapacity(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs index e1f12a51b..55a920129 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs @@ -19,7 +19,7 @@ namespace Barotrauma.Items.Components get { return Vector2.Zero; } } - public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) { if (dockingState == 0.0f) return; @@ -39,7 +39,8 @@ namespace Barotrauma.Items.Components drawPos, new Rectangle( rect.Center.X + (int)(rect.Width / 2 * (1.0f - dockingState)), rect.Y, - (int)(rect.Width / 2 * dockingState), rect.Height), Color.White); + (int)(rect.Width / 2 * dockingState), rect.Height), + overrideColor ?? Color.White); } else @@ -48,7 +49,8 @@ namespace Barotrauma.Items.Components drawPos - Vector2.UnitX * (rect.Width / 2 * dockingState), new Rectangle( rect.X, rect.Y, - (int)(rect.Width / 2 * dockingState), rect.Height), Color.White); + (int)(rect.Width / 2 * dockingState), rect.Height), + overrideColor ?? Color.White); } } else @@ -61,7 +63,8 @@ namespace Barotrauma.Items.Components drawPos - Vector2.UnitY * (rect.Height / 2 * dockingState), new Rectangle( rect.X, rect.Y, - rect.Width, (int)(rect.Height / 2 * dockingState)), Color.White); + rect.Width, (int)(rect.Height / 2 * dockingState)), + overrideColor ?? Color.White); } else { @@ -69,7 +72,8 @@ namespace Barotrauma.Items.Components drawPos, new Rectangle( rect.X, rect.Y + rect.Height / 2 + (int)(rect.Height / 2 * (1.0f - dockingState)), - rect.Width, (int)(rect.Height / 2 * dockingState)), Color.White); + rect.Width, (int)(rect.Height / 2 * dockingState)), + overrideColor ?? Color.White); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 96607abbd..ac8f2e935 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -182,6 +182,7 @@ namespace Barotrauma public Rectangle BackgroundFrame { get; protected set; } + private List[] partialReceivedItemIDs; private List[] receivedItemIDs; private CoroutineHandle syncItemsCoroutine; @@ -257,7 +258,7 @@ namespace Barotrauma else { LocalizedString description = item.Description; - if (item.HasTag("identitycard") || item.HasTag("despawncontainer")) + if (item.HasTag(Tags.IdCard) || item.HasTag(Tags.DespawnContainer)) { string[] readTags = item.Tags.Split(','); string idName = null; @@ -347,6 +348,9 @@ namespace Barotrauma { toolTip += item.Prefab.GetSkillRequirementHints(character); } +#if DEBUG + toolTip += $" ({item.Prefab.Identifier})"; +#endif return RichString.Rich(toolTip); } } @@ -387,6 +391,12 @@ namespace Barotrauma /// public RectTransform RectTransform; + /// + /// Normally false - we don't draw the UI because it's drawn when the player hovers the cursor over the item in their inventory. + /// Enabled in special cases like equippable fabricators where the inventory is a part of the fabricator UI. + /// + public bool DrawWhenEquipped; + public static SlotReference SelectedSlot { get @@ -417,6 +427,7 @@ namespace Barotrauma padding = new Vector4(spacing.X, spacing.Y, spacing.X, spacing.X); + Vector2 slotAreaSize = new Vector2( columns * rectSize.X + (columns - 1) * spacing.X, rows * rectSize.Y + (rows - 1) * spacing.Y); @@ -427,6 +438,8 @@ namespace Barotrauma GameMain.GraphicsWidth / 2 - slotAreaSize.X / 2, GameMain.GraphicsHeight / 2 - slotAreaSize.Y / 2); + Vector2 center = topLeft + slotAreaSize / 2; + if (RectTransform != null) { Vector2 scale = new Vector2( @@ -438,6 +451,8 @@ namespace Barotrauma padding.X *= scale.X; padding.Z *= scale.X; padding.Y *= scale.Y; padding.W *= scale.Y; + center = RectTransform.Rect.Center.ToVector2(); + topLeft = RectTransform.TopLeft.ToVector2() + new Vector2(padding.X, padding.Y); prevRect = RectTransform.Rect; } @@ -445,8 +460,14 @@ namespace Barotrauma Rectangle slotRect = new Rectangle((int)topLeft.X, (int)topLeft.Y, (int)rectSize.X, (int)rectSize.Y); for (int i = 0; i < capacity; i++) { - slotRect.X = (int)(topLeft.X + (rectSize.X + spacing.X) * (i % slotsPerRow)); - slotRect.Y = (int)(topLeft.Y + (rectSize.Y + spacing.Y) * ((int)Math.Floor((double)i / slotsPerRow))); + int row = (int)Math.Floor((double)i / slotsPerRow); + int slotsPerThisRow = Math.Min(slotsPerRow, capacity - row * slotsPerRow); + + int rowWidth = (int)(rectSize.X * slotsPerThisRow + spacing.X * (slotsPerThisRow - 1)); + slotRect.X = (int)(center.X) - rowWidth / 2; + slotRect.X += (int)((rectSize.X + spacing.X) * (i % slotsPerThisRow)); + + slotRect.Y = (int)(topLeft.Y + (rectSize.Y + spacing.Y) * row); visualSlots[i] = new VisualSlot(slotRect); visualSlots[i].InteractRect = new Rectangle( (int)(visualSlots[i].Rect.X - spacing.X / 2 - 1), (int)(visualSlots[i].Rect.Y - spacing.Y / 2 - 1), @@ -489,7 +510,7 @@ namespace Barotrauma return owner.SelectedCharacter != null|| (!(owner is Character character)) || !container.KeepOpenWhenEquippedBy(character) || !owner.HasEquippedItem(container.Item); } - protected virtual bool HideSlot(int i) + public virtual bool HideSlot(int i) { return visualSlots[i].Disabled || (slots[i].HideIfEmpty && slots[i].Empty()); } @@ -673,6 +694,7 @@ namespace Barotrauma var container = item.GetComponent(); if (container == null || !container.DrawInventory) { return; } + if (container.Inventory.DrawWhenEquipped) { return; } var subInventory = container.Inventory; if (subInventory.visualSlots == null) { subInventory.CreateSlots(); } @@ -871,44 +893,34 @@ namespace Barotrauma if (Character.Controlled.Inventory != null && !isSubEditor) { - var inv = Character.Controlled.Inventory; - for (var i = 0; i < inv.visualSlots.Length; i++) + if (IsOnInventorySlot(Character.Controlled.Inventory)) { return true; } + } + + if (Character.Controlled.SelectedCharacter?.Inventory != null && !isSubEditor) + { + if (IsOnInventorySlot(Character.Controlled.SelectedCharacter.Inventory)) { return true; } + } + + bool IsOnInventorySlot(Inventory inventory) + { + for (var i = 0; i < inventory.visualSlots.Length; i++) { - var slot = inv.visualSlots[i]; + if (inventory.HideSlot(i)) { continue; } + var slot = inventory.visualSlots[i]; if (slot.InteractRect.Contains(PlayerInput.MousePosition)) { return true; } // check if the equip button actually exists - if (slot.EquipButtonRect.Contains(PlayerInput.MousePosition) && - i >= 0 && inv.slots.Length > i && - !inv.slots[i].Empty()) - { - return true; - } - } - } - - if (Character.Controlled.SelectedCharacter?.Inventory != null && !isSubEditor) - { - var inv = Character.Controlled.SelectedCharacter.Inventory; - for (var i = 0; i < inv.visualSlots.Length; i++) - { - var slot = inv.visualSlots[i]; - if (slot.InteractRect.Contains(PlayerInput.MousePosition)) - { - return true; - } - - // check if the equip button actually exists - if (slot.EquipButtonRect.Contains(PlayerInput.MousePosition) && - i >= 0 && inv.slots.Length > i && - !inv.slots[i].Empty()) + if (slot.EquipButtonRect.Contains(PlayerInput.MousePosition) && + i >= 0 && inventory.slots.Length > i && + !inventory.slots[i].Empty()) { return true; } } + return false; } if (Character.Controlled.SelectedItem != null) @@ -1034,7 +1046,7 @@ namespace Barotrauma protected static void DrawToolTip(SpriteBatch spriteBatch, RichString toolTip, Rectangle highlightedSlot) { - GUIComponent.DrawToolTip(spriteBatch, toolTip, highlightedSlot); + GUIComponent.DrawToolTip(spriteBatch, toolTip, highlightedSlot, Anchor.BottomRight); } public void DrawSubInventory(SpriteBatch spriteBatch, int slotIndex) @@ -1046,6 +1058,7 @@ namespace Barotrauma if (container == null || !container.DrawInventory) { return; } if (container.Inventory.visualSlots == null || !container.Inventory.isSubInventory) { return; } + if (container.Inventory.DrawWhenEquipped) { return; } int itemCapacity = container.Capacity; @@ -1215,8 +1228,8 @@ namespace Barotrauma else { DraggingItems.ForEachMod(it => it.Drop(Character.Controlled)); + DraggingItems.First().CreateDroppedStack(DraggingItems, allowClientExecute: false); } - SoundPlayer.PlayUISound(removed ? GUISoundType.PickItem : GUISoundType.DropItem); } } @@ -1258,11 +1271,19 @@ namespace Barotrauma { allowCombine = false; } + int itemCount = 0; foreach (Item item in DraggingItems) { bool success = selectedInventory.TryPutItem(item, slotIndex, allowSwapping: !anySuccess, allowCombine, Character.Controlled); - anySuccess |= success; - if (!success) { break; } + if (success) + { + anySuccess = true; + itemCount++; + } + if (!success || itemCount >= item.Prefab.GetMaxStackSize(selectedInventory)) + { + break; + } } if (anySuccess) @@ -1360,10 +1381,12 @@ namespace Barotrauma protected static Rectangle GetSubInventoryHoverArea(SlotReference subSlot) { Rectangle hoverArea; - if (!subSlot.Inventory.Movable() || + if ((Screen.Selected != GameMain.SubEditorScreen || GameMain.SubEditorScreen.DrawCharacterInventory) && + (!subSlot.Inventory.Movable() || (Character.Controlled?.Inventory == subSlot.ParentInventory && !Character.Controlled.HasEquippedItem(subSlot.Item)) || - (subSlot.ParentInventory is CharacterInventory characterInventory && characterInventory.CurrentLayout != CharacterInventory.Layout.Default)) + (subSlot.ParentInventory is CharacterInventory characterInventory && characterInventory.CurrentLayout != CharacterInventory.Layout.Default))) { + //slot not visible as a separate, movable panel -> just use the area of the slot directly hoverArea = subSlot.Slot.Rect; hoverArea.Location += subSlot.Slot.DrawOffset.ToPoint(); hoverArea = Rectangle.Union(hoverArea, subSlot.Slot.EquipButtonRect); @@ -1372,7 +1395,10 @@ namespace Barotrauma { hoverArea = subSlot.Inventory.BackgroundFrame; hoverArea.Location += subSlot.Slot.DrawOffset.ToPoint(); - hoverArea = Rectangle.Union(hoverArea, subSlot.Inventory.movableFrameRect); + if (subSlot.Inventory.movableFrameRect != Rectangle.Empty) + { + hoverArea = Rectangle.Union(hoverArea, subSlot.Inventory.movableFrameRect); + } } if (subSlot.Inventory?.visualSlots != null) @@ -1465,18 +1491,28 @@ namespace Barotrauma color: Character.Controlled.FocusedItem == null && !mouseOnHealthInterface ? GUIStyle.Red : Color.LightGreen, font: GUIStyle.SmallFont); } + + Item draggedItem = DraggingItems.First(); + sprite.Draw(spriteBatch, itemPos + Vector2.One * 2, Color.Black, scale: scale); sprite.Draw(spriteBatch, itemPos, - sprite == DraggingItems.First().Sprite ? DraggingItems.First().GetSpriteColor() : DraggingItems.First().GetInventoryIconColor(), + sprite == draggedItem.Sprite ? draggedItem.GetSpriteColor() : draggedItem.GetInventoryIconColor(), scale: scale); - if (DraggingItems.First().Prefab.MaxStackSize > 1) + if (draggedItem.Prefab.GetMaxStackSize(null) > 1) { + int stackAmount = DraggingItems.Count; + if (selectedSlot?.ParentInventory != null) + { + stackAmount = Math.Min( + stackAmount, + selectedSlot.ParentInventory.HowManyCanBePut(draggedItem.Prefab, selectedSlot.SlotIndex, draggedItem.Condition)); + } Vector2 stackCountPos = itemPos + Vector2.One * iconSize * 0.25f; - string stackCountText = "x" + DraggingItems.Count; + string stackCountText = "x" + stackAmount; GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos + Vector2.One, Color.Black); - GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos, Color.White); + GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos, GUIStyle.TextColorBright); } } } @@ -1673,7 +1709,7 @@ namespace Barotrauma color: GUIStyle.Red, scale: iconSize.X / stealIcon.size.X); } - int maxStackSize = item.Prefab.MaxStackSize; + int maxStackSize = item.Prefab.GetMaxStackSize(inventory); if (inventory is ItemInventory itemInventory) { maxStackSize = Math.Min(maxStackSize, itemInventory.Container.GetMaxStackSize(slotIndex)); @@ -1691,7 +1727,7 @@ namespace Barotrauma } } - if (HealingCooldown.IsOnCooldown && item.HasTag(HealingCooldown.MedicalItemTag)) + if (HealingCooldown.IsOnCooldown && item.HasTag(Tags.MedicalItem)) { RectangleF cdRect = rect; // shrink the rect from top to bottom depending on HealingCooldown.NormalizedCooldown @@ -1794,10 +1830,15 @@ namespace Barotrauma } } - public void ClientEventRead(IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg) { UInt16 lastEventID = msg.ReadUInt16(); - SharedRead(msg, out receivedItemIDs); + partialReceivedItemIDs ??= new List[capacity]; + SharedRead(msg, partialReceivedItemIDs, out bool readyToApply); + if (!readyToApply) { return; } + + receivedItemIDs = partialReceivedItemIDs.ToArray(); + partialReceivedItemIDs = null; //delay applying the new state if less than 1 second has passed since this client last sent a state to the server //prevents the inventory from briefly reverting to an old state if items are moved around in quick succession @@ -1866,7 +1907,7 @@ namespace Barotrauma if (!receivedItemIDs[i].Any()) { continue; } foreach (UInt16 id in receivedItemIDs[i]) { - if (!(Entity.FindEntityByID(id) is Item item) || slots[i].Contains(item)) { continue; } + if (Entity.FindEntityByID(id) is not Item item || slots[i].Contains(item)) { continue; } if (!TryPutItem(item, i, false, false, null, false)) { ForceToSlot(item, i); @@ -1883,11 +1924,5 @@ namespace Barotrauma receivedItemIDs = null; } - - public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) - { - SharedWrite(msg, extraData); - syncItemsDelay = 1.0f; - } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index c7da76748..0f42a313c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -38,7 +38,7 @@ namespace Barotrauma } } - partial void AssignCampaignInteractionTypeProjSpecific(CampaignMode.InteractionType interactionType) + partial void AssignCampaignInteractionTypeProjSpecific(CampaignMode.InteractionType interactionType, IEnumerable targetClients) { if (interactionType == CampaignMode.InteractionType.None) { @@ -316,6 +316,11 @@ namespace Barotrauma } public override void Draw(SpriteBatch spriteBatch, bool editing, bool back = true) + { + Draw(spriteBatch, editing, back, overrideColor: null); + } + + public void Draw(SpriteBatch spriteBatch, bool editing, bool back = true, Color? overrideColor = null) { if (!Visible || (!editing && HiddenInGame) || !SubEditorScreen.IsLayerVisible(this)) { return; } @@ -328,7 +333,9 @@ namespace Barotrauma else if (!ShowItems) { return; } } - Color color = IsIncludedInSelection && editing ? GUIStyle.Blue : GetSpriteColor(withHighlight: true); + Color color = + overrideColor ?? + (IsIncludedInSelection && editing ? GUIStyle.Blue : GetSpriteColor(withHighlight: true)); bool isWiringMode = editing && SubEditorScreen.TransparentWiringMode && SubEditorScreen.IsWiringMode() && !isWire && parentInventory == null; bool renderTransparent = isWiringMode && GetComponent() == null; @@ -412,15 +419,7 @@ namespace Barotrauma } else { - Vector2 origin = activeSprite.Origin; - if ((activeSprite.effects & SpriteEffects.FlipHorizontally) == SpriteEffects.FlipHorizontally) - { - origin.X = activeSprite.SourceRect.Width - origin.X; - } - if ((activeSprite.effects & SpriteEffects.FlipVertically) == SpriteEffects.FlipVertically) - { - origin.Y = activeSprite.SourceRect.Height - origin.Y; - } + Vector2 origin = GetSpriteOrigin(activeSprite); if (color.A > 0) { activeSprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, color, origin, RotationRad, Scale, activeSprite.effects, depth); @@ -484,7 +483,8 @@ namespace Barotrauma } } } - body.Draw(spriteBatch, activeSprite, color, depth, Scale); + Vector2 origin = GetSpriteOrigin(activeSprite); + body.Draw(spriteBatch, activeSprite, color, depth, Scale, origin: origin); if (fadeInBrokenSprite != null) { float d = Math.Min(depth + (fadeInBrokenSprite.Sprite.Depth - activeSprite.Depth - 0.000001f), 0.999f); @@ -536,7 +536,7 @@ namespace Barotrauma //causing them to be removed from the list for (int i = drawableComponents.Count - 1; i >= 0; i--) { - drawableComponents[i].Draw(spriteBatch, editing, depth); + drawableComponents[i].Draw(spriteBatch, editing, depth, overrideColor); } if (GameMain.DebugDraw) @@ -604,6 +604,20 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, from, to, lineColor, width: 1); //GUI.DrawString(spriteBatch, from, $"Linked to {e.Name}", lineColor, Color.Black * 0.5f); } + + Vector2 GetSpriteOrigin(Sprite sprite) + { + Vector2 origin = sprite.Origin; + if ((sprite.effects & SpriteEffects.FlipHorizontally) == SpriteEffects.FlipHorizontally) + { + origin.X = sprite.SourceRect.Width - origin.X; + } + if ((sprite.effects & SpriteEffects.FlipVertically) == SpriteEffects.FlipVertically) + { + origin.Y = sprite.SourceRect.Height - origin.Y; + } + return origin; + } } partial void OnCollisionProjSpecific(float impact) @@ -1076,7 +1090,7 @@ namespace Barotrauma if (!ignoreLocking && ic.LockGuiFramePosition) { continue; } //if the frame covers nearly all of the screen, don't trying to prevent overlaps because it'd fail anyway if (ic.GuiFrame.Rect.Width >= GameMain.GraphicsWidth * 0.9f && ic.GuiFrame.Rect.Height >= GameMain.GraphicsHeight * 0.9f) { continue; } - ic.GuiFrame.RectTransform.ScreenSpaceOffset = Point.Zero; + ic.GuiFrame.RectTransform.ScreenSpaceOffset = ic.GuiFrameOffset; elementsToMove.Add(ic.GuiFrame); debugInitialHudPositions.Add(ic.GuiFrame.Rect); } @@ -1281,6 +1295,11 @@ namespace Barotrauma nameText += $" ({idName})"; } } + if (DroppedStack.Any()) + { + nameText += $" x{DroppedStack.Count()}"; + } + texts.Add(new ColoredText(nameText, GUIStyle.TextColorNormal, false, false)); if (CampaignMode.BlocksInteraction(CampaignInteractionType)) @@ -1350,10 +1369,16 @@ namespace Barotrauma } } - if (Character.Controlled != null && Character.Controlled.SelectedItem != this && GetComponent() == null) + var character = Character.Controlled; + var selectedItem = Character.Controlled?.SelectedItem; + if (character != null && selectedItem != this && GetComponent() == null) { - if (Character.Controlled.SelectedItem?.GetComponent()?.TargetItem != this && - !Character.Controlled.HeldItems.Any(it => it.GetComponent()?.TargetItem == this)) + bool insideCircuitBox = + selectedItem?.GetComponent() != null && + selectedItem.ContainedItems.Contains(this); + if (!insideCircuitBox && + selectedItem?.GetComponent()?.TargetItem != this && + !character.HeldItems.Any(it => it.GetComponent()?.TargetItem == this)) { return; } @@ -1407,7 +1432,7 @@ namespace Barotrauma int containerIndex = msg.ReadRangedInteger(0, components.Count - 1); if (components[containerIndex] is ItemContainer container) { - container.Inventory.ClientEventRead(msg, sendingTime); + container.Inventory.ClientEventRead(msg); } else { @@ -1421,7 +1446,12 @@ namespace Barotrauma SetCondition(newCondition, isNetworkEvent: true, executeEffects: !loadingRound); break; case EventType.AssignCampaignInteraction: - CampaignInteractionType = (CampaignMode.InteractionType)msg.ReadByte(); + bool isVisible = msg.ReadBoolean(); + if (isVisible) + { + var interactionType = (CampaignMode.InteractionType)msg.ReadByte(); + AssignCampaignInteractionType(interactionType); + } break; case EventType.ApplyStatusEffect: { @@ -1486,6 +1516,30 @@ namespace Barotrauma AddUpgrade(upgrade, false); } break; + case EventType.DroppedStack: + int itemCount = msg.ReadRangedInteger(0, Inventory.MaxPossibleStackSize); + if (itemCount > 0) + { + List droppedStack = new List(); + for (int i = 0; i < itemCount; i++) + { + var id = msg.ReadUInt16(); + if (FindEntityByID(id) is not Item droppedItem) + { + DebugConsole.ThrowError($"Error while reading {EventType.DroppedStack} message: could not find an item with the ID {id}."); + } + else + { + droppedStack.Add(droppedItem); + } + } + CreateDroppedStack(droppedStack, allowClientExecute: true); + } + else + { + RemoveFromDroppedStack(allowClientExecute: true); + } + break; default: throw new Exception($"Malformed incoming item event: unsupported event type {eventType}"); } @@ -1511,7 +1565,7 @@ namespace Barotrauma { var component = componentStateEventData.Component; if (component is null) { throw error("component was null"); } - if (!(component is IClientSerializable clientSerializable)) { throw error($"component was not {nameof(IClientSerializable)}"); } + if (component is not IClientSerializable clientSerializable) { throw error($"component was not {nameof(IClientSerializable)}"); } int componentIndex = components.IndexOf(component); if (componentIndex < 0) { throw error("component did not belong to item"); } msg.WriteRangedInteger(componentIndex, 0, components.Count - 1); @@ -1525,7 +1579,7 @@ namespace Barotrauma int containerIndex = components.IndexOf(container); if (containerIndex < 0) { throw error("container did not belong to item"); } msg.WriteRangedInteger(containerIndex, 0, components.Count - 1); - container.Inventory.ClientEventWrite(msg, extraData); + container.Inventory.ClientEventWrite(msg, inventoryStateEventData); } break; case TreatmentEventData treatmentEventData: @@ -1551,7 +1605,7 @@ namespace Barotrauma { if (GameMain.Client == null) { return; } - if (parentInventory != null || body == null || !body.Enabled || Removed || (GetComponent()?.IsStuckToTarget ?? false)) + if (parentInventory != null || body == null || !body.Enabled || Removed || (GetComponent() is { IsStuckToTarget: true })) { positionBuffer.Clear(); return; @@ -1567,12 +1621,20 @@ namespace Barotrauma body.CorrectPosition(positionBuffer, out Vector2 newPosition, out Vector2 newVelocity, out float newRotation, out float newAngularVelocity); body.LinearVelocity = newVelocity; body.AngularVelocity = newAngularVelocity; - if (Vector2.DistanceSquared(newPosition, body.SimPosition) > 0.0001f || + float distSqr = Vector2.DistanceSquared(newPosition, body.SimPosition); + + if (distSqr > 0.0001f || Math.Abs(newRotation - body.Rotation) > 0.01f) { body.TargetPosition = newPosition; body.TargetRotation = newRotation; body.MoveToTargetPosition(lerp: true); + if (distSqr > 10.0f * 10.0f) + { + //very large change in position, we need to recheck which submarine the item is in + Submarine = null; + UpdateTransform(); + } } Vector2 displayPos = ConvertUnits.ToDisplayUnits(body.SimPosition); @@ -1594,8 +1656,12 @@ namespace Barotrauma return; } + var posInfo = body.ClientRead(msg, sendingTime, parentDebugName: Name); msg.ReadPadBits(); + + if (GetComponent() is { IsStuckToTarget: true }) { return; } + if (posInfo != null) { int index = 0; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemInventory.cs index 35e6f8385..97cff0b73 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemInventory.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System.Linq; @@ -83,5 +84,11 @@ namespace Barotrauma base.Draw(spriteBatch, subInventory); } } + + public void ClientEventWrite(IWriteMessage msg, Item.InventoryStateEventData extraData) + { + SharedWrite(msg, extraData.SlotRange); + syncItemsDelay = 1.0f; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs index ad94444ce..b422225e8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs @@ -1,5 +1,7 @@ using Barotrauma.Lights; +using Barotrauma.Particles; using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; namespace Barotrauma @@ -28,36 +30,60 @@ namespace Barotrauma underwaterExplosion.StartDelay = 0.0f; } } - - for (int i = 0; i < Attack.Range * 0.1f; i++) + if (!underwater && (flames || smoke)) { - if (!underwater) + for (int i = 0; i < Attack.Range * 0.025f; i++) { - float particleSpeed = Rand.Range(0.0f, 1.0f); - particleSpeed = particleSpeed * particleSpeed * Attack.Range; + float distFactor = 0.0f; + + if (i > 0 && Attack.Range > 100.0f) + { + distFactor = Rand.Range(0.0f, 1.0f); + //sqrt to make larger values more common (= more particles spawn further away from the origin) + distFactor = MathF.Sqrt(distFactor); + } + float sizeFactor = MathHelper.Clamp(Attack.Range / 1000.0f, 0.0f, 1.0f); + float minScale = MathHelper.Lerp(0.2f, 1.0f, sizeFactor); + float maxScale = MathUtils.InverseLerp(2.0f, 3.0f, sizeFactor); + //larger particles closer to the origin + float particleScale = MathHelper.Clamp(1.0f - distFactor, minScale, maxScale); + + var particlePrefab = ParticleManager.FindPrefab("explosionfire"); + Vector2 pos = worldPosition; + if (i > 0) + { + pos = ClampParticlePos(worldPosition + Rand.Vector(Attack.Range * distFactor * 0.3f), hull, particlePrefab); + } if (flames) { - float particleScale = MathHelper.Clamp(Attack.Range * 0.0025f, 0.5f, 2.0f); - var flameParticle = GameMain.ParticleManager.CreateParticle("explosionfire", - ClampParticlePos(worldPosition + Rand.Vector((float)System.Math.Sqrt(Rand.Range(0.0f, Attack.Range))), hull), - Rand.Vector(Rand.Range(0.0f, particleSpeed)), 0.0f, hull); - if (flameParticle != null) flameParticle.Size *= particleScale; + var flameParticle = GameMain.ParticleManager.CreateParticle(particlePrefab, + pos, + velocity: Vector2.Zero, hullGuess: hull); + if (flameParticle != null) + { + //brief delay to particles futher from origin + flameParticle.StartDelay = distFactor * sizeFactor; + flameParticle.Size *= particleScale; + } } if (smoke) { - GameMain.ParticleManager.CreateParticle(Rand.Range(0.0f, 1.0f) < 0.5f ? "explosionsmoke" : "smoke", - ClampParticlePos(worldPosition + Rand.Vector((float)System.Math.Sqrt(Rand.Range(0.0f, Attack.Range))), hull), - Rand.Vector(Rand.Range(0.0f, particleSpeed)), 0.0f, hull); + GameMain.ParticleManager.CreateParticle( + ParticleManager.FindPrefab(Rand.Range(0.0f, 1.0f) < 0.5f ? "explosionsmoke" : "smoke"), + pos, velocity: Vector2.Zero, hullGuess: hull); } } - else if (underwaterBubble) + } + + for (int i = 0; i < Attack.Range * 0.1f; i++) + { + if (underwater && underwaterBubble) { Vector2 bubblePos = Rand.Vector(Rand.Range(0.0f, Attack.Range * 0.5f)); GameMain.ParticleManager.CreateParticle("risingbubbles", worldPosition + bubblePos, - Vector2.Zero, 0.0f, hull); - + velocity: Vector2.Zero, hullGuess: hull); if (i < Attack.Range * 0.02f) { var underwaterExplosion = GameMain.ParticleManager.CreateParticle("underwaterexplosion", worldPosition + bubblePos, @@ -66,33 +92,46 @@ namespace Barotrauma { underwaterExplosion.Size *= MathHelper.Clamp(Attack.Range / 300.0f, 0.5f, 2.0f) * Rand.Range(0.8f, 1.2f); } - } - + } } - if (sparks) { GameMain.ParticleManager.CreateParticle("spark", worldPosition, - Rand.Vector(Rand.Range(1200.0f, 2400.0f)), 0.0f, hull); + Rand.Vector(Rand.Range(800.0f, 1500.0f)), 0.0f, hull); + } + if (debris) + { + GameMain.ParticleManager.CreateParticle("explosiondebris", worldPosition, + Rand.Vector(Rand.Range(800.0f, 2000.0f)), 0.0f, hull); } } if (flash) { - float displayRange = flashRange ?? Attack.Range; + float displayRange = flashRange ?? (Attack.Range * 2); if (displayRange < 0.1f) { return; } var light = new LightSource(worldPosition, displayRange, flashColor, null); CoroutineManager.StartCoroutine(DimLight(light)); } } - private Vector2 ClampParticlePos(Vector2 particlePos, Hull hull) + private static Vector2 ClampParticlePos(Vector2 particlePos, Hull hull, ParticlePrefab particlePrefab) { - if (hull == null) return particlePos; + float minX = hull.WorldRect.X; + float maxX = hull.WorldRect.Right; + float minY = hull.WorldRect.Y - hull.WorldRect.Height; + float maxY = hull.WorldRect.Y; + if (particlePrefab != null) + { + minX = Math.Min(minX + particlePrefab.CollisionRadius, hull.WorldRect.Center.X); + maxX = Math.Max(maxX - particlePrefab.CollisionRadius, hull.WorldRect.Center.X); + minY = Math.Min(minY + particlePrefab.CollisionRadius, hull.WorldRect.Y - hull.WorldRect.Height / 2); + maxY = Math.Max(maxY - particlePrefab.CollisionRadius, hull.WorldRect.Y - hull.WorldRect.Height / 2); + } return new Vector2( - MathHelper.Clamp(particlePos.X, hull.WorldRect.X, hull.WorldRect.Right), - MathHelper.Clamp(particlePos.Y, hull.WorldRect.Y - hull.WorldRect.Height, hull.WorldRect.Y)); + MathHelper.Clamp(particlePos.X, minX, maxX), + MathHelper.Clamp(particlePos.Y, minY, maxY)); } private IEnumerable DimLight(LightSource light) @@ -100,9 +139,11 @@ namespace Barotrauma float currBrightness = 1.0f; while (light.Color.A > 0.0f && flashDuration > 0.0f && currBrightness > 0.0f) { - light.Color = new Color(light.Color.R, light.Color.G, light.Color.B, (byte)(currBrightness * 255)); - currBrightness -= 1.0f / flashDuration * CoroutineManager.DeltaTime; - + if (!CoroutineManager.Paused) + { + light.Color = new Color(light.Color.R, light.Color.G, light.Color.B, (byte)(currBrightness * 255)); + currBrightness -= 1.0f / flashDuration * CoroutineManager.DeltaTime; + } yield return CoroutineStatus.Running; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs index d23d0b542..4ec014826 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -199,7 +199,7 @@ namespace Barotrauma Sprite activeSprite = obj.Sprite; activeSprite?.Draw( spriteBatch, - new Vector2(obj.Position.X, -obj.Position.Y) - camDiff * obj.Position.Z / 10000.0f, + new Vector2(obj.Position.X, -obj.Position.Y) - camDiff * obj.Position.Z * ParallaxStrength, Color.Lerp(obj.Prefab.SpriteColor, obj.Prefab.SpriteColor.Multiply(Level.Loaded.BackgroundTextureColor), obj.Position.Z / 3000.0f), activeSprite.Origin, obj.CurrentRotation, @@ -218,7 +218,7 @@ namespace Barotrauma obj.ActivePrefab.DeformableSprite.Reset(); } obj.ActivePrefab.DeformableSprite?.Draw(cam, - new Vector3(new Vector2(obj.Position.X, obj.Position.Y) - camDiff * obj.Position.Z / 10000.0f, z * 10.0f), + new Vector3(new Vector2(obj.Position.X, obj.Position.Y) - camDiff * obj.Position.Z * ParallaxStrength, z * 10.0f), obj.ActivePrefab.DeformableSprite.Origin, obj.CurrentRotation, obj.CurrentScale, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs index 7faf33912..82ad83280 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs @@ -561,7 +561,7 @@ namespace Barotrauma.Lights if (IsSegmentFacing(losVertices[0].Pos, losVertices[1].Pos, lightSourcePos)) { - Array.Reverse(ShadowVertices); + Array.Reverse(ShadowVertices, 0, ShadowVertexCount); } CalculateLosPenumbraVertices(lightSourcePos); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index e846bfa4f..c04db86d6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -455,6 +455,10 @@ namespace Barotrauma.Lights if (drawDeformSprites == (limb.DeformSprite == null)) { continue; } limb.Draw(spriteBatch, cam, lightColor); } + foreach (var heldItem in character.HeldItems) + { + heldItem.Draw(spriteBatch, editing: false, overrideColor: Color.Black); + } } } @@ -480,9 +484,9 @@ namespace Barotrauma.Lights if (ConnectionPanel.ShouldDebugDrawWiring) { - foreach (MapEntity e in (Submarine.VisibleEntities ?? MapEntity.mapEntityList)) + foreach (MapEntity e in (Submarine.VisibleEntities ?? MapEntity.MapEntityList)) { - if (e is Item item && item.GetComponent() is Wire wire) + if (e is Item item && !item.HiddenInGame && item.GetComponent() is Wire wire) { wire.DebugDraw(spriteBatch, alpha: 0.4f); } @@ -719,6 +723,7 @@ namespace Barotrauma.Lights { foreach (var ch in convexHulls) { + if (!ch.Enabled) { continue; } Vector2 currentViewPos = pos; Vector2 defaultViewPos = ViewTarget.DrawPosition; if (ch.ParentEntity?.Submarine != null) @@ -742,10 +747,13 @@ namespace Barotrauma.Lights { if (!convexHull.Enabled || !convexHull.Intersects(camView)) { continue; } - Vector2 relativeLightPos = pos; - if (convexHull.ParentEntity?.Submarine != null) { relativeLightPos -= convexHull.ParentEntity.Submarine.Position; } + Vector2 relativeViewPos = pos; + if (convexHull.ParentEntity?.Submarine != null) + { + relativeViewPos -= convexHull.ParentEntity.Submarine.DrawPosition; + } - convexHull.CalculateLosVertices(relativeLightPos); + convexHull.CalculateLosVertices(relativeViewPos); for (int i = 0; i < convexHull.ShadowVertexCount; i++) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index 9574a3d6a..7606b40c6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -53,6 +53,10 @@ namespace Barotrauma.Lights [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = -360, MaxValueFloat = 360, ValueStep = 1, DecimalCount = 0)] public float Rotation { get; set; } + [Serialize(false, IsPropertySaveable.Yes, "Directional lights only shine in \"one direction\", meaning no shadows are cast behind them."+ + " Note that this does not affect how the light texture is drawn: if you want something like a conical spotlight, you should use an appropriate texture for that.")] + public bool Directional { get; set; } + public Vector2 GetOffset() => Vector2.Transform(Offset, Matrix.CreateRotationZ(MathHelper.ToRadians(Rotation))); private float flicker; @@ -206,6 +210,8 @@ namespace Barotrauma.Lights private readonly List convexHullsInRange; + private readonly HashSet visibleConvexHulls = new HashSet(); + public Texture2D texture; public SpriteEffects LightSpriteEffect; @@ -312,6 +318,8 @@ namespace Barotrauma.Lights if (Math.Abs(value - rotation) < 0.001f) { return; } rotation = value; + dir = new Vector2(MathF.Cos(rotation), -MathF.Sin(rotation)); + if (Math.Abs(rotation - prevCalculatedRotation) < RotationRecalculationThreshold && vertices != null) { return; @@ -322,6 +330,8 @@ namespace Barotrauma.Lights } } + private Vector2 dir = Vector2.UnitX; + private Vector2 _spriteScale = Vector2.One; public Vector2 SpriteScale @@ -445,7 +455,7 @@ namespace Barotrauma.Lights public bool Enabled = true; private readonly ISerializableEntity conditionalTarget; - private readonly PropertyConditional.Comparison comparison; + private readonly PropertyConditional.LogicalOperatorType logicalOperator; private readonly List conditionals = new List(); public LightSource(ContentXElement element, ISerializableEntity conditionalTarget = null) @@ -453,11 +463,7 @@ namespace Barotrauma.Lights { lightSourceParams = new LightSourceParams(element); CastShadows = element.GetAttributeBool("castshadows", true); - string comparison = element.GetAttributeString("comparison", null); - if (comparison != null) - { - Enum.TryParse(comparison, ignoreCase: true, out this.comparison); - } + logicalOperator = element.GetAttributeEnum("comparison", logicalOperator); if (lightSourceParams.DeformableLightSpriteElement != null) { @@ -470,13 +476,7 @@ namespace Barotrauma.Lights switch (subElement.Name.ToString().ToLowerInvariant()) { case "conditional": - foreach (XAttribute attribute in subElement.Attributes()) - { - if (PropertyConditional.IsValid(attribute)) - { - conditionals.Add(new PropertyConditional(attribute)); - } - } + conditionals.AddRange(PropertyConditional.FromXElement(subElement)); break; } } @@ -539,11 +539,32 @@ namespace Barotrauma.Lights var fullChList = ConvexHull.HullLists.FirstOrDefault(chList => chList.Submarine == sub); if (fullChList == null) { return; } + //used to check whether the lightsource hits the target hull if the light is directional + Vector2 ray = new Vector2(dir.X, -dir.Y) * TextureRange; + Vector2 normal = new Vector2(-ray.Y, ray.X); + chList.List.Clear(); foreach (var convexHull in fullChList.List) { if (!convexHull.Enabled) { continue; } if (!MathUtils.CircleIntersectsRectangle(lightPos, TextureRange, convexHull.BoundingBox)) { continue; } + if (lightSourceParams.Directional) + { + Rectangle bounds = convexHull.BoundingBox; + //invert because GetLineRectangleIntersection uses the messed up rects that start from top-left + bounds.Y -= bounds.Height; + + //the ray can't hit if + // center is in the opposite direction from the ray (cheapest check first) + if (Vector2.Dot(ray, convexHull.BoundingBox.Center.ToVector2() - lightPos) <= 0 && + /*ray doesn't hit the convex hull*/ + !MathUtils.GetLineRectangleIntersection(lightPos, lightPos + ray, bounds, out _) && + /*normal vectors of the ray don't hit the convex hull */ + !MathUtils.GetLineRectangleIntersection(lightPos + normal, lightPos - normal, bounds, out _)) + { + continue; + } + } chList.List.Add(convexHull); } chList.IsHidden.RemoveWhere(ch => !chList.List.Contains(ch)); @@ -717,6 +738,8 @@ namespace Barotrauma.Lights public void RayCastTask(Vector2 drawPos, float rotation) { + visibleConvexHulls.Clear(); + Vector2 drawOffset = Vector2.Zero; float boundsExtended = TextureRange; if (OverrideLightTexture != null) @@ -854,13 +877,15 @@ namespace Barotrauma.Lights } } + const float MinPointDistance = 6; + //remove points that are very close to each other for (int i = 0; i < points.Count; i++) { for (int j = Math.Min(i + 4, points.Count - 1); j > i; j--) { - if (Math.Abs(points[i].WorldPos.X - points[j].WorldPos.X) < 6 && - Math.Abs(points[i].WorldPos.Y - points[j].WorldPos.Y) < 6) + if (Math.Abs(points[i].WorldPos.X - points[j].WorldPos.X) < MinPointDistance && + Math.Abs(points[i].WorldPos.Y - points[j].WorldPos.Y) < MinPointDistance) { points.RemoveAt(j); } @@ -890,7 +915,7 @@ namespace Barotrauma.Lights foreach (SegmentPoint p in points) { Vector2 dir = Vector2.Normalize(p.WorldPos - drawPos); - Vector2 dirNormal = new Vector2(-dir.Y, dir.X) * 3; + Vector2 dirNormal = new Vector2(-dir.Y, dir.X) * MinPointDistance; //do two slightly offset raycasts to hit the segment itself and whatever's behind it var intersection1 = RayCast(drawPos, drawPos + dir * boundsExtended * 2 - dirNormal, visibleSegments); @@ -904,17 +929,12 @@ namespace Barotrauma.Lights bool isPoint1 = MathUtils.LineToPointDistanceSquared(seg1.Start.WorldPos, seg1.End.WorldPos, p.WorldPos) < 25.0f; bool isPoint2 = MathUtils.LineToPointDistanceSquared(seg2.Start.WorldPos, seg2.End.WorldPos, p.WorldPos) < 25.0f; + bool markAsVisible = false; if (isPoint1 && isPoint2) { //hit at the current segmentpoint -> place the segmentpoint into the list verts.Add(p.WorldPos); - - foreach (ConvexHullList hullList in convexHullsInRange) - { - hullList.IsHidden.Remove(p.ConvexHull); - hullList.IsHidden.Remove(seg1.ConvexHull); - hullList.IsHidden.Remove(seg2.ConvexHull); - } + markAsVisible = true; } else if (intersection1.index != intersection2.index) { @@ -922,13 +942,13 @@ namespace Barotrauma.Lights //we definitely want to generate new geometry here verts.Add(isPoint1 ? p.WorldPos : intersection1.pos); verts.Add(isPoint2 ? p.WorldPos : intersection2.pos); - - foreach (ConvexHullList hullList in convexHullsInRange) - { - hullList.IsHidden.Remove(p.ConvexHull); - hullList.IsHidden.Remove(seg1.ConvexHull); - hullList.IsHidden.Remove(seg2.ConvexHull); - } + markAsVisible = true; + } + if (markAsVisible) + { + visibleConvexHulls.Add(p.ConvexHull); + visibleConvexHulls.Add(seg1.ConvexHull); + visibleConvexHulls.Add(seg2.ConvexHull); } //if neither of the conditions above are met, we just assume //that the raycasts both resulted on the same segment @@ -1029,11 +1049,9 @@ namespace Barotrauma.Lights Vector2 drawPos = calculatedDrawPos; - float cosAngle = (float)Math.Cos(Rotation); - float sinAngle = -(float)Math.Sin(Rotation); - Vector2 uvOffset = Vector2.Zero; Vector2 overrideTextureDims = Vector2.One; + Vector2 dir = this.dir; if (OverrideLightTexture != null) { overrideTextureDims = new Vector2(OverrideLightTexture.SourceRect.Width, OverrideLightTexture.SourceRect.Height); @@ -1042,8 +1060,7 @@ namespace Barotrauma.Lights if (LightSpriteEffect == SpriteEffects.FlipHorizontally) { origin.X = OverrideLightTexture.SourceRect.Width - origin.X; - cosAngle = -cosAngle; - sinAngle = -sinAngle; + dir = -dir; } if (LightSpriteEffect == SpriteEffects.FlipVertically) { origin.Y = OverrideLightTexture.SourceRect.Height - origin.Y; } uvOffset = (origin / overrideTextureDims) - new Vector2(0.5f, 0.5f); @@ -1116,8 +1133,8 @@ namespace Barotrauma.Lights //calculate texture coordinates based on the light's rotation Vector2 originDiff = diff; - diff.X = originDiff.X * cosAngle - originDiff.Y * sinAngle; - diff.Y = originDiff.X * sinAngle + originDiff.Y * cosAngle; + diff.X = originDiff.X * dir.X - originDiff.Y * dir.Y; + diff.Y = originDiff.X * dir.Y + originDiff.Y * dir.X; diff *= (overrideTextureDims / OverrideLightTexture.size);// / (1.0f - Math.Max(Math.Abs(uvOffset.X), Math.Abs(uvOffset.Y))); diff += uvOffset; } @@ -1222,9 +1239,6 @@ namespace Barotrauma.Lights } drawPos.Y = -drawPos.Y; - float cosAngle = (float)Math.Cos(Rotation); - float sinAngle = -(float)Math.Sin(Rotation); - float bounds = TextureRange; if (OverrideLightTexture != null) @@ -1236,8 +1250,8 @@ namespace Barotrauma.Lights origin /= Math.Max(overrideTextureDims.X, overrideTextureDims.Y); origin *= TextureRange; - drawPos.X += origin.X * sinAngle + origin.Y * cosAngle; - drawPos.Y += origin.X * cosAngle + origin.Y * sinAngle; + drawPos.X += origin.X * dir.Y + origin.Y * dir.X; + drawPos.Y += origin.X * dir.X + origin.Y * dir.Y; } //add a square-shaped boundary to make sure we've got something to construct the triangles from @@ -1343,7 +1357,7 @@ namespace Barotrauma.Lights { if (conditionals.None()) { return; } if (conditionalTarget == null) { return; } - if (comparison == PropertyConditional.Comparison.And) + if (logicalOperator == PropertyConditional.LogicalOperatorType.And) { Enabled = conditionals.All(c => c.Matches(conditionalTarget)); } @@ -1396,6 +1410,14 @@ namespace Barotrauma.Lights return; } + foreach (var visibleConvexHull in visibleConvexHulls) + { + foreach (var convexHullList in convexHullsInRange) + { + convexHullList.IsHidden.Remove(visibleConvexHull); + } + } + CalculateLightVertices(verts); LastRecalculationTime = (float)Timing.TotalTime; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs index 2170143c5..222d4cae3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs @@ -22,7 +22,7 @@ namespace Barotrauma foreach (MapEntity e in linkedTo) { - bool isLinkAllowed = e is Item item && item.HasTag("dock"); + bool isLinkAllowed = e is Item item && item.HasTag(Tags.DockingPort); GUI.DrawLine(spriteBatch, new Vector2(WorldPosition.X, -WorldPosition.Y), diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index 151e15a14..e487bb33d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -25,7 +25,7 @@ namespace Barotrauma public event Action Resized; - private static bool resizing; + public static bool Resizing { get; private set; } private int resizeDirX, resizeDirY; private Rectangle? prevRect; @@ -122,11 +122,11 @@ namespace Barotrauma /// public static void UpdateSelecting(Camera cam) { - if (resizing) + if (Resizing) { if (!SelectedAny) { - resizing = false; + Resizing = false; } return; } @@ -243,7 +243,7 @@ namespace Barotrauma } else { - foreach (MapEntity e in mapEntityList) + foreach (MapEntity e in MapEntityList) { if (!e.SelectableInEditor) { continue; } if (e.IsMouseOn(position)) @@ -352,7 +352,7 @@ namespace Barotrauma selectionSize.X = position.X - selectionPos.X; selectionSize.Y = selectionPos.Y - position.Y; - foreach (MapEntity entity in mapEntityList) + foreach (MapEntity entity in MapEntityList) { entity.IsIncludedInSelection = false; } @@ -454,7 +454,7 @@ namespace Barotrauma selectionPos = Vector2.Zero; selectionSize = Vector2.Zero; - foreach (MapEntity entity in mapEntityList) + foreach (MapEntity entity in MapEntityList) { entity.IsIncludedInSelection = false; } @@ -525,7 +525,7 @@ namespace Barotrauma if (!isShiftDown) { return null; } - foreach (MapEntity e in mapEntityList) + foreach (MapEntity e in MapEntityList) { if (!e.SelectableInEditor || e is not Item potentialContainer) { continue; } @@ -730,7 +730,7 @@ namespace Barotrauma static partial void UpdateAllProjSpecific(float deltaTime) { - var entitiesToRender = Submarine.VisibleEntities ?? mapEntityList; + var entitiesToRender = Submarine.VisibleEntities ?? MapEntityList; foreach (MapEntity me in entitiesToRender) { if (me is Item item) @@ -832,35 +832,39 @@ namespace Barotrauma } if (selectionPos != Vector2.Zero) { - var (sizeX, sizeY) = selectionSize; - var (posX, posY) = selectionPos; - - posY = -posY; - - Vector2[] corners = - { - new Vector2(posX, posY), - new Vector2(posX + sizeX, posY), - new Vector2(posX + sizeX, posY + sizeY), - new Vector2(posX, posY + sizeY) - }; - - Color selectionColor = GUIStyle.Blue; - float thickness = Math.Max(2f, 2f / Screen.Selected.Cam.Zoom); - - GUI.DrawFilledRectangle(spriteBatch, corners[0], selectionSize, selectionColor * 0.1f); - - Vector2 offset = new Vector2(0f, thickness / 2f); - - if (sizeY < 0) { offset.Y = -offset.Y; } - - spriteBatch.DrawLine(corners[0], corners[1], selectionColor, thickness); - spriteBatch.DrawLine(corners[1] - offset, corners[2] + offset, selectionColor, thickness); - spriteBatch.DrawLine(corners[2], corners[3], selectionColor, thickness); - spriteBatch.DrawLine(corners[3] + offset, corners[0] - offset, selectionColor, thickness); + DrawSelectionRect(spriteBatch, selectionPos, selectionSize, GUIStyle.Blue); } } + public static void DrawSelectionRect(SpriteBatch spriteBatch, Vector2 pos, Vector2 size, Color color) + { + var (sizeX, sizeY) = size; + var (posX, posY) = pos; + + posY = -posY; + + Vector2[] corners = + { + new Vector2(posX, posY), + new Vector2(posX + sizeX, posY), + new Vector2(posX + sizeX, posY + sizeY), + new Vector2(posX, posY + sizeY) + }; + + float thickness = Math.Max(2f, 2f / Screen.Selected.Cam.Zoom); + + GUI.DrawFilledRectangle(spriteBatch, corners[0], size, color * 0.1f); + + Vector2 offset = new Vector2(0f, thickness / 2f); + + if (sizeY < 0) { offset.Y = -offset.Y; } + + spriteBatch.DrawLine(corners[0], corners[1], color, thickness); + spriteBatch.DrawLine(corners[1] - offset, corners[2] + offset, color, thickness); + spriteBatch.DrawLine(corners[2], corners[3], color, thickness); + spriteBatch.DrawLine(corners[3] + offset, corners[0] - offset, color, thickness); + } + public static List FilteredSelectedList { get; private set; } = new List(); public static void UpdateEditor(Camera cam, float deltaTime) @@ -995,10 +999,10 @@ namespace Barotrauma { if (CopiedList.Count == 0) { return; } - List prevEntities = new List(mapEntityList); + List prevEntities = new List(MapEntityList); Clone(CopiedList); - var clones = mapEntityList.Except(prevEntities).ToList(); + var clones = MapEntityList.Except(prevEntities).ToList(); var nonWireClones = clones.Where(c => !(c is Item item) || item.GetComponent() == null); if (!nonWireClones.Any()) { nonWireClones = clones; } @@ -1027,12 +1031,12 @@ namespace Barotrauma /// public static List CopyEntities(List entities) { - List prevEntities = new List(mapEntityList); + List prevEntities = new List(MapEntityList); CopiedList = Clone(entities); //find all new entities created during cloning - var newEntities = mapEntityList.Except(prevEntities).ToList(); + var newEntities = MapEntityList.Except(prevEntities).ToList(); //do a "shallow remove" (removes the entities from the game without removing links between them) // -> items will stay in their containers @@ -1089,37 +1093,36 @@ namespace Barotrauma public virtual void DrawEditing(SpriteBatch spriteBatch, Camera cam) { } + float ResizeHandleSize => 10 * GUI.Scale; + float ResizeHandleHighlightDistance => 8 * GUI.Scale; + private void UpdateResizing(Camera cam) { IsHighlighted = true; int startX = ResizeHorizontal ? -1 : 0; - int StartY = ResizeVertical ? -1 : 0; + int startY = ResizeVertical ? -1 : 0; for (int x = startX; x < 2; x += 2) { - for (int y = StartY; y < 2; y += 2) + for (int y = startY; y < 2; y += 2) { - Vector2 handlePos = cam.WorldToScreen(Position + new Vector2(x * (rect.Width * 0.5f + 5), y * (rect.Height * 0.5f + 5))); - - bool highlighted = Vector2.Distance(PlayerInput.MousePosition, handlePos) < 5.0f; + Vector2 handlePos = cam.WorldToScreen(Position + new Vector2(x * (rect.Width * 0.5f), y * (rect.Height * 0.5f))); + bool highlighted = Vector2.DistanceSquared(PlayerInput.MousePosition, handlePos) < ResizeHandleHighlightDistance * ResizeHandleHighlightDistance; if (highlighted && PlayerInput.PrimaryMouseButtonDown()) { selectionPos = Vector2.Zero; resizeDirX = x; resizeDirY = y; - resizing = true; + Resizing = true; startMovingPos = Vector2.Zero; - foreach (var mapEntity in mapEntityList) - { - if (mapEntity != this) { mapEntity.isHighlighted = false; } - } + ClearHighlightedEntities(); } } } - if (resizing) + if (Resizing) { if (prevRect == null) { @@ -1129,7 +1132,7 @@ namespace Barotrauma Vector2 placePosition = new Vector2(rect.X, rect.Y); Vector2 placeSize = new Vector2(rect.Width, rect.Height); - Vector2 mousePos = Submarine.MouseToWorldGrid(cam, Submarine.MainSub); + Vector2 mousePos = Submarine.MouseToWorldGrid(cam, Submarine.MainSub, round: true); if (PlayerInput.IsShiftDown()) { @@ -1145,8 +1148,8 @@ namespace Barotrauma { mousePos.X = Math.Min(mousePos.X, rect.Right - Submarine.GridSize.X); - placeSize.X = (placePosition.X + placeSize.X) - mousePos.X; - placePosition.X = mousePos.X; + placeSize.X = MathF.Round((placePosition.X + placeSize.X) - mousePos.X); + placePosition.X = MathF.Round(mousePos.X); } if (resizeDirY < 0) { @@ -1168,7 +1171,7 @@ namespace Barotrauma if (!PlayerInput.PrimaryMouseButtonHeld()) { - resizing = false; + Resizing = false; Resized?.Invoke(rect); if (prevRect != null) { @@ -1201,13 +1204,16 @@ namespace Barotrauma { for (int y = StartY; y < 2; y += 2) { - Vector2 handlePos = cam.WorldToScreen(Position + new Vector2(x * (rect.Width * 0.5f + 5), y * (rect.Height * 0.5f + 5))); - - bool highlighted = Vector2.Distance(PlayerInput.MousePosition, handlePos) < 5.0f; + Vector2 handlePos = cam.WorldToScreen(Position + new Vector2(x * (rect.Width * 0.5f), y * (rect.Height * 0.5f))); + bool highlighted = Vector2.DistanceSquared(PlayerInput.MousePosition, handlePos) < ResizeHandleHighlightDistance * ResizeHandleHighlightDistance; + if (highlighted && !PlayerInput.PrimaryMouseButtonHeld()) + { + GUI.MouseCursor = CursorState.Hand; + } GUI.DrawRectangle(spriteBatch, - handlePos - new Vector2(3.0f, 3.0f), - new Vector2(6.0f, 6.0f), + handlePos - new Vector2(ResizeHandleSize / 2), + new Vector2(ResizeHandleSize), Color.White * (highlighted ? 1.0f : 0.6f), true, 0, (int)Math.Max(1.5f / GameScreen.Selected.Cam.Zoom, 1.0f)); @@ -1224,7 +1230,7 @@ namespace Barotrauma Rectangle selectionRect = Submarine.AbsRect(pos, size); - foreach (MapEntity entity in mapEntityList) + foreach (MapEntity entity in MapEntityList) { if (!entity.SelectableInEditor) { continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index 9f1b8c657..3c28cbe17 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -56,15 +56,43 @@ namespace Barotrauma if (!CastShadow) { return; } convexHulls ??= new List(); - var h = new ConvexHull( - new Rectangle((position - size / 2).ToPoint(), size.ToPoint()), - IsHorizontal, - this); - if (Math.Abs(rotation) > 0.001f) + + //if the convex hull is longer than this, we need to split it to multiple parts + //very large convex hulls don't play nicely with the lighting or LOS, because the shadow cast + //by the convex hull would need to be extruded very far to cover the whole screen + const float MaxConvexHullLength = 1024.0f; + float length = IsHorizontal ? size.X : size.Y; + int convexHullCount = (int)Math.Max(1, Math.Ceiling(length / MaxConvexHullLength)); + + Vector2 sectionSize = size; + if (convexHullCount > 1) { - h.Rotate(position, rotation); + if (IsHorizontal) + { + sectionSize.X = length / convexHullCount; + } + else + { + sectionSize.Y = length / convexHullCount; + } + } + + for (int i = 0; i < convexHullCount; i++) + { + Vector2 offset = + (IsHorizontal ? Vector2.UnitX : Vector2.UnitY) * + (i * length / convexHullCount); + + var h = new ConvexHull( + new Rectangle((position - size / 2 + offset).ToPoint(), sectionSize.ToPoint()), + IsHorizontal, + this); + if (Math.Abs(rotation) > 0.001f) + { + h.Rotate(position, rotation); + } + convexHulls.Add(h); } - convexHulls.Add(h); } public override void UpdateEditing(Camera cam, float deltaTime) @@ -498,7 +526,7 @@ namespace Barotrauma private bool ConditionalMatches(PropertyConditional conditional) { - if (!string.IsNullOrEmpty(conditional.TargetItemComponentName)) + if (!string.IsNullOrEmpty(conditional.TargetItemComponent)) { return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index 795401f2f..6d330e43e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using Barotrauma.Networking; using FarseerPhysics; using Microsoft.Xna.Framework; @@ -70,15 +71,16 @@ namespace Barotrauma if (visibleEntities == null) { - visibleEntities = new List(MapEntity.mapEntityList.Count); + visibleEntities = new List(MapEntity.MapEntityList.Count); } else { visibleEntities.Clear(); } - foreach (MapEntity entity in MapEntity.mapEntityList) + foreach (MapEntity entity in MapEntity.MapEntityList) { + if (entity == null || entity.Removed) { continue; } if (entity.Submarine != null) { if (!visibleSubs.Contains(entity.Submarine)) { continue; } @@ -102,7 +104,7 @@ namespace Barotrauma public static void Draw(SpriteBatch spriteBatch, bool editing = false) { - var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.mapEntityList; + var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList; foreach (MapEntity e in entitiesToRender) { @@ -112,7 +114,7 @@ namespace Barotrauma public static void DrawFront(SpriteBatch spriteBatch, bool editing = false, Predicate predicate = null) { - var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.mapEntityList; + var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList; foreach (MapEntity e in entitiesToRender) { @@ -161,7 +163,7 @@ namespace Barotrauma private static readonly List depthSortedDamageable = new List(); public static void DrawDamageable(SpriteBatch spriteBatch, Effect damageEffect, bool editing = false, Predicate predicate = null) { - var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.mapEntityList; + var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList; depthSortedDamageable.Clear(); @@ -200,7 +202,7 @@ namespace Barotrauma public static void DrawPaintedColors(SpriteBatch spriteBatch, bool editing = false, Predicate predicate = null) { - var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.mapEntityList; + var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList; foreach (MapEntity e in entitiesToRender) { @@ -220,7 +222,7 @@ namespace Barotrauma public static void DrawBack(SpriteBatch spriteBatch, bool editing = false, Predicate predicate = null) { - var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.mapEntityList; + var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList; foreach (MapEntity e in entitiesToRender) { @@ -501,7 +503,7 @@ namespace Barotrauma { foreach (Item item in Item.ItemList) { - if (item.GetComponent() == null) { continue; } + if (item.GetComponent() == null) { continue; } if (!item.linkedTo.Any()) { if (!IsWarningSuppressed(SubEditorScreen.WarningType.DisconnectedVents)) @@ -529,7 +531,7 @@ namespace Barotrauma warnings.Add(SubEditorScreen.WarningType.NoCargoSpawnpoints); } } - if (!Item.ItemList.Any(it => it.GetComponent() != null && it.HasTag("ballast"))) + if (Item.ItemList.None(it => it.GetComponent() != null && it.HasTag(Tags.Ballast))) { if (!IsWarningSuppressed(SubEditorScreen.WarningType.NoBallastTag)) { @@ -537,6 +539,14 @@ namespace Barotrauma warnings.Add(SubEditorScreen.WarningType.NoBallastTag); } } + if (Item.ItemList.None(it => it.HasTag(Tags.HiddenItemContainer))) + { + if (!IsWarningSuppressed(SubEditorScreen.WarningType.NoHiddenContainers)) + { + errorMsgs.Add(TextManager.Get("NoHiddenContainersWarning").Value); + warnings.Add(SubEditorScreen.WarningType.NoHiddenContainers); + } + } } else if (Info.Type == SubmarineType.OutpostModule) { @@ -581,7 +591,7 @@ namespace Barotrauma } } - if ((MapEntity.mapEntityList.Count - Item.ItemList.Count - Hull.HullList.Count - WayPoint.WayPointList.Count - Gap.GapList.Count) > SubEditorScreen.MaxStructures * entityCountWarningThreshold) + if ((MapEntity.MapEntityList.Count - Item.ItemList.Count - Hull.HullList.Count - WayPoint.WayPointList.Count - Gap.GapList.Count) > SubEditorScreen.MaxStructures * entityCountWarningThreshold) { if (!IsWarningSuppressed(SubEditorScreen.WarningType.StructureCount)) { @@ -641,7 +651,7 @@ namespace Barotrauma } } - foreach (MapEntity e in MapEntity.mapEntityList) + foreach (MapEntity e in MapEntity.MapEntityList) { if (Vector2.Distance(e.Position, HiddenSubPosition) > 20000) { @@ -653,7 +663,7 @@ namespace Barotrauma } } - foreach (MapEntity e in MapEntity.mapEntityList) + foreach (MapEntity e in MapEntity.MapEntityList) { if (Vector2.Distance(e.Position, HiddenSubPosition) > 20000) { @@ -703,12 +713,12 @@ namespace Barotrauma return GameMain.LightManager.Lights.Count(l => l.CastShadows && !l.IsBackground) - disabledItemLightCount; } - public static Vector2 MouseToWorldGrid(Camera cam, Submarine sub) + public static Vector2 MouseToWorldGrid(Camera cam, Submarine sub, bool round = false) { Vector2 position = PlayerInput.MousePosition; position = cam.ScreenToWorld(position); - Vector2 worldGridPos = VectorToWorldGrid(position); + Vector2 worldGridPos = VectorToWorldGrid(position, round); if (sub != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs index 8491dd736..e056cc997 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs @@ -1,5 +1,6 @@ using Barotrauma.Extensions; using Barotrauma.IO; +using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; @@ -9,7 +10,6 @@ using System.Globalization; using System.Linq; using System.Threading.Tasks; using System.Xml.Linq; -using Barotrauma.Items.Components; namespace Barotrauma { @@ -26,22 +26,51 @@ namespace Barotrauma private GUIFrame previewFrame; + private sealed class LoadedHull + { + public UInt16 ID; + public readonly ImmutableList LinkedHulls; + public readonly Rectangle Rect; + public readonly Identifier NameIdentifier; + + public LoadedHull(XElement element) + { + ID = (ushort)element.GetAttributeInt("id", Entity.NullEntityID); + NameIdentifier = element.GetAttributeIdentifier("roomname", ""); + Rect = element.GetAttributeRect("rect", Rectangle.Empty); + Rect.Y = -Rect.Y; + LinkedHulls = element.GetAttributeUshortArray("linked", Array.Empty()).ToImmutableList(); + } + } + private sealed class HullCollection { - public readonly List Rects; + public readonly List Hulls = new List(); + public readonly List Rects = new List(); public readonly LocalizedString Name; - public HullCollection(Identifier identifier) + public HullCollection(LoadedHull hull) { - Rects = new List(); - Name = TextManager.Get(identifier).Fallback(identifier.Value); + Name = TextManager.Get(hull.NameIdentifier).Fallback(hull.NameIdentifier.ToString()); + AddHull(hull); } - public void AddRect(XElement element) + public void AddHull(LoadedHull hull) { - Rectangle rect = element.GetAttributeRect("rect", Rectangle.Empty); - rect.Y = -rect.Y; - Rects.Add(rect); + Hulls.Add(hull); + Rects.Add(hull.Rect); + } + + private bool Contains(UInt16 hullId) + { + return Hulls.Any(h => h.ID == hullId); + } + + public bool IsLinkedTo(HullCollection other) + { + return + Hulls.Any(h => h.LinkedHulls.Any(id => other.Contains(id))) || + other.Hulls.Any(h => h.LinkedHulls.Any(id => Contains(id))); } } @@ -56,7 +85,7 @@ namespace Barotrauma } } - private readonly Dictionary hullCollections; + private readonly List hullCollections = new List(); private readonly List doors; private static SubmarinePreview instance = null; @@ -80,7 +109,7 @@ namespace Barotrauma isDisposed = false; loadTask = null; - hullCollections = new Dictionary(); + hullCollections = new List(); doors = new List(); previewFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null); @@ -253,7 +282,7 @@ namespace Barotrauma } var wireNodes = new List(); - + List loadedHulls = new List(); foreach (var subElement in submarineInfo.SubmarineElement.Elements()) { if (subElement.GetAttributeBool("hiddeningame", false)) { continue; } @@ -275,12 +304,7 @@ namespace Barotrauma Identifier identifier = subElement.GetAttributeIdentifier("roomname", ""); if (!identifier.IsEmpty) { - if (!hullCollections.TryGetValue(identifier, out HullCollection hullCollection)) - { - hullCollection = new HullCollection(identifier); - hullCollections.Add(identifier, hullCollection); - } - hullCollection.AddRect(subElement); + loadedHulls.Add(new LoadedHull(subElement)); } break; } @@ -288,6 +312,34 @@ namespace Barotrauma await Task.Yield(); } + //List tempHullCollections = new List(); + foreach (LoadedHull hull in loadedHulls) + { + hullCollections.Add(new HullCollection(hull)); + } + + bool intersectionFound; + do + { + intersectionFound = false; + for (int i = 0; i < hullCollections.Count; i++) + { + for (int j = i + 1; j < hullCollections.Count; j++) + { + var collection1 = hullCollections[i]; + var collection2 = hullCollections[j]; + if (collection1.IsLinkedTo(collection2)) + { + collection2.Hulls.ForEach(h => collection1.AddHull(h)); + hullCollections.Remove(collection2); + intersectionFound = true; + break; + } + } + if (intersectionFound) { break; } + } + } while (intersectionFound); + bounds = (spriteRecorder.Min, spriteRecorder.Max); wireNodes.ForEach(BakeWireNodes); @@ -682,7 +734,7 @@ namespace Barotrauma spriteBatch.Begin(SpriteSortMode.BackToFront, rasterizerState: GameMain.ScissorTestEnable, transformMatrix: camera.Transform); GameMain.Instance.GraphicsDevice.ScissorRectangle = scissorRectangle; - foreach (var hullCollection in hullCollections.Values) + foreach (var hullCollection in hullCollections) { bool mouseOver = false; if (GUI.MouseOn == null || GUI.MouseOn == component) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs index f86c9cd48..1a9fb2c71 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs @@ -538,7 +538,7 @@ namespace Barotrauma.Networking { string extension = Path.GetExtension(file); if ((!extension.Equals(".sub", StringComparison.OrdinalIgnoreCase) - && !file.Equals("gamesession.xml")) + && !file.Equals(SaveUtil.GameSessionFileName)) || file.CleanUpPathCrossPlatform(correctFilenameCase: false).Contains('/')) { ErrorMessage = $"Found unexpected file in \"{fileTransfer.FileName}\"! ({file})"; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index bf6adf1cf..36740fb62 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -112,9 +112,8 @@ namespace Barotrauma.Networking //has the client been given a character to control this round public bool HasSpawned; - public bool SpawnAsTraitor; public LocalizedString TraitorFirstObjective; - public TraitorMissionPrefab TraitorMission = null; + public TraitorEventPrefab TraitorMission = null; public byte SessionId { get; private set; } @@ -404,7 +403,7 @@ namespace Barotrauma.Networking if (SteamManager.IsInitialized && steamConnection.AccountInfo.AccountId.TryUnwrap(out var accountId) && accountId is SteamId steamId) { serverDisplayName = steamId.ToString(); - string steamUserName = Steamworks.SteamFriends.GetFriendPersonaName(steamId.Value); + string steamUserName = new Steamworks.Friend(steamId.Value).Name; if (!string.IsNullOrEmpty(steamUserName) && steamUserName != "[unknown]") { serverDisplayName = steamUserName; @@ -775,15 +774,15 @@ namespace Barotrauma.Networking } } - byte traitorCount = inc.ReadByte(); - List traitorResults = new List(); - for (int i = 0; i(inc); } roundInitStatus = RoundInitStatus.Interrupted; - CoroutineManager.StartCoroutine(EndGame(endMessage, traitorResults, transitionType), "EndGame"); + CoroutineManager.StartCoroutine(EndGame(endMessage, transitionType, traitorResults), "EndGame"); GUI.SetSavingIndicatorState(save); break; case ServerPacketHeader.CAMPAIGN_SETUP_INFO: @@ -829,18 +828,21 @@ namespace Barotrauma.Networking case ServerPacketHeader.MEDICAL: campaign?.MedicalClinic?.ClientRead(inc); break; + case ServerPacketHeader.CIRCUITBOX: + ReadCircuitBoxMessage(inc); + break; case ServerPacketHeader.MONEY: campaign?.ClientReadMoney(inc); break; case ServerPacketHeader.READY_CHECK: ReadyCheck.ClientRead(inc); break; + case ServerPacketHeader.TRAITOR_MESSAGE: + TraitorManager.ClientRead(inc); + break; case ServerPacketHeader.FILE_TRANSFER: FileReceiver.ReadMessage(inc); break; - case ServerPacketHeader.TRAITOR_MESSAGE: - ReadTraitorMessage(inc); - break; case ServerPacketHeader.MISSION: { int missionIndex = inc.ReadByte(); @@ -985,7 +987,7 @@ namespace Barotrauma.Networking if (disconnectPacket.IsEventSyncError) { GameMain.NetLobbyScreen.Select(); - GameMain.GameSession?.EndRound("", null); + GameMain.GameSession?.EndRound(""); GameStarted = false; myCharacter = null; } @@ -1179,44 +1181,20 @@ namespace Barotrauma.Networking } } - private void ReadTraitorMessage(IReadMessage inc) + private static void ReadCircuitBoxMessage(IReadMessage inc) { - TraitorMessageType messageType = (TraitorMessageType)inc.ReadByte(); - string missionIdentifier = inc.ReadString(); - string messageFmt = inc.ReadString(); - LocalizedString message = TextManager.GetServerMessage(messageFmt); + var header = INetSerializableStruct.Read(inc); - var missionPrefab = TraitorMissionPrefab.Prefabs.Find(t => t.Identifier == missionIdentifier); - Sprite icon = missionPrefab?.Icon; - - switch (messageType) + INetSerializableStruct data = header.Opcode switch { - case TraitorMessageType.Objective: - var isTraitor = !message.IsNullOrEmpty(); - SpawnAsTraitor = isTraitor; - TraitorFirstObjective = message; - TraitorMission = missionPrefab; - if (Character != null) - { - Character.IsTraitor = isTraitor; - Character.TraitorCurrentObjective = message; - } - break; - case TraitorMessageType.Console: - GameMain.Client.AddChatMessage(ChatMessage.Create("", message.Value, ChatMessageType.Console, null)); - DebugConsole.NewMessage(message); - break; - case TraitorMessageType.ServerMessageBox: - var msgBox = new GUIMessageBox("", message, Array.Empty(), type: GUIMessageBox.Type.InGame, icon: icon); - if (msgBox.Icon != null) - { - msgBox.IconColor = missionPrefab.IconColor; - } - break; - case TraitorMessageType.Server: - default: - GameMain.Client.AddChatMessage(message.Value, ChatMessageType.Server); - break; + CircuitBoxOpcode.Cursor => INetSerializableStruct.Read(inc), + CircuitBoxOpcode.Error => INetSerializableStruct.Read(inc), + _ => throw new ArgumentOutOfRangeException(nameof(header.Opcode), header.Opcode, "This data cannot be handled using direct network messages.") + }; + + if (header.FindTarget().TryUnwrap(out CircuitBox box)) + { + box.ClientRead(data); } } @@ -1372,7 +1350,6 @@ namespace Barotrauma.Networking ServerSettings.AllowRewiring = inc.ReadBoolean(); ServerSettings.AllowFriendlyFire = inc.ReadBoolean(); ServerSettings.LockAllDefaultWires = inc.ReadBoolean(); - ServerSettings.AllowRagdollButton = inc.ReadBoolean(); ServerSettings.AllowLinkingWifiToChat = inc.ReadBoolean(); ServerSettings.MaximumMoneyTransferRequest = inc.ReadInt32(); bool usingShuttle = GameMain.NetLobbyScreen.UsingShuttle = inc.ReadBoolean(); @@ -1698,7 +1675,7 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Success; } - public IEnumerable EndGame(string endMessage, List traitorResults = null, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None) + public IEnumerable EndGame(string endMessage, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None, TraitorManager.TraitorResults? traitorResults = null) { //round starting up, wait for it to finish DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 60); @@ -1717,14 +1694,13 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Success; } - GameMain.GameSession?.EndRound(endMessage, traitorResults, transitionType); + GameMain.GameSession?.EndRound(endMessage, transitionType, traitorResults); ServerSettings.ServerDetailsChanged = true; GameStarted = false; Character.Controlled = null; WaitForNextRoundRespawn = null; - SpawnAsTraitor = false; GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; GameMain.LightManager.LosEnabled = false; RespawnManager = null; @@ -1976,7 +1952,9 @@ namespace Barotrauma.Networking bool allowSpectating = inc.ReadBoolean(); - YesNoMaybe traitorsEnabled = (YesNoMaybe)inc.ReadRangedInteger(0, 2); + float traitorProbability = inc.ReadSingle(); + int traitorDangerLevel = inc.ReadRangedInteger(TraitorEventPrefab.MinDangerLevel, TraitorEventPrefab.MaxDangerLevel); + MissionType missionType = (MissionType)inc.ReadRangedInteger(0, (int)MissionType.All); int modeIndex = inc.ReadByte(); @@ -2020,7 +1998,9 @@ namespace Barotrauma.Networking if (!allowSubVoting) { GameMain.NetLobbyScreen.TrySelectSub(selectSubName, selectSubHash, GameMain.NetLobbyScreen.SubList); } GameMain.NetLobbyScreen.TrySelectSub(selectShuttleName, selectShuttleHash, GameMain.NetLobbyScreen.ShuttleList.ListBox); - GameMain.NetLobbyScreen.SetTraitorsEnabled(traitorsEnabled); + GameMain.NetLobbyScreen.SetTraitorProbability(traitorProbability); + GameMain.NetLobbyScreen.SetTraitorDangerLevel(traitorDangerLevel); + GameMain.NetLobbyScreen.SetMissionType(missionType); GameMain.NetLobbyScreen.SelectMode(modeIndex); @@ -2498,9 +2478,9 @@ namespace Barotrauma.Networking break; case FileTransferType.CampaignSave: - 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) + XElement gameSessionDocRoot = SaveUtil.DecompressSaveAndLoadGameSessionDoc(transfer.FilePath)?.Root; + byte campaignID = (byte)MathHelper.Clamp(gameSessionDocRoot.GetAttributeInt("campaignid", 0), 0, 255); + if (GameMain.GameSession?.GameMode is not MultiPlayerCampaign campaign || campaign.CampaignID != campaignID) { string savePath = transfer.FilePath; GameMain.GameSession = new GameSession(null, savePath, GameModePreset.MultiPlayerCampaign, CampaignSettings.Empty); @@ -2512,7 +2492,7 @@ namespace Barotrauma.Networking GameMain.GameSession.SavePath = transfer.FilePath; if (GameMain.GameSession.SubmarineInfo == null || campaign.Map == null) { - string subPath = Path.Combine(SaveUtil.TempPath, gameSessionDoc.Root.GetAttributeString("submarine", "")) + ".sub"; + string subPath = Path.Combine(SaveUtil.TempPath, gameSessionDocRoot.GetAttributeString("submarine", "")) + ".sub"; GameMain.GameSession.SubmarineInfo = new SubmarineInfo(subPath, ""); } @@ -2528,7 +2508,8 @@ namespace Barotrauma.Networking if (Screen.Selected == GameMain.NetLobbyScreen) { - //reselect to refrest the state of the lobby screen (enable spectate button, etc) + //reselect to refresh the state of the lobby screen (enable spectate button, etc) + GameMain.NetLobbyScreen.SaveAppearance(); GameMain.NetLobbyScreen.Select(); } @@ -2606,7 +2587,7 @@ namespace Barotrauma.Networking public void WriteCharacterInfo(IWriteMessage msg, string newName = null) { - msg.WriteBoolean(characterInfo == null); + msg.WriteBoolean(GameMain.NetLobbyScreen.Spectating); msg.WritePadBits(); if (characterInfo == null) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs index 3cff1e7b4..6718bcbf4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs @@ -53,7 +53,7 @@ namespace Barotrauma.Networking protected ConnectionInitialization initializationStep; public bool ContentPackageOrderReceived { get; set; } protected int passwordSalt; - protected Steamworks.AuthTicket? steamAuthTicket; + protected Option steamAuthTicket; private GUIMessageBox? passwordMsgBox; public bool WaitingForPassword @@ -82,7 +82,7 @@ namespace Barotrauma.Networking Initialization = ConnectionInitialization.SteamTicketAndVersion }; - if (steamAuthTicket is { Canceled: true }) + if (steamAuthTicket.TryUnwrap(out var authTicket) && authTicket is { Canceled: true }) { throw new InvalidOperationException("ReadConnectionInitializationStep failed: Steam auth ticket has been cancelled."); } @@ -92,11 +92,7 @@ namespace Barotrauma.Networking Name = GameMain.Client.Name, OwnerKey = ownerKey, SteamId = SteamManager.GetSteamId().Select(id => (AccountId)id), - SteamAuthTicket = steamAuthTicket?.Data switch - { - null => Option.None(), - var ticketData => Option.Some(ticketData) - }, + SteamAuthTicket = steamAuthTicket.Bind(t => t.Data != null ? Option.Some(t.Data) : Option.None), GameVersion = GameMain.Version.ToString(), Language = GameSettings.CurrentConfig.Language.Value }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs index 8e599ee17..1ed7a4e53 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Text; using Lidgren.Network; using Barotrauma.Steam; +using System.Net.Sockets; namespace Barotrauma.Networking { @@ -28,8 +29,12 @@ namespace Barotrauma.Networking netPeerConfiguration = new NetPeerConfiguration("barotrauma") { - UseDualModeSockets = GameSettings.CurrentConfig.UseDualModeSockets + DualStack = GameSettings.CurrentConfig.UseDualModeSockets }; + if (endpoint.NetEndpoint.Address.AddressFamily == AddressFamily.InterNetworkV6) + { + netPeerConfiguration.LocalAddress = System.Net.IPAddress.IPv6Any; + } netPeerConfiguration.DisableMessageType( NetIncomingMessageType.DebugMessage @@ -53,8 +58,8 @@ namespace Barotrauma.Networking if (SteamManager.IsInitialized) { - steamAuthTicket = SteamManager.GetAuthSessionTicket(); - if (steamAuthTicket == null) + steamAuthTicket = SteamManager.GetAuthSessionTicketForMultiplayer(ServerEndpoint); + if (steamAuthTicket.IsNone()) { throw new Exception("GetAuthSessionTicket returned null"); } @@ -207,8 +212,8 @@ namespace Barotrauma.Networking netClient.Shutdown(peerDisconnectPacket.ToLidgrenStringRepresentation()); netClient = null; - steamAuthTicket?.Cancel(); - steamAuthTicket = null; + if (steamAuthTicket.TryUnwrap(out var ticket)) { ticket.Cancel(); } + steamAuthTicket = Option.None; callbacks.OnDisconnect.Invoke(peerDisconnectPacket); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs index e833fa7eb..08318d13f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs @@ -40,7 +40,7 @@ namespace Barotrauma.Networking ContentPackageOrderReceived = false; - steamAuthTicket = SteamManager.GetAuthSessionTicket(); + steamAuthTicket = SteamManager.GetAuthSessionTicketForMultiplayer(ServerEndpoint); //TODO: wait for GetAuthSessionTicketResponse_t if (steamAuthTicket == null) @@ -363,8 +363,8 @@ namespace Barotrauma.Networking Steamworks.SteamNetworking.ResetActions(); Steamworks.SteamNetworking.CloseP2PSessionWithUser(hostSteamId.Value); - steamAuthTicket?.Cancel(); - steamAuthTicket = null; + if (steamAuthTicket.TryUnwrap(out var ticket)) { ticket.Cancel(); } + steamAuthTicket = Option.None; callbacks.OnDisconnect.Invoke(peerDisconnectPacket); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs index 30a3b78a8..5441869bd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs @@ -49,11 +49,15 @@ namespace Barotrauma.Networking respawnPromptCoroutine = CoroutineManager.Invoke(() => { - if (Character.Controlled != null || (!(GameMain.GameSession?.IsRunning ?? false))) { return; } + if (Character.Controlled != null || (GameMain.GameSession is not { IsRunning: true })) { return; } - LocalizedString text = - TextManager.GetWithVariable("respawnskillpenalty", "[percentage]", ((int)(SkillReductionOnDeath * 100)).ToString()) - + "\n\n" + TextManager.Get("respawnquestionprompt"); + LocalizedString text = TextManager.Get("respawnquestionprompt"); + if (SkillLossPercentageOnDeath > 0) + { + text = + TextManager.GetWithVariable("respawnskillpenalty", "[percentage]", ((int)SkillLossPercentageOnDeath).ToString()) + + "\n\n" + text; + }; var respawnPrompt = new GUIMessageBox( TextManager.Get("tutorial.tryagainheader"), text, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs index 23a9f6bef..f7c5d0a44 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Reflection; using System.Xml.Linq; using Barotrauma.Steam; +using System.Globalization; namespace Barotrauma.Networking { @@ -65,8 +66,8 @@ namespace Barotrauma.Networking [Serialize(false, IsPropertySaveable.Yes)] public bool AllowRespawn { get; set; } - [Serialize(YesNoMaybe.No, IsPropertySaveable.Yes)] - public YesNoMaybe TraitorsEnabled { get; set; } + [Serialize(0.0f, IsPropertySaveable.Yes)] + public float TraitorProbability { get; set; } [Serialize(PlayStyle.Casual, IsPropertySaveable.Yes)] public PlayStyle PlayStyle { get; set; } @@ -397,10 +398,10 @@ namespace Barotrauma.Networking public IEnumerable GetPlayStyleTags() { yield return $"Karma.{KarmaEnabled}".ToIdentifier(); - yield return (TraitorsEnabled == YesNoMaybe.Yes ? $"Traitors.True" : $"Traitors.False").ToIdentifier(); + yield return (TraitorProbability > 0.0f ? $"Traitors.True" : $"Traitors.False").ToIdentifier(); yield return $"VoIP.{VoipEnabled}".ToIdentifier(); yield return $"FriendlyFire.{FriendlyFireEnabled}".ToIdentifier(); - yield return $"Modded.{ContentPackages.Any()}".ToIdentifier(); + yield return $"Modded.{IsModded}".ToIdentifier(); } public void UpdateInfo(Func valueGetter) @@ -424,7 +425,7 @@ namespace Barotrauma.Networking VoipEnabled = getBool("voicechatenabled"); GameMode = valueGetter("gamemode")?.ToIdentifier() ?? Identifier.Empty; - if (Enum.TryParse(valueGetter("traitors"), out YesNoMaybe traitorsEnabled)) { TraitorsEnabled = traitorsEnabled; } + if (float.TryParse(valueGetter("traitors"), NumberStyles.Any, CultureInfo.InvariantCulture, out float traitorProbability)) { TraitorProbability = traitorProbability; } if (Enum.TryParse(valueGetter("playstyle"), out PlayStyle playStyle)) { PlayStyle = playStyle; } Language = valueGetter("language")?.ToLanguageIdentifier() ?? LanguageIdentifier.None; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs index e0476a625..76036545e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs @@ -146,7 +146,9 @@ namespace Barotrauma.Networking List tickBoxes = new List(); foreach (MessageType msgType in Enum.GetValues(typeof(MessageType))) { - var tickBox = new GUITickBox(new RectTransform(new Point(tickBoxContainer.Rect.Width, (int)(25 * GUI.Scale)), tickBoxContainer.RectTransform), TextManager.Get("ServerLog." + messageTypeName[msgType]), font: GUIStyle.SmallFont) + var tickBox = new GUITickBox(new RectTransform(new Point(tickBoxContainer.Rect.Width, (int)(25 * GUI.Scale)), tickBoxContainer.RectTransform), + TextManager.Get("ServerLog." + messageTypeName[msgType]).Fallback(messageTypeName[msgType]), + font: GUIStyle.SmallFont) { Selected = true, TextColor = messageColor[msgType], diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index 57c19e71c..82f553984 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -182,7 +182,8 @@ namespace Barotrauma.Networking int? missionTypeAnd = null, float? levelDifficulty = null, bool? autoRestart = null, - int traitorSetting = 0, + float? traitorProbability = null, + int traitorDangerLevel = 0, int botCount = 0, int botSpawnMode = 0, bool? useRespawnShuttle = null) @@ -244,7 +245,11 @@ namespace Barotrauma.Networking { outMsg.WriteRangedInteger(missionTypeOr ?? (int)Barotrauma.MissionType.None, 0, (int)Barotrauma.MissionType.All); outMsg.WriteRangedInteger(missionTypeAnd ?? (int)Barotrauma.MissionType.All, 0, (int)Barotrauma.MissionType.All); - outMsg.WriteByte((byte)(traitorSetting + 1)); + + outMsg.WriteBoolean(traitorProbability != null); + outMsg.WriteSingle(traitorProbability ?? 0.0f); + outMsg.WriteByte((byte)(traitorDangerLevel + 1)); + outMsg.WriteByte((byte)(botCount + 1)); outMsg.WriteByte((byte)(botSpawnMode + 1)); @@ -557,6 +562,26 @@ namespace Barotrauma.Networking }; slider.OnMoved(slider, slider.BarScroll); + LocalizedString skillLossLabel = TextManager.Get("ServerSettingsSkillLossPercentageOnDeath"); + var skillLossText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), roundsContent.RectTransform), skillLossLabel); + var skillLossSlider = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.05f), roundsContent.RectTransform), barSize: 0.1f, style: "GUISlider") + { + UserData = skillLossText, + Range = new Vector2(0, 100), + StepValue = 1, + OnMoved = (GUIScrollBar scrollBar, float barScroll) => + { + GUITextBlock text = scrollBar.UserData as GUITextBlock; + text.Text = TextManager.AddPunctuation( + ':', + skillLossLabel, + TextManager.GetWithVariable("percentageformat", "[value]", ((int)Math.Round(scrollBar.BarScrollValue)).ToString())); + return true; + } + }; + GetPropertyData(nameof(SkillLossPercentageOnDeath)).AssignGUIComponent(skillLossSlider); + skillLossSlider.OnMoved(skillLossSlider, skillLossSlider.BarScroll); + var respawnBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), sliderLayout.RectTransform), TextManager.Get("ServerSettingsAllowRespawning")); GetPropertyData(nameof(AllowRespawn)).AssignGUIComponent(respawnBox); @@ -690,7 +715,7 @@ namespace Barotrauma.Networking Stretch = true }; - var traitorsMinPlayerCount = CreateLabeledNumberInput(numberLayout, "ServerSettingsTraitorsMinPlayerCount", 1, 16, "ServerSettingsTraitorsMinPlayerCountToolTip"); + var traitorsMinPlayerCount = CreateLabeledNumberInput(numberLayout, "ServerSettingsTraitorsMinPlayerCount", 2, 16, "ServerSettingsTraitorsMinPlayerCountToolTip"); GetPropertyData(nameof(TraitorsMinPlayerCount)).AssignGUIComponent(traitorsMinPlayerCount); var maximumTransferAmount = CreateLabeledNumberInput(numberLayout, "serversettingsmaximumtransferrequest", 0, CampaignMode.MaxMoney, "serversettingsmaximumtransferrequesttooltip"); @@ -701,9 +726,6 @@ namespace Barotrauma.Networking lootedMoneyDestination.AddItem(TextManager.Get("lootedmoneydestination.wallet"), LootedMoneyDestination.Wallet); GetPropertyData(nameof(LootedMoneyDestination)).AssignGUIComponent(lootedMoneyDestination); - var ragdollButtonBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), numberLayout.RectTransform), TextManager.Get("ServerSettingsAllowRagdollButton")); - GetPropertyData(nameof(AllowRagdollButton)).AssignGUIComponent(ragdollButtonBox); - var disableBotConversationsBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), numberLayout.RectTransform), TextManager.Get("ServerSettingsDisableBotConversations")); GetPropertyData(nameof(DisableBotConversations)).AssignGUIComponent(disableBotConversationsBox); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs index 470219c9f..9ca9c34f9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs @@ -188,6 +188,10 @@ namespace Barotrauma msg.WriteBoolean(false); //not initiating a vote msg.WriteInt32(money); break; + case VoteType.Traitor: + //use 0 to indicate we voted for no-one + msg.WriteInt32((data as Client)?.SessionId ?? 0); + break; } msg.WritePadBits(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs index 3f8492120..9d762e0d0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs @@ -85,7 +85,7 @@ namespace Barotrauma.Particles public float StartDelay { get { return startDelay; } - set { startDelay = MathHelper.Clamp(value, Prefab.StartDelayMin, prefab.StartDelayMax); } + set { startDelay = Math.Max(value, 0.0f); } } public Vector2 Size @@ -311,7 +311,8 @@ namespace Barotrauma.Particles { foreach (ParticleEmitter emitter in subEmitters) { - emitter.Emit(deltaTime, position, currentHull); + emitter.Emit(deltaTime, position, currentHull, particleRotation: rotation, + sizeMultiplier: emitter.Prefab.Properties.CopyParentParticleScale ? Math.Max(size.X, size.Y) : 1.0f); } } @@ -566,11 +567,12 @@ namespace Barotrauma.Particles drawPosition = Timing.Interpolate(prevPosition, position); drawRotation = Timing.Interpolate(prevRotation, rotation); } - + public void Draw(SpriteBatch spriteBatch) { - Vector2 drawSize = size; + if (startDelay > 0.0f) { return; } + Vector2 drawSize = size; if (prefab.GrowTime > 0.0f && totalLifeTime - lifeTime < prefab.GrowTime) { drawSize *= MathUtils.SmoothStep((totalLifeTime - lifeTime) / prefab.GrowTime); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs index 55d0d15cd..4bd6e7605 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs @@ -88,6 +88,9 @@ namespace Barotrauma.Particles [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Only relevant for status effects. Makes the emitter copy the angle from the target of the effect instead of the entity applying the effect.")] public bool CopyTargetAngle { get; set; } + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Only relevant for particles spawned by another particle. Makes the emitter copy the scale of the parent particle.")] + public bool CopyParentParticleScale { get; set; } + [Editable, Serialize("1,1,1,1", IsPropertySaveable.Yes)] public Color ColorMultiplier { get; set; } @@ -195,7 +198,11 @@ namespace Barotrauma.Particles private void Emit(Vector2 position, Hull hullGuess, float angle, float particleRotation, float velocityMultiplier, float sizeMultiplier, Color? colorMultiplier = null, ParticlePrefab overrideParticle = null, bool mirrorAngle = false, Tuple tracerPoints = null) { var particlePrefab = overrideParticle ?? Prefab.ParticlePrefab; - if (particlePrefab == null) { return; } + if (particlePrefab == null) + { + DebugConsole.AddWarning($"Could not find the particle prefab \"{Prefab.ParticlePrefabName}\"."); + return; + } Vector2 velocity = Vector2.Zero; if (!MathUtils.NearlyEqual(Prefab.Properties.VelocityMax * velocityMultiplier, 0.0f) || !MathUtils.NearlyEqual(Prefab.Properties.DistanceMax, 0.0f)) @@ -206,7 +213,7 @@ namespace Barotrauma.Particles position += dir * Rand.Range(Prefab.Properties.DistanceMin, Prefab.Properties.DistanceMax); } - var particle = GameMain.ParticleManager.CreateParticle(particlePrefab, position, velocity, particleRotation, hullGuess, lifeTimeMultiplier: Prefab.Properties.LifeTimeMultiplier, tracerPoints: tracerPoints); + var particle = GameMain.ParticleManager.CreateParticle(particlePrefab, position, velocity, particleRotation, hullGuess, particlePrefab.DrawOnTop || Prefab.DrawOnTop, lifeTimeMultiplier: Prefab.Properties.LifeTimeMultiplier, tracerPoints: tracerPoints); if (particle != null) { @@ -227,6 +234,7 @@ namespace Barotrauma.Particles public Rectangle CalculateParticleBounds(Vector2 startPosition) { Rectangle bounds = new Rectangle((int)startPosition.X, (int)startPosition.Y, (int)startPosition.X, (int)startPosition.Y); + if (Prefab.ParticlePrefab == null) { return bounds; } for (float angle = Prefab.Properties.AngleMinRad; angle <= Prefab.Properties.AngleMaxRad; angle += 0.1f) { @@ -255,16 +263,22 @@ namespace Barotrauma.Particles } bounds = new Rectangle(bounds.X, bounds.Y, bounds.Width - bounds.X, bounds.Height - bounds.Y); - return bounds; } } class ParticleEmitterPrefab { - private readonly Identifier particlePrefabName; + public readonly Identifier ParticlePrefabName; - public ParticlePrefab ParticlePrefab => ParticlePrefab.Prefabs[particlePrefabName]; + public ParticlePrefab ParticlePrefab + { + get + { + ParticlePrefab.Prefabs.TryGet(ParticlePrefabName, out var prefab); + return prefab; + } + } public readonly ParticleEmitterProperties Properties; @@ -273,13 +287,13 @@ namespace Barotrauma.Particles public ParticleEmitterPrefab(ContentXElement element) { Properties = new ParticleEmitterProperties(element); - particlePrefabName = element.GetAttributeIdentifier("particle", ""); + ParticlePrefabName = element.GetAttributeIdentifier("particle", ""); } public ParticleEmitterPrefab(ParticlePrefab prefab, ParticleEmitterProperties properties) { Properties = properties; - particlePrefabName = prefab.Identifier; + ParticlePrefabName = prefab.Identifier; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs index 9aa60c592..37c906798 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs @@ -43,6 +43,13 @@ namespace Barotrauma.Particles } private Particle[] particles; + /// + /// Used for rendering the particles in the order in which they were created (starting from the most recent one) + /// to avoid the order of the particles shuffling around when particles are removed from the pool. + /// Linked list for fast additions and removals at the middle of the list. + /// + private readonly LinkedList particlesInCreationOrder = new LinkedList(); + private Camera cam; public Camera Camera @@ -75,7 +82,7 @@ namespace Barotrauma.Particles return CreateParticle(prefab, position, velocity, rotation, hullGuess, collisionIgnoreTimer: collisionIgnoreTimer, tracerPoints:tracerPoints); } - public Particle CreateParticle(ParticlePrefab prefab, Vector2 position, Vector2 velocity, float rotation = 0.0f, Hull hullGuess = null, float collisionIgnoreTimer = 0f, float lifeTimeMultiplier = 1f, Tuple tracerPoints = null) + public Particle CreateParticle(ParticlePrefab prefab, Vector2 position, Vector2 velocity, float rotation = 0.0f, Hull hullGuess = null, bool drawOnTop = false, float collisionIgnoreTimer = 0f, float lifeTimeMultiplier = 1f, Tuple tracerPoints = null) { if (prefab == null || prefab.Sprites.Count == 0) { return null; } if (particleCount >= MaxParticles) @@ -116,12 +123,13 @@ namespace Barotrauma.Particles } if (particles[particleCount] == null) { particles[particleCount] = new Particle(); } + Particle particle = particles[particleCount]; - particles[particleCount].Init(prefab, position, velocity, rotation, hullGuess, prefab.DrawOnTop, collisionIgnoreTimer, lifeTimeMultiplier, tracerPoints: tracerPoints); - + particle.Init(prefab, position, velocity, rotation, hullGuess, drawOnTop, collisionIgnoreTimer, lifeTimeMultiplier, tracerPoints: tracerPoints); particleCount++; + particlesInCreationOrder.AddFirst(particle); - return particles[particleCount - 1]; + return particle; } public static List GetPrefabList() @@ -137,11 +145,10 @@ namespace Barotrauma.Particles private void RemoveParticle(int index) { + particlesInCreationOrder.Remove(particles[index]); particleCount--; - Particle swap = particles[index]; - particles[index] = particles[particleCount]; - particles[particleCount] = swap; + (particles[particleCount], particles[index]) = (particles[index], particles[particleCount]); } @@ -201,9 +208,8 @@ namespace Barotrauma.Particles { ParticlePrefab.DrawTargetType drawTarget = inWater ? ParticlePrefab.DrawTargetType.Water : ParticlePrefab.DrawTargetType.Air; - for (int i = 0; i < particleCount; i++) + foreach (var particle in particlesInCreationOrder) { - var particle = particles[i]; if (particle.BlendState != blendState) { continue; } //equivalent to !particles[i].DrawTarget.HasFlag(drawTarget) but garbage free and faster if ((particle.DrawTarget & drawTarget) == 0) { continue; } @@ -215,14 +221,15 @@ namespace Barotrauma.Particles continue; } } - - particles[i].Draw(spriteBatch); + + particle.Draw(spriteBatch); } } public void ClearParticles() { particleCount = 0; + particlesInCreationOrder.Clear(); } public void RemoveByPrefab(ParticlePrefab prefab) @@ -234,6 +241,7 @@ namespace Barotrauma.Particles { if (i < particleCount) { particleCount--; } + particlesInCreationOrder.Remove(particles[particleCount]); Particle swap = particles[particleCount]; particles[particleCount] = null; particles[i] = swap; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs index 23fb1ea3d..c550fbfee 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs @@ -165,7 +165,7 @@ namespace Barotrauma.Particles [Editable, Serialize("0.0,0.0", IsPropertySaveable.No, description: "How much the size of the particle changes per second. The rate of growth for each particle is randomize between SizeChangeMin and SizeChangeMax.")] public Vector2 SizeChangeMax { get; private set; } - [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "How many seconds it takes for the particle to grow to it's initial size.")] + [Editable(minValue: 0, maxValue: float.MaxValue, decimals: 2), Serialize(0.0f, IsPropertySaveable.No, description: "How many seconds it takes for the particle to grow to it's initial size.")] public float GrowTime { get; private set; } //rendering ----------------------------------------- diff --git a/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs index 8f61bf354..e8445be17 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs @@ -28,7 +28,7 @@ namespace Barotrauma scale, color, Dir < 0, invert); } - public void Draw(SpriteBatch spriteBatch, Sprite sprite, Color color, float? depth = null, float scale = 1.0f, bool mirrorX = false, bool mirrorY = false) + public void Draw(SpriteBatch spriteBatch, Sprite sprite, Color color, float? depth = null, float scale = 1.0f, bool mirrorX = false, bool mirrorY = false, Vector2? origin = null) { if (!Enabled) { return; } UpdateDrawPosition(); @@ -42,7 +42,7 @@ namespace Barotrauma { spriteEffect |= SpriteEffects.FlipVertically; } - sprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y), color, -drawRotation, scale, spriteEffect, depth); + sprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y), color, origin ?? sprite.Origin, - drawRotation, scale, spriteEffect, depth); } public void DebugDraw(SpriteBatch spriteBatch, Color color, bool forceColor = false) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Program.cs b/Barotrauma/BarotraumaClient/ClientSource/Program.cs index 1440b125b..a3d44b5d6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Program.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Program.cs @@ -132,7 +132,7 @@ namespace Barotrauma try { - if (exception is GameMain.LoadingException) + if (exception.StackTrace.Contains("Barotrauma.GameMain.Load")) { //exception occurred in loading screen: //assume content packages are the culprit and reset them diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs index bb8f98570..ec8146ae0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs @@ -421,7 +421,7 @@ namespace Barotrauma } } - public abstract void UpdateLoadMenu(IEnumerable saveFiles = null); + public abstract void CreateLoadMenu(IEnumerable saveFiles = null); protected bool DeleteSave(GUIButton button, object obj) { @@ -434,7 +434,7 @@ namespace Barotrauma { SaveUtil.DeleteSave(saveInfo.FilePath); prevSaveFiles?.RemoveAll(s => s.FilePath == saveInfo.FilePath); - UpdateLoadMenu(prevSaveFiles.ToList()); + CreateLoadMenu(prevSaveFiles.ToList()); return true; }); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs index c86f3c04e..0a4a9958e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs @@ -162,7 +162,7 @@ namespace Barotrauma verticalLayout.Recalculate(); - UpdateLoadMenu(saveFiles); + CreateLoadMenu(saveFiles); } private IEnumerable WaitForCampaignSetup() @@ -192,7 +192,7 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - public override void UpdateLoadMenu(IEnumerable saveFiles = null) + public override void CreateLoadMenu(IEnumerable saveFiles = null) { prevSaveFiles?.Clear(); prevSaveFiles = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs index f3e1cfdaa..6f51a78d9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs @@ -20,11 +20,10 @@ namespace Barotrauma private GUIButton nextButton; private GUIListBox characterInfoColumns; - public SinglePlayerCampaignSetupUI(GUIComponent newGameContainer, GUIComponent loadGameContainer, IEnumerable submarines, IEnumerable saveFiles = null) + public SinglePlayerCampaignSetupUI(GUIComponent newGameContainer, GUIComponent loadGameContainer) : base(newGameContainer, loadGameContainer) { - UpdateNewGameMenu(submarines); - UpdateLoadMenu(saveFiles); + CreateNewGameMenu(); } private int currentPage = 0; @@ -73,7 +72,7 @@ namespace Barotrauma }); } - private void UpdateNewGameMenu(IEnumerable submarines) + private void CreateNewGameMenu() { pageContainer = new GUIListBox(new RectTransform(Vector2.One, newGameContainer.RectTransform), style: null, isHorizontal: true) @@ -92,7 +91,7 @@ namespace Barotrauma Anchor.Center)); } - CreateFirstPage(createPageLayout(), submarines); + CreateFirstPage(createPageLayout()); CreateSecondPage(createPageLayout()); pageContainer.RecalculateChildren(); @@ -108,7 +107,7 @@ namespace Barotrauma SetPage(0); } - private void CreateFirstPage(GUILayoutGroup firstPageLayout, IEnumerable submarines) + private void CreateFirstPage(GUILayoutGroup firstPageLayout) { firstPageLayout.RelativeSpacing = 0.02f; @@ -238,8 +237,6 @@ namespace Barotrauma columnContainer.Recalculate(); leftColumn.Recalculate(); rightColumn.Recalculate(); - - if (submarines != null) { UpdateSubList(submarines); } } private void CreateSecondPage(GUILayoutGroup secondPageLayout) @@ -578,7 +575,7 @@ namespace Barotrauma } } - public override void UpdateLoadMenu(IEnumerable saveFiles = null) + public override void CreateLoadMenu(IEnumerable saveFiles = null) { prevSaveFiles?.Clear(); prevSaveFiles = null; @@ -625,21 +622,21 @@ namespace Barotrauma var saveFrame = CreateSaveElement(saveInfo); if (saveFrame == null) { continue; } - XDocument doc = SaveUtil.LoadGameSessionDoc(saveInfo.FilePath); + XElement docRoot = SaveUtil.ExtractGameSessionRootElementFromSaveFile(saveInfo.FilePath); - if (doc?.Root == null) + if (docRoot == null) { DebugConsole.ThrowError("Error loading save file \"" + saveInfo.FilePath + "\". The file may be corrupted."); saveFrame.GetChild().TextColor = GUIStyle.Red; continue; } - if (doc.Root.GetChildElement("multiplayercampaign") != null) + if (docRoot.GetChildElement("multiplayercampaign") != null) { //multiplayer campaign save in the wrong folder -> don't show the save saveList.Content.RemoveChild(saveFrame); continue; } - if (!SaveUtil.IsSaveFileCompatible(doc)) + if (!SaveUtil.IsSaveFileCompatible(docRoot)) { saveFrame.GetChild().TextColor = GUIStyle.Red; saveFrame.ToolTip = TextManager.Get("campaignmode.incompatiblesave"); @@ -668,14 +665,14 @@ namespace Barotrauma string fileName = saveInfo.FilePath; - XDocument doc = SaveUtil.LoadGameSessionDoc(fileName); - if (doc?.Root == null) + XElement docRoot = SaveUtil.ExtractGameSessionRootElementFromSaveFile(fileName); + if (docRoot == null) { DebugConsole.ThrowError("Error loading save file \"" + fileName + "\". The file may be corrupted."); return false; } - loadGameButton.Enabled = SaveUtil.IsSaveFileCompatible(doc); + loadGameButton.Enabled = SaveUtil.IsSaveFileCompatible(docRoot); RemoveSaveFrame(); @@ -684,7 +681,7 @@ namespace Barotrauma .Select(t => (LocalizedString)t.ToLocalUserString()) .Fallback(TextManager.Get("Unknown")); - string mapseed = doc.Root.GetAttributeString("mapseed", "unknown"); + string mapseed = docRoot.GetAttributeString("mapseed", "unknown"); var saveFileFrame = new GUIFrame( new RectTransform(new Vector2(0.45f, 0.6f), loadGameContainer.RectTransform, Anchor.TopRight) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index b3e163a16..f93163433 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -1187,6 +1187,11 @@ namespace Barotrauma.CharacterEditor private void CreateLimb(ContentXElement newElement) { + if (RagdollParams.MainElement == null) + { + DebugConsole.ThrowError("Main element null! Failed to create a limb."); + return; + } var lastElement = RagdollParams.MainElement.GetChildElements("limb").LastOrDefault(); if (lastElement != null) { @@ -1217,6 +1222,11 @@ namespace Barotrauma.CharacterEditor DebugConsole.ThrowError(GetCharacterEditorTranslation("ExistingJointFound").Replace("[limbid1]", fromLimb.ToString()).Replace("[limbid2]", toLimb.ToString())); return; } + if (RagdollParams.MainElement == null) + { + DebugConsole.ThrowError("The main element of the ragdoll params is null! Failed to create a joint."); + return; + } //RagdollParams.StoreState(); Vector2 a1 = anchor1 ?? Vector2.Zero; Vector2 a2 = anchor2 ?? Vector2.Zero; @@ -1775,7 +1785,7 @@ namespace Barotrauma.CharacterEditor string ragdollPath = RagdollParams.GetDefaultFile(name, contentPackage); RagdollParams ragdollParams = isHumanoid ? RagdollParams.CreateDefault(ragdollPath, name, ragdoll) - : RagdollParams.CreateDefault(ragdollPath, name, ragdoll) as RagdollParams; + : RagdollParams.CreateDefault(ragdollPath, name, ragdoll); // Animations AnimationParams.ClearCache(); @@ -1789,6 +1799,7 @@ namespace Barotrauma.CharacterEditor foreach (var animation in animations) { XElement element = animation.MainElement; + if (element == null) { continue; } element.SetAttributeValue("type", name); string fullPath = AnimationParams.GetDefaultFile(name, animation.AnimationType); element.Name = AnimationParams.GetDefaultFileName(name, animation.AnimationType); @@ -2140,7 +2151,8 @@ namespace Barotrauma.CharacterEditor { foreach (var limb in character.AnimController.Limbs) { - limb.ActiveSprite.ReloadTexture(); + if (limb == null) { continue; } + limb.ActiveSprite?.ReloadTexture(); limb.WearingItems.ForEach(i => i.Sprite.ReloadTexture()); limb.OtherWearables.ForEach(w => w.Sprite.ReloadTexture()); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EditorNode.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EditorNode.cs index 5383d7614..c5424e4ba 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EditorNode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EditorNode.cs @@ -25,7 +25,7 @@ namespace Barotrauma public bool CanAddConnections { get; set; } - public readonly List Connections = new List(); + public readonly List Connections = new List(); public readonly List RemovableTypes = new List(); @@ -47,7 +47,7 @@ namespace Barotrauma public XElement SaveConnections() { XElement allConnections = new XElement("Connections", new XAttribute("i", ID)); - foreach (NodeConnection connection in Connections) + foreach (EventEditorNodeConnection connection in Connections) { XElement connectionElement = new XElement("Connection"); connectionElement.Add(new XAttribute("i", connection.ID)); @@ -93,12 +93,12 @@ namespace Barotrauma if (id < 0) { continue; } - NodeConnection? connection = Connections.Find(c => c.ID == id); + EventEditorNodeConnection? 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 }; + connection = new EventEditorNodeConnection(this, NodeConnectionType.Option) { ID = id, EndConversation = endConversation }; Connections.Add(connection); } else @@ -143,7 +143,7 @@ namespace Barotrauma if (id2 < 0 || node < 0) { continue; } EditorNode? otherNode = EventEditorScreen.nodeList.Find(editorNode => editorNode.ID == node); - NodeConnection? otherConnection = otherNode?.Connections.Find(c => c.ID == id2); + EventEditorNodeConnection? otherConnection = otherNode?.Connections.Find(c => c.ID == id2); if (otherConnection != null) { connection.ConnectedTo.Add(otherConnection); @@ -184,20 +184,20 @@ namespace Barotrauma 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); + EventEditorNodeConnection? conn = Connections.Find(connection => connection.Type == type && !connection.ConnectedTo.Any()); + EventEditorNodeConnection? found = otherNode.Connections.Find(connection => connection.Type == NodeConnectionType.Activate); if (found != null) { conn?.ConnectedTo.Add(found); } } - public void Connect(NodeConnection connection, NodeConnection ownConnection) + public static void Connect(EventEditorNodeConnection connection, EventEditorNodeConnection ownConnection) { connection.ConnectedTo.Add(ownConnection); } - public void Disconnect(NodeConnection conn) + public static void Disconnect(EventEditorNodeConnection conn) { foreach (var connection in EventEditorScreen.nodeList.SelectMany(editorNode => editorNode.Connections.Where(connection => connection.ConnectedTo.Contains(conn)))) { @@ -207,7 +207,7 @@ namespace Barotrauma public void ClearConnections() { - foreach (NodeConnection conn in Connections) + foreach (EventEditorNodeConnection conn in Connections) { conn.ClearConnections(); } @@ -218,7 +218,7 @@ namespace Barotrauma return Rectangle; } - public NodeConnection? GetConnectionOnMouse(Vector2 mousePos) + public EventEditorNodeConnection? GetConnectionOnMouse(Vector2 mousePos) { return Connections.FirstOrDefault(eventNodeConnection => eventNodeConnection.DrawRectangle.Contains(mousePos)); } @@ -254,7 +254,7 @@ namespace Barotrauma 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) + foreach (EventEditorNodeConnection connection in Connections) { switch (connection.Type.NodeSide) { @@ -275,10 +275,10 @@ namespace Barotrauma public virtual void AddOption() { - Connections.Add(new NodeConnection(this, NodeConnectionType.Option)); + Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Option)); } - public void RemoveOption(NodeConnection connection) + public void RemoveOption(EventEditorNodeConnection connection) { int index = Connections.IndexOf(connection); foreach (var nodeConnection in Connections.Skip(index)) @@ -313,7 +313,7 @@ namespace Barotrauma foreach (EditorNode editorNode in EventEditorScreen.nodeList) { - List childConnection = editorNode.Connections.Where(connection => connection.Type == NodeConnectionType.Next || + List childConnection = editorNode.Connections.Where(connection => connection.Type == NodeConnectionType.Next || connection.Type == NodeConnectionType.Option || connection.Type == NodeConnectionType.Failure || connection.Type == NodeConnectionType.Success || @@ -338,18 +338,18 @@ namespace Barotrauma 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)); + Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Activate)); + Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Next)); foreach (PropertyInfo property in properties) { - Connections.Add(new NodeConnection(this, NodeConnectionType.Value, property.Name, property.PropertyType, property)); + Connections.Add(new EventEditorNodeConnection(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)); + Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Success)); + Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Failure)); } if (IsInstanceOf(type, typeof(ConversationAction))) @@ -360,7 +360,7 @@ namespace Barotrauma if (IsInstanceOf(type, typeof(StatusEffectAction)) || IsInstanceOf(type, typeof(MissionAction))) { - Connections.Add(new NodeConnection(this, NodeConnectionType.Add)); + Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Add)); } } @@ -395,7 +395,7 @@ namespace Barotrauma return ScaleRectFromConnections(Connections, Rectangle); } - public static Rectangle ScaleRectFromConnections(List connections, Rectangle baseRect) + 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), @@ -409,15 +409,15 @@ namespace Barotrauma public Tuple[] GetOptions() { - IEnumerable myNode = Connections.Where(connection => connection.Type == NodeConnectionType.Option).ToArray(); + IEnumerable myNode = Connections.Where(connection => connection.Type == NodeConnectionType.Option).ToArray(); List> list = new List>(); if (myNode != null) { - foreach (NodeConnection connection in myNode) + foreach (EventEditorNodeConnection connection in myNode) { if (connection.ConnectedTo.Any()) { - foreach (NodeConnection nodeConnection in connection.ConnectedTo) + foreach (EventEditorNodeConnection nodeConnection in connection.ConnectedTo) { list.Add(Tuple.Create((EditorNode?) nodeConnection.Parent, connection.OptionText, connection.EndConversation)); } @@ -464,7 +464,7 @@ namespace Barotrauma Type = type; Value = type.IsValueType ? Activator.CreateInstance(type) : null; Size = new Vector2(256, 32); - Connections.Add(new NodeConnection(this, NodeConnectionType.Out, "Output", Type)); + Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Out, "Output", Type)); } public override XElement Save() @@ -564,7 +564,7 @@ namespace Barotrauma Vector2 pos = GetDrawRectangle().Location.ToVector2() + (GetDrawRectangle().Size.ToVector2() / 2) - (valueTextSize / 2); Rectangle drawRect = Rectangle; drawRect.Inflate(-1, -1); - GUI.DrawString(spriteBatch, pos, WrappedText, NodeConnection.GetPropertyColor(Type), font: GUIStyle.SubHeadingFont); + GUI.DrawString(spriteBatch, pos, WrappedText, EventEditorNodeConnection.GetPropertyColor(Type), font: GUIStyle.SubHeadingFont); } } @@ -587,9 +587,9 @@ namespace Barotrauma { 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)); + Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Activate)); + Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Next)); + Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Add)); } public CustomNode() : this("Custom") @@ -605,7 +605,7 @@ namespace Barotrauma { Prompt(s => { - Connections.Add(new NodeConnection(this, NodeConnectionType.Value, s, typeof(string))); + Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Value, s, typeof(string))); return true; }); } @@ -617,7 +617,7 @@ namespace Barotrauma 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)) + foreach (EventEditorNodeConnection connection in Connections.FindAll(connection => connection.Type == NodeConnectionType.Value)) { newElement.Add(new XElement("Value", new XAttribute("name", connection.Attribute))); } @@ -634,7 +634,7 @@ namespace Barotrauma 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))); + newNode.Connections.Add(new EventEditorNodeConnection(newNode, NodeConnectionType.Value, valueElement.GetAttributeString("name", string.Empty), typeof(string))); } return newNode; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/NodeConnection.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorNodeConnection.cs similarity index 86% rename from Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/NodeConnection.cs rename to Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorNodeConnection.cs index d5814d94a..056913fb5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/NodeConnection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorNodeConnection.cs @@ -39,7 +39,7 @@ namespace Barotrauma } } - internal class NodeConnection + internal class EventEditorNodeConnection { public string Attribute { get; } @@ -128,7 +128,7 @@ namespace Barotrauma public readonly EditorNode Parent; - public readonly List ConnectedTo = new List(); + public readonly List ConnectedTo = new List(); private readonly Color bgColor = Color.DarkGray * 0.8f; @@ -163,7 +163,7 @@ namespace Barotrauma ConnectedTo.Clear(); } - public NodeConnection(EditorNode parent, NodeConnectionType type, string attribute = "", Type? valueType = null, PropertyInfo? propertyInfo = null) + public EventEditorNodeConnection(EditorNode parent, NodeConnectionType type, string attribute = "", Type? valueType = null, PropertyInfo? propertyInfo = null) { Type = type; ValueType = valueType; @@ -244,7 +244,7 @@ namespace Barotrauma private void DrawConnections(SpriteBatch spriteBatch, int yOffset, float width = 2, Color? overrideColor = null) { - foreach (NodeConnection? eventNodeConnection in ConnectedTo) + foreach (EventEditorNodeConnection? eventNodeConnection in ConnectedTo) { if (eventNodeConnection != null) { @@ -279,37 +279,9 @@ namespace Barotrauma 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; - } + float knobLength = 24 * (yOffset + 1); + Vector2 start = new Vector2(DrawRectangle.Right, DrawRectangle.Center.Y); + var (points, _) = ToolBox.GetSquareLineBetweenPoints(start, position, knobLength); Color drawColor = Parent is ValueNode ? GetPropertyColor(ValueType) : GUIStyle.Red; @@ -318,11 +290,11 @@ namespace Barotrauma 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[0], 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); + GUI.DrawLine(spriteBatch, points[2], points[3], drawColor, width: (int)width); + GUI.DrawLine(spriteBatch, points[3], points[4], drawColor, width: (int)width); + GUI.DrawLine(spriteBatch, points[4], points[5], drawColor, width: (int)width); } private static readonly Color defaultColor = new Color(139, 233, 253); @@ -345,7 +317,7 @@ namespace Barotrauma return color; } - public bool CanConnect(NodeConnection otherNode) + public bool CanConnect(EventEditorNodeConnection otherNode) { if (otherNode.OverrideValue != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs index aee1762ac..9a733e235 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs @@ -25,7 +25,7 @@ namespace Barotrauma private readonly List selectedNodes = new List(); public static Vector2 DraggingPosition = Vector2.Zero; - public static NodeConnection? DraggedConnection; + public static EventEditorNodeConnection? DraggedConnection; private EditorNode? draggedNode; private Vector2 dragOffset; @@ -394,7 +394,7 @@ namespace Barotrauma newNode = new CustomNode(subElement.Name.ToString()) { Position = new Vector2(ident, 0), ID = CreateID() }; foreach (XAttribute attribute in subElement.Attributes().Where(attribute => !attribute.ToString().StartsWith("_"))) { - newNode.Connections.Add(new NodeConnection(newNode, NodeConnectionType.Value, attribute.Name.ToString(), typeof(string))); + newNode.Connections.Add(new EventEditorNodeConnection(newNode, NodeConnectionType.Value, attribute.Name.ToString(), typeof(string))); } } @@ -414,7 +414,7 @@ namespace Barotrauma { if (xElement.Name.ToString().ToLowerInvariant() == "option") { - NodeConnection optionConnection = new NodeConnection(newNode, NodeConnectionType.Option) + EventEditorNodeConnection optionConnection = new EventEditorNodeConnection(newNode, NodeConnectionType.Option) { OptionText = xElement.GetAttributeString("text", string.Empty), EndConversation = xElement.GetAttributeBool("endconversation", false) @@ -423,7 +423,7 @@ namespace Barotrauma } } - foreach (NodeConnection connection in newNode.Connections) + foreach (EventEditorNodeConnection connection in newNode.Connections) { if (connection.Type == NodeConnectionType.Value) { @@ -479,8 +479,8 @@ namespace Barotrauma case "option": if (parent != null) { - NodeConnection? activateConnection = newNode.Connections.Find(connection => connection.Type == NodeConnectionType.Activate); - NodeConnection? optionConnection = parent.Connections.FirstOrDefault(connection => + EventEditorNodeConnection? activateConnection = newNode.Connections.Find(connection => connection.Type == NodeConnectionType.Activate); + EventEditorNodeConnection? optionConnection = parent.Connections.FirstOrDefault(connection => connection.Type == NodeConnectionType.Option && string.Equals(connection.OptionText, parentElement.GetAttributeString("text", string.Empty), StringComparison.Ordinal)); if (activateConnection != null) @@ -671,7 +671,7 @@ namespace Barotrauma } } - private void CreateContextMenu(EditorNode node, NodeConnection? connection = null) + private void CreateContextMenu(EditorNode node, EventEditorNodeConnection? connection = null) { if (GUIContextMenu.CurrentContextMenu != null) { return; } @@ -757,7 +757,7 @@ namespace Barotrauma return true; } - private static void CreateEditMenu(ValueNode? node, NodeConnection? connection = null) + private static void CreateEditMenu(ValueNode? node, EventEditorNodeConnection? connection = null) { object? newValue; Type? type; @@ -972,7 +972,7 @@ namespace Barotrauma { if (PlayerInput.PrimaryMouseButtonDown()) { - NodeConnection? connection = node.GetConnectionOnMouse(mousePos); + EventEditorNodeConnection? connection = node.GetConnectionOnMouse(mousePos); if (connection != null && connection.Type.NodeSide == NodeConnectionType.Side.Right) { if (connection.Type != NodeConnectionType.Out) @@ -1019,7 +1019,7 @@ namespace Barotrauma if (PlayerInput.SecondaryMouseButtonClicked()) { - NodeConnection? connection = node.GetConnectionOnMouse(mousePos); + EventEditorNodeConnection? connection = node.GetConnectionOnMouse(mousePos); if (node.GetDrawRectangle().Contains(mousePos) || connection != null) { CreateContextMenu(node, node.GetConnectionOnMouse(mousePos)); @@ -1088,7 +1088,7 @@ namespace Barotrauma if (!DraggedConnection.CanConnect(nodeOnMouse)) { continue; } nodeOnMouse.ClearConnections(); - DraggedConnection.Parent.Connect(DraggedConnection, nodeOnMouse); + EditorNode.Connect(DraggedConnection, nodeOnMouse); break; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index 072a2a6d9..917bf10f6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -392,7 +392,7 @@ namespace Barotrauma Level.Loaded?.DrawDebugOverlay(spriteBatch, cam); if (GameMain.DebugDraw) { - MapEntity.mapEntityList.ForEach(me => me.AiTarget?.Draw(spriteBatch)); + MapEntity.MapEntityList.ForEach(me => me.AiTarget?.Draw(spriteBatch)); Character.CharacterList.ForEach(c => c.AiTarget?.Draw(spriteBatch)); if (GameMain.GameSession?.EventManager != null) { @@ -415,7 +415,7 @@ namespace Barotrauma GameMain.LightManager.LosEffect.Parameters["xLosAlpha"].SetValue(GameMain.LightManager.LosAlpha); Color losColor; - if (GameMain.LightManager.LosMode is LosMode.Transparent or LosMode.BlockOutsideView) + if (GameMain.LightManager.LosMode == LosMode.Transparent) { //convert the los color to HLS and make sure the luminance of the color is always the same //as the luminance of the ambient light color diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index 24e67f496..9d122c629 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -236,6 +236,7 @@ namespace Barotrauma } }; + new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), paddedRightPanel.RectTransform), TextManager.Get("leveleditor.test")) { @@ -314,6 +315,52 @@ namespace Barotrauma { RelativeOffset = new Vector2(leftPanel.RectTransform.RelativeSize.X * 2, 0.0f) }, style: "GUIFrameTop"); } + public void TestLevelGenerationForErrors(int amountOfLevelsToGenerate) + { + CoroutineManager.StartCoroutine(GenerateLevels()); + + IEnumerable GenerateLevels() + { + using var errorCatcher = DebugConsole.ErrorCatcher.Create(); + for (int i = 0; i < amountOfLevelsToGenerate; i++) + { + Submarine.Unload(); + GameMain.LightManager.ClearLights(); + + currentLevelData = LevelData.CreateRandom(ToolBox.RandomSeed(10), generationParams: selectedParams); + currentLevelData.ForceOutpostGenerationParams = outpostParamsList.SelectedData as OutpostGenerationParams; + currentLevelData.AllowInvalidOutpost = allowInvalidOutpost.Selected; + var dummyLocations = GameSession.CreateDummyLocations(currentLevelData); + DebugConsole.NewMessage("*****************************************************************************"); + DebugConsole.NewMessage($"Generating level {(i + 1)}/{amountOfLevelsToGenerate}: "); + DebugConsole.NewMessage(" Seed: " + currentLevelData.Seed); + DebugConsole.NewMessage(" Outpost parameters: " + (currentLevelData.ForceOutpostGenerationParams?.Name ?? "None")); + DebugConsole.NewMessage(" Level generation params: " + selectedParams.Identifier); + DebugConsole.NewMessage(" Mirrored: " + mirrorLevel.Selected); + DebugConsole.NewMessage(" Adjacent locations: " + (dummyLocations[0]?.Type.Identifier ?? "none".ToIdentifier()) + ", " + (dummyLocations[1]?.Type.Identifier ?? "none".ToIdentifier())); + + yield return CoroutineStatus.Running; + + Level.Generate(currentLevelData, mirror: mirrorLevel.Selected, startLocation: dummyLocations[0], endLocation: dummyLocations[1]); + Submarine.MainSub?.SetPosition(Level.Loaded.StartPosition); + GameMain.LightManager.AddLight(pointerLightSource); + seedBox.Deselect(); + + if (errorCatcher.Errors.Any()) + { + DebugConsole.ThrowError("Error while generating level:"); + errorCatcher.Errors.ToList().ForEach(e => DebugConsole.ThrowError(e.Text)); + yield return CoroutineStatus.Success; + } + yield return CoroutineStatus.Running; + } + } + + + } + + + public override void Select() { base.Select(); @@ -903,6 +950,7 @@ namespace Barotrauma spriteBatch.End(); } + public override void Update(double deltaTime) { if (lightingEnabled.Selected) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index 959881cc8..bd977496b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -83,7 +83,6 @@ namespace Barotrauma { SetMenuTabPositioning(); CreateHostServerFields(); - CreateCampaignSetupUI(); SettingsMenu.Create(menuTabs[Tab.Settings].RectTransform); if (remoteContentDoc?.Root != null) { @@ -634,7 +633,7 @@ namespace Barotrauma campaignSetupUI.UpdateSubList(SubmarineInfo.SavedSubmarines); break; case Tab.LoadGame: - campaignSetupUI.UpdateLoadMenu(); + campaignSetupUI.CreateLoadMenu(); break; case Tab.Settings: SettingsMenu.Create(menuTabs[Tab.Settings].RectTransform); @@ -1224,7 +1223,7 @@ namespace Barotrauma var paddedLoadGame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), menuTabs[Tab.LoadGame].RectTransform, Anchor.Center) { AbsoluteOffset = new Point(0, 10) }, style: null); - campaignSetupUI = new SinglePlayerCampaignSetupUI(newGameContent, paddedLoadGame, SubmarineInfo.SavedSubmarines) + campaignSetupUI = new SinglePlayerCampaignSetupUI(newGameContent, paddedLoadGame) { LoadGame = LoadGame, StartNewGame = StartGame @@ -1550,6 +1549,14 @@ namespace Barotrauma try { if (!t.TryGetResult(out IRestResponse remoteContentResponse)) { throw new Exception("Task did not return a valid result"); } + if (remoteContentResponse.StatusCode != HttpStatusCode.OK) + { + DebugConsole.AddWarning( + "Failed to receive remote main menu content. " + + "There may be an issue with your internet connection, or the master server might be temporarily unavailable " + + $"(error code: {remoteContentResponse.StatusCode})"); + return; + } string xml = remoteContentResponse.Content; int index = xml.IndexOf('<'); if (index > 0) { xml = xml.Substring(index, xml.Length - index); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs index 8536cf80a..b6169da32 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs @@ -378,7 +378,7 @@ namespace Barotrauma } string dir = path.RemoveFromEnd(ModReceiver.Extension, StringComparison.OrdinalIgnoreCase); - SaveUtil.DecompressToDirectory(path, dir, file => { }); + SaveUtil.DecompressToDirectory(path, dir); var result = ContentPackage.TryLoad(Path.Combine(dir, ContentPackage.FileListFileName)); if (!result.TryUnwrapSuccess(out var newPackage)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 886f4311f..f004779d1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -22,8 +22,6 @@ namespace Barotrauma private GUIComponent jobVariantTooltip; - private SubmarinePreview submarinePreview; - private readonly GUITextBox chatInput; private readonly GUITextBox serverLogFilter; public GUITextBox ChatInput @@ -38,8 +36,10 @@ namespace Barotrauma private readonly GUIScrollBar levelDifficultyScrollBar; - private readonly GUIButton[] traitorProbabilityButtons; + private readonly List traitorElements = new List(); + private readonly GUIScrollBar traitorProbabilitySlider; private readonly GUITextBlock traitorProbabilityText; + private readonly GUILayoutGroup traitorDangerGroup; private readonly GUIButton[] botCountButtons; private readonly GUITextBlock botCountText; @@ -68,6 +68,7 @@ namespace Barotrauma public static GUIButton JobInfoFrame; private readonly GUITickBox spectateBox; + public bool Spectating => spectateBox is { Selected: true, Visible: true }; private readonly GUIFrame playerInfoContainer; @@ -1070,15 +1071,27 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), settingsHolder.RectTransform) { MinSize = new Point(0, 25) }, TextManager.Get("Settings"), font: GUIStyle.SubHeadingFont); - var settingsFrame = new GUIFrame(new RectTransform(Vector2.One, settingsHolder.RectTransform), style: "InnerFrame"); - var settingsContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), settingsFrame.RectTransform, Anchor.Center)) + + var settingsFrameTop = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.55f), settingsHolder.RectTransform), style: "InnerFrame"); + var settingsContentTop = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), settingsFrameTop.RectTransform, Anchor.Center)) { - RelativeSpacing = 0.025f + Stretch = true, + AbsoluteSpacing = GUI.IntScale(10) + }; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), settingsHolder.RectTransform) { MinSize = new Point(0, 30) }, + TextManager.Get("TraitorSettings"), font: GUIStyle.SubHeadingFont); + + var settingsFrameBottom = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.35f), settingsHolder.RectTransform), style: "InnerFrame"); + var settingsContentBottom = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.85f), settingsFrameBottom.RectTransform, Anchor.Center)) + { + Stretch = true, + AbsoluteSpacing = GUI.IntScale(10) }; //seed ------------------------------------------------------------------ - var seedLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), TextManager.Get("LevelSeed")); + var seedLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), settingsContentTop.RectTransform), TextManager.Get("LevelSeed")); SeedBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), seedLabel.RectTransform, Anchor.CenterRight)); SeedBox.OnDeselected += (textBox, key) => { @@ -1089,7 +1102,10 @@ namespace Barotrauma //level difficulty ------------------------------------------------------------------ - var difficultyHolder = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), settingsContent.RectTransform), style: null); + var difficultyHolder = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), settingsContentTop.RectTransform), style: null) + { + CanBeFocused = true + }; var difficultyLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), difficultyHolder.RectTransform), TextManager.Get("LevelDifficulty")) { @@ -1116,46 +1132,16 @@ namespace Barotrauma if (!EventManagerSettings.Prefabs.Any()) { return true; } difficultyName.Text = EventManagerSettings.GetByDifficultyPercentile(value).Name - + " (" + ((int)Math.Round(scrollbar.BarScrollValue)) + " %)"; + + $" ({TextManager.GetWithVariable("percentageformat", "[value]", ((int)Math.Round(scrollbar.BarScrollValue)).ToString())})"; difficultyName.TextColor = ToolBox.GradientLerp(scrollbar.BarScroll, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); return true; }; clientDisabledElements.Add(levelDifficultyScrollBar); - //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); - - var traitorProbContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), traitorsSettingHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.05f, Stretch = true }; - traitorProbabilityButtons = new GUIButton[2]; - traitorProbabilityButtons[0] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), traitorProbContainer.RectTransform), style: "GUIButtonToggleLeft") - { - OnClicked = (button, obj) => - { - GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, traitorSetting: -1); - return true; - } - }; - - traitorProbabilityText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), traitorProbContainer.RectTransform), TextManager.Get("No"), - textAlignment: Alignment.Center, style: "GUITextBox"); - traitorProbabilityButtons[1] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), traitorProbContainer.RectTransform), style: "GUIButtonToggleRight") - { - OnClicked = (button, obj) => - { - GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, traitorSetting: 1); - return true; - } - }; - - clientDisabledElements.AddRange(traitorProbabilityButtons); - //bot count ------------------------------------------------------------------ - var botCountSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; + var botCountSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContentTop.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), botCountSettingHolder.RectTransform), TextManager.Get("BotCount"), wrap: true); var botCountContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), botCountSettingHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.05f, Stretch = true }; @@ -1178,10 +1164,10 @@ namespace Barotrauma return true; } }; - + botCountSettingHolder.RectTransform.MinSize = new Point(0, SeedBox.RectTransform.MinSize.Y); clientDisabledElements.AddRange(botCountButtons); - var botSpawnModeSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; + var botSpawnModeSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContentTop.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), botSpawnModeSettingHolder.RectTransform), TextManager.Get("BotSpawnMode"), wrap: true); var botSpawnModeContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), botSpawnModeSettingHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.05f, Stretch = true }; @@ -1205,23 +1191,108 @@ namespace Barotrauma } }; - List settingsElements = settingsContent.Children.ToList(); - for (int i = 0; i < settingsElements.Count; i++) - { - if (settingsElements[i].CountChildren > 0) - { - settingsElements[i].RectTransform.MinSize = new Point(0, Math.Max(settingsElements[i].RectTransform.Children.Max(c => c.Rect.Height), (int)(20 * GUI.Scale))); - } - } + clientDisabledElements.AddRange(botSpawnModeButtons); - settingsBlocker = new GUIFrame(new RectTransform(Vector2.One, settingsFrame.RectTransform), style: "InnerFrame") + settingsBlocker = new GUIFrame(new RectTransform(Vector2.One, settingsFrameTop.RectTransform), style: "InnerFrame") { - Color = Color.Black * 0.5f, + Color = Color.Black * 0.85f, IgnoreLayoutGroups = true, Visible = false }; + new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.3f), settingsBlocker.RectTransform, Anchor.Center), + TextManager.Get("settings.campaigndisabled"), wrap: true, style: "InnerFrame", textAlignment: Alignment.Center, textColor: GUIStyle.TextColorNormal); - clientDisabledElements.AddRange(botSpawnModeButtons); + //traitor probability ------------------------------------------------------------------ + + var traitorsProbHolder = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.6f), settingsContentBottom.RectTransform), style: null); + + var traitorProbLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), traitorsProbHolder.RectTransform), TextManager.Get("traitor.probability")); + + traitorProbabilitySlider = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.5f), traitorsProbHolder.RectTransform, Anchor.BottomCenter), style: "GUISlider", barSize: 0.2f) + { + Step = 0.05f, + Range = new Vector2(0.0f, 1.0f), + ToolTip = TextManager.Get("traitor.probability.tooltip"), + OnReleased = (scrollbar, value) => + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, traitorProbability: value); + return true; + } + }; + var traitorProbabilityText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), traitorProbLabel.RectTransform), "", textAlignment: Alignment.CenterRight) + { + ToolTip = TextManager.Get("traitor.probability.tooltip") + }; + traitorProbabilitySlider.OnMoved = (scrollbar, value) => + { + traitorProbabilityText.Text = TextManager.GetWithVariable("percentageformat", "[value]", ((int)Math.Round(scrollbar.BarScrollValue * 100)).ToString()); + traitorProbabilityText.TextColor = + value <= 0.0f ? + GUIStyle.Green : + ToolBox.GradientLerp(scrollbar.BarScroll, GUIStyle.Yellow, GUIStyle.Orange, GUIStyle.Red); + return true; + }; + + traitorElements.Clear(); + traitorElements.Add(traitorProbabilityText); + traitorElements.Add(traitorProbabilitySlider); + clientDisabledElements.Add(traitorProbabilitySlider); + + var traitorDangerHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.4f), settingsContentBottom.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true + }; + + new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), traitorDangerHolder.RectTransform), TextManager.Get("traitor.dangerlevelsetting"), wrap: true) + { + ToolTip = TextManager.Get("traitor.dangerlevelsetting.tooltip") + }; + + var traitorDangerContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), traitorDangerHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.05f, Stretch = true }; + var traitorDangerButtons = new GUIButton[2]; + traitorDangerButtons[0] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), traitorDangerContainer.RectTransform), style: "GUIButtonToggleLeft") + { + OnClicked = (button, obj) => + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, traitorDangerLevel: -1); + return true; + } + }; + + traitorDangerGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 1.0f), traitorDangerContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true, + AbsoluteSpacing = 1 + }; + for (int i = TraitorEventPrefab.MinDangerLevel; i <= TraitorEventPrefab.MaxDangerLevel; i++) + { + var difficultyColor = Mission.GetDifficultyColor(i); + new GUIImage(new RectTransform(new Vector2(0.75f), traitorDangerGroup.RectTransform), "DifficultyIndicator", scaleToFit: true) + { + ToolTip = + RichString.Rich( + $"‖color:{Color.White.ToStringHex()}‖{TextManager.Get($"traitor.dangerlevel.{i}")}‖color:end‖" + '\n' + + TextManager.Get($"traitor.dangerlevel.{i}.description")), + Color = difficultyColor, + DisabledColor = Color.Gray * 0.5f, + }; + } + + traitorDangerButtons[1] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), traitorDangerContainer.RectTransform), style: "GUIButtonToggleRight") + { + OnClicked = (button, obj) => + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, traitorDangerLevel: 1); + return true; + } + }; + + SetTraitorDangerIndicators(GameMain.Client?.ServerSettings.TraitorDangerLevel ?? TraitorEventPrefab.MinDangerLevel); + traitorElements.AddRange(traitorDangerButtons); + clientDisabledElements.AddRange(traitorDangerButtons); + + settingsContentTop.Recalculate(); + settingsContentBottom.Recalculate(); } public void StopWaitingForStartRound() @@ -1264,6 +1335,7 @@ namespace Barotrauma public override void Deselect() { + SaveAppearance(); chatInput.Deselect(); CampaignCharacterDiscarded = false; @@ -1338,34 +1410,35 @@ namespace Barotrauma public void RefreshEnabledElements() { - ServerName.Readonly = !GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - ServerMessage.Readonly = !GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - missionTypeList.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + bool manageSettings = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + + ServerName.Readonly = !manageSettings; + ServerMessage.Readonly = !manageSettings; + missionTypeList.Enabled = manageSettings; foreach (var tickBox in missionTypeTickBoxes) { - tickBox.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + tickBox.Enabled = manageSettings; } - SeedBox.Enabled = !CampaignFrame.Visible && !CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - levelDifficultyScrollBar.Enabled = !CampaignFrame.Visible && !CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + SeedBox.Enabled = !CampaignFrame.Visible && !CampaignSetupFrame.Visible && manageSettings; + levelDifficultyScrollBar.Enabled = !CampaignFrame.Visible && !CampaignSetupFrame.Visible && manageSettings; levelDifficultyScrollBar.ToolTip = string.Empty; if (!levelDifficultyScrollBar.Enabled) { levelDifficultyScrollBar.ToolTip = TextManager.Get("campaigndifficultydisabled"); } - traitorProbabilityButtons[0].Enabled = traitorProbabilityButtons[1].Enabled = traitorProbabilityText.Enabled = - !CampaignFrame.Visible && !CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - botCountButtons[0].Enabled = botCountButtons[1].Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - botSpawnModeButtons[0].Enabled = botSpawnModeButtons[1].Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + traitorElements.ForEach(e => e.Enabled = manageSettings); + botCountButtons[0].Enabled = botCountButtons[1].Enabled = manageSettings; + botSpawnModeButtons[0].Enabled = botSpawnModeButtons[1].Enabled = manageSettings; - autoRestartBox.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + autoRestartBox.Enabled = manageSettings; - SettingsButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + SettingsButton.Visible = manageSettings; SettingsButton.OnClicked = GameMain.Client.ServerSettings.ToggleSettingsFrame; StartButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ManageRound) && !GameMain.Client.GameStarted && !CampaignSetupFrame.Visible && !CampaignFrame.Visible; - ServerName.Readonly = !GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - ServerMessage.Readonly = !GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - shuttleTickBox.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings) && !GameMain.Client.GameStarted; + ServerName.Readonly = !manageSettings; + ServerMessage.Readonly = !manageSettings; + shuttleTickBox.Enabled = manageSettings && !GameMain.Client.GameStarted; SubList.Enabled = !CampaignFrame.Visible && (GameMain.Client.ServerSettings.AllowSubVoting || GameMain.Client.HasPermission(ClientPermissions.SelectSub)); ShuttleList.Enabled = ShuttleList.ButtonEnabled = GameMain.Client.HasPermission(ClientPermissions.SelectSub) && !GameMain.Client.GameStarted; ModeList.Enabled = !GameMain.Client.GameStarted && (GameMain.Client.ServerSettings.AllowModeVoting || GameMain.Client.HasPermission(ClientPermissions.SelectMode)); @@ -1375,7 +1448,7 @@ namespace Barotrauma roundControlsHolder.Children.ForEach(c => c.RectTransform.RelativeSize = Vector2.One); roundControlsHolder.Recalculate(); - SubVisibilityButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + SubVisibilityButton.Visible = manageSettings; ReadyToStartBox.Parent.Visible = !GameMain.Client.GameStarted; @@ -1415,6 +1488,7 @@ namespace Barotrauma public void CreatePlayerFrame(GUIComponent parent, bool createPendingText = true, bool alwaysAllowEditing = false) { + if (GameMain.Client == null) { return; } UpdatePlayerFrame( Character.Controlled?.Info ?? playerInfoContainer.Children?.First().UserData as CharacterInfo ?? GameMain.Client.CharacterInfo, allowEditing: alwaysAllowEditing || campaignCharacterInfo == null, @@ -1424,6 +1498,7 @@ namespace Barotrauma private void UpdatePlayerFrame(CharacterInfo characterInfo, bool allowEditing, GUIComponent parent, bool createPendingText = true) { + if (GameMain.Client == null) { return; } createPendingChangesText = createPendingText; if (characterInfo == null || CampaignCharacterDiscarded) { @@ -1447,7 +1522,6 @@ namespace Barotrauma bool nameChangePending = isGameRunning && GameMain.Client.PendingName != string.Empty && GameMain.Client?.Character?.Name != GameMain.Client.PendingName; changesPendingText = null; - if (TabMenu.PendingChanges) { CreateChangesPendingText(); @@ -1754,6 +1828,16 @@ namespace Barotrauma jobVariantTooltip.RectTransform.MinSize = new Point(0, content.RectTransform.Children.Sum(c => c.Rect.Height + content.AbsoluteSpacing)); } + private void SetTraitorDangerIndicators(int dangerLevel) + { + int i = 0; + foreach (var child in traitorDangerGroup.Children) + { + child.Enabled = i < dangerLevel; + i++; + } + } + public bool ToggleSpectate(GUITickBox tickBox) { SetSpectate(tickBox.Selected); @@ -2088,7 +2172,7 @@ namespace Barotrauma private Action DrawDownloadThrobber(Client client, params GUIComponent[] otherComponents) => (sb, c) => DrawDownloadThrobber(client, otherComponents, sb, c); //poor man's currying - private void DrawDownloadThrobber(Client client, GUIComponent[] otherComponents, SpriteBatch spriteBatch, GUICustomComponent component) + private static void DrawDownloadThrobber(Client client, GUIComponent[] otherComponents, SpriteBatch spriteBatch, GUICustomComponent component) { if (!client.IsDownloading) { @@ -2322,7 +2406,14 @@ namespace Barotrauma PlayerFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null) { - OnClicked = (btn, userdata) => { if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) ClosePlayerFrame(btn, userdata); return true; } + OnClicked = (btn, userdata) => + { + if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) + { + ClosePlayerFrame(btn, userdata); + } + return true; + } }; new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, PlayerFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); @@ -2412,7 +2503,7 @@ namespace Barotrauma //reset rank to custom rankDropDown.SelectItem(null); - if (!(PlayerFrame.UserData is Client client)) { return false; } + if (PlayerFrame.UserData is not Client client) { return false; } foreach (GUIComponent child in tickbox.Parent.GetChild().Content.Children) { @@ -2432,7 +2523,7 @@ namespace Barotrauma foreach (ClientPermissions permission in Enum.GetValues(typeof(ClientPermissions))) { - if (permission == ClientPermissions.None || permission == ClientPermissions.All) continue; + if (permission == ClientPermissions.None || permission == ClientPermissions.All) { continue; } var permissionTick = new GUITickBox(new RectTransform(new Vector2(0.15f, 0.15f), permissionsBox.Content.RectTransform), TextManager.Get("ClientPermission." + permission), font: GUIStyle.SmallFont) @@ -2445,7 +2536,7 @@ namespace Barotrauma //reset rank to custom rankDropDown.SelectItem(null); - if (!(PlayerFrame.UserData is Client client)) { return false; } + if (PlayerFrame.UserData is not Client client) { return false; } var thisPermission = (ClientPermissions)tickBox.UserData; if (tickBox.Selected) @@ -2480,7 +2571,7 @@ namespace Barotrauma //reset rank to custom rankDropDown.SelectItem(null); - if (!(PlayerFrame.UserData is Client client)) { return false; } + if (PlayerFrame.UserData is not Client client) { return false; } foreach (GUIComponent child in tickbox.Parent.GetChild().Content.Children) { @@ -2803,7 +2894,7 @@ namespace Barotrauma publicOrPrivate.RectTransform.NonScaledSize = (publicOrPrivate.Font.MeasureString(publicOrPrivate.Text) + new Vector2(25, 8) * GUI.Scale).ToPoint(); } - private void DrawJobVariantItems(SpriteBatch spriteBatch, GUICustomComponent component, JobVariant jobPrefab, int itemsPerRow) + private static void DrawJobVariantItems(SpriteBatch spriteBatch, GUICustomComponent component, JobVariant jobPrefab, int itemsPerRow) { var itemIdentifiers = jobPrefab.Prefab.PreviewItems[jobPrefab.Variant] .Where(it => it.ShowPreview) @@ -2938,28 +3029,22 @@ namespace Barotrauma { OnHeadSwitch = menu => { - StoreHead(true); UpdateJobPreferences(info); SelectAppearanceTab(button, _); - }, - OnSliderMoved = (bar, scroll) => - { - StoreHead(false); - return false; - }, - OnSliderReleased = SaveHead + } }; return false; } - private bool SaveHead(GUIScrollBar scrollBar, float barScroll) => StoreHead(true); - private bool StoreHead(bool save) + public bool SaveAppearance() { - var info = GameMain.Client.CharacterInfo; + var info = GameMain.Client?.CharacterInfo; + if (info?.Head == null) { return false; } var characterConfig = MultiplayerPreferences.Instance; - characterConfig.TagSet.Clear(); characterConfig.TagSet.UnionWith(info.Head.Preset.TagSet); + characterConfig.TagSet.Clear(); + characterConfig.TagSet.UnionWith(info.Head.Preset.TagSet); characterConfig.HairIndex = info.Head.HairIndex; characterConfig.BeardIndex = info.Head.BeardIndex; characterConfig.MoustacheIndex = info.Head.MoustacheIndex; @@ -2968,15 +3053,12 @@ namespace Barotrauma characterConfig.FacialHairColor = info.Head.FacialHairColor; characterConfig.SkinColor = info.Head.SkinColor; - if (save) + if (GameMain.GameSession?.IsRunning ?? false) { - if (GameMain.GameSession?.IsRunning ?? false) - { - TabMenu.PendingChanges = true; - CreateChangesPendingText(); - } - GameSettings.SaveCurrentConfig(); + TabMenu.PendingChanges = true; + CreateChangesPendingText(); } + GameSettings.SaveCurrentConfig(); return true; } @@ -3143,7 +3225,7 @@ namespace Barotrauma return true; } - private GUIImage[][] AddJobSpritesToGUIComponent(GUIComponent parent, JobPrefab jobPrefab, bool selectedByPlayer) + private static GUIImage[][] AddJobSpritesToGUIComponent(GUIComponent parent, JobPrefab jobPrefab, bool selectedByPlayer) { GUIFrame innerFrame = null; List outfitPreviews = jobPrefab.GetJobOutfitSprites(CharacterPrefab.HumanPrefab.CharacterInfoPrefab, useInventoryIcon: true, out var maxDimensions); @@ -3221,6 +3303,7 @@ namespace Barotrauma if ((prevMode == GameModePreset.PvP) != (SelectedMode == GameModePreset.PvP)) { + SaveAppearance(); UpdatePlayerFrame(null); GameMain.Client.ConnectedClients.ForEach(c => SetPlayerNameAndJobPreference(c)); } @@ -3363,7 +3446,11 @@ namespace Barotrauma if (!enabled) { //remove campaign character from the panel - if (campaignCharacterInfo != null) { UpdatePlayerFrame(null); } + if (campaignCharacterInfo != null) + { + UpdatePlayerFrame(null); + SetSpectate(spectateBox.Selected); + } campaignCharacterInfo = null; CampaignCharacterDiscarded = false; } @@ -3411,7 +3498,7 @@ namespace Barotrauma private bool ViewJobInfo(GUIButton button, object obj) { - if (!(button.UserData is JobVariant jobPrefab)) { return false; } + if (button.UserData is not JobVariant jobPrefab) { return false; } JobInfoFrame = jobPrefab.Prefab.CreateInfoFrame(out GUIComponent buttonContainer); GUIButton closeButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.05f), buttonContainer.RectTransform, Anchor.BottomRight), @@ -3541,7 +3628,7 @@ namespace Barotrauma } } - private GUIButton CreateJobVariantButton(JobVariant jobPrefab, int variantIndex, int variantCount, GUIComponent slot) + private static GUIButton CreateJobVariantButton(JobVariant jobPrefab, int variantIndex, int variantCount, GUIComponent slot) { float relativeSize = 0.15f; @@ -3631,9 +3718,13 @@ namespace Barotrauma } if (subList == SubList) + { FailedSelectedSub = null; + } else + { FailedSelectedShuttle = null; + } //hashes match, all good if (sub.MD5Hash?.StringRepresentation == md5Hash && SubmarineInfo.SavedSubmarines.Contains(sub)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs index a47aa5922..d9a3de1c8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs @@ -1047,7 +1047,7 @@ namespace Barotrauma } if (filterTraitorValue != TernaryOption.Any) { - if ((serverInfo.TraitorsEnabled == YesNoMaybe.Yes || serverInfo.TraitorsEnabled == YesNoMaybe.Maybe) != (filterTraitorValue == TernaryOption.Enabled)) + if ((serverInfo.TraitorProbability > 0.0f) != (filterTraitorValue == TernaryOption.Enabled)) { return false; } @@ -1829,8 +1829,6 @@ namespace Barotrauma { graphics.Clear(Color.CornflowerBlue); - GameMain.TitleScreen.DrawLoadingText = false; - spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); GameMain.MainMenuScreen.DrawBackground(graphics, spriteBatch); GUI.Draw(Cam, spriteBatch); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index a9b03c0ec..bbae3c7c7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -75,6 +75,7 @@ namespace Barotrauma NoCargoSpawnpoints, NoBallastTag, NonLinkedGaps, + NoHiddenContainers, StructureCount, WallCount, ItemCount, @@ -232,6 +233,8 @@ namespace Barotrauma public override Camera Cam => cam; + public bool DrawCharacterInventory => dummyCharacter != null && WiringMode; + public static XDocument AutoSaveInfo; private static readonly string autoSavePath = Path.Combine("Submarines", ".AutoSaves"); private static readonly string autoSaveInfoPath = Path.Combine(autoSavePath, "autosaves.xml"); @@ -583,7 +586,7 @@ namespace Barotrauma if (!(o is string layer)) { return false; } MapEntity.SelectedList.Clear(); - foreach (MapEntity entity in MapEntity.mapEntityList.Where(me => !me.Removed && me.Layer == layer)) + foreach (MapEntity entity in MapEntity.MapEntityList.Where(me => !me.Removed && me.Layer == layer)) { if (entity.IsSelected) { continue; } @@ -842,7 +845,7 @@ namespace Barotrauma var structureCount = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), structureCountText.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight); structureCount.TextGetter = () => { - int count = MapEntity.mapEntityList.Count - Item.ItemList.Count - Hull.HullList.Count - WayPoint.WayPointList.Count - Gap.GapList.Count; + int count = MapEntity.MapEntityList.Count - Item.ItemList.Count - Hull.HullList.Count - WayPoint.WayPointList.Count - Gap.GapList.Count; structureCount.TextColor = count > MaxStructures ? GUIStyle.Red : Color.Lerp(GUIStyle.Green, GUIStyle.Orange, count / (float)MaxStructures); return count.ToString(); }; @@ -1163,7 +1166,7 @@ namespace Barotrauma foreach (MapEntityPrefab ep in entityLists[categoryKey]) { #if !DEBUG - if (ep.HideInMenus && !GameMain.DebugDraw) { continue; } + if ((ep.HideInMenus || ep.HideInEditors) && !GameMain.DebugDraw) { continue; } #endif CreateEntityElement(ep, entitiesPerRow, entityListInner.Content); } @@ -1182,7 +1185,7 @@ namespace Barotrauma foreach (MapEntityPrefab ep in MapEntityPrefab.List) { #if !DEBUG - if (ep.HideInMenus && !GameMain.DebugDraw) { continue; } + if ((ep.HideInMenus || ep.HideInEditors) && !GameMain.DebugDraw) { continue; } #endif CreateEntityElement(ep, entitiesPerRow, allEntityList.Content); } @@ -1220,7 +1223,7 @@ namespace Barotrauma frame.Color = Color.Magenta; frame.ToolTip = $"{frame.ToolTip}\n‖color:{XMLExtensions.ToStringHex(Color.MediumPurple)}‖{ep.ContentPackage?.Name}‖color:end‖"; } - if (ep.HideInMenus) + if (ep.HideInMenus || ep.HideInEditors) { frame.Color = Color.Red; name = "[HIDDEN] " + name; @@ -1638,7 +1641,7 @@ namespace Barotrauma /// The saving is ran in another thread to avoid lag spikes private static void AutoSave() { - if (MapEntity.mapEntityList.Any() && GameSettings.CurrentConfig.EnableSubmarineAutoSave && !isAutoSaving) + if (MapEntity.MapEntityList.Any() && GameSettings.CurrentConfig.EnableSubmarineAutoSave && !isAutoSaving) { if (MainSub != null) { @@ -1939,7 +1942,7 @@ namespace Barotrauma var matchingFile = modProject.Files.FirstOrDefault(f => f.Type == subFileType && filePath.CleanUpPath().Equals(f.Path.CleanUpPath(), StringComparison.OrdinalIgnoreCase)); if (matchingFile != null) { - File.Delete(matchingFile.Path.Replace(ContentPath.ModDirStr, packageDir)); + File.Delete(matchingFile.Path.Replace(ContentPath.ModDirStr, packageDir, StringComparison.OrdinalIgnoreCase)); modProject.RemoveFile(matchingFile); } var newFile = ModProject.File.FromPath(filePath, subFileType); @@ -2852,7 +2855,7 @@ namespace Barotrauma { OnClicked = (button, o) => { - var requiredPackages = MapEntity.mapEntityList.Select(e => e?.Prefab?.ContentPackage) + var requiredPackages = MapEntity.MapEntityList.Select(e => e?.Prefab?.ContentPackage) .Where(cp => cp != null) .Distinct().OfType().Select(p => p.Name).ToHashSet(); var tickboxes = requiredContentPackList.Content.Children.OfType().ToArray(); @@ -3493,7 +3496,15 @@ namespace Barotrauma var ownerPackage = GetLocalPackageThatOwnsSub(selectedSubInfo); if (ownerPackage is null) { - if (GetWorkshopPackageThatOwnsSub(selectedSubInfo) is ContentPackage workshopPackage) + if (IsVanillaSub(selectedSubInfo)) + { +#if DEBUG + LoadSub(selectedSubInfo); +#else + AskLoadVanillaSub(selectedSubInfo); +#endif + } + else if (GetWorkshopPackageThatOwnsSub(selectedSubInfo) is ContentPackage workshopPackage) { if (workshopPackage.TryExtractSteamWorkshopId(out var workshopId) && publishedWorkshopItemIds.Contains(workshopId.Value)) @@ -3505,14 +3516,6 @@ namespace Barotrauma AskLoadSubscribedSub(selectedSubInfo); } } - else if (IsVanillaSub(selectedSubInfo)) - { -#if DEBUG - LoadSub(selectedSubInfo); -#else - AskLoadVanillaSub(selectedSubInfo); -#endif - } } else { @@ -3792,10 +3795,7 @@ namespace Barotrauma wiringModeTickBox.Selected = newMode == Mode.Wiring; lockMode = false; - foreach (MapEntity me in MapEntity.mapEntityList) - { - me.IsHighlighted = false; - } + MapEntity.ClearHighlightedEntities(); MapEntity.DeselectAll(); MapEntity.FilteredSelectedList.Clear(); @@ -3806,7 +3806,8 @@ namespace Barotrauma { var item = new Item(MapEntityPrefab.Find(null, "screwdriver") as ItemPrefab, Vector2.Zero, null); dummyCharacter.Inventory.TryPutItem(item, null, new List() { InvSlotType.RightHand }); - wiringToolPanel = CreateWiringPanel(); + Point wirePos = new Point((int)(10 * GUI.Scale), TopPanel.Rect.Height + entityCountPanel.Rect.Height + (int)(10 * GUI.Scale)); + wiringToolPanel = CreateWiringPanel(wirePos, SelectWire); } } @@ -3830,11 +3831,11 @@ namespace Barotrauma Item target = null; var single = targets.Count == 1 ? targets.Single() : null; - if (single is Item item && item.Components.Any(ic => !(ic is ConnectionPanel) && ic is not Repairable && ic.GuiFrame != null)) + if (single is Item item && item.Components.Any(static ic => ic is not ConnectionPanel && ic is not Repairable && ic.GuiFrame != null)) { // Do not offer the ability to open the inventory if the inventory should never be drawn - var container = item.GetComponent(); - if (container == null || container.DrawInventory) { target = item; } + var containers = item.GetComponents(); + if (containers.Any(static c => c.DrawInventory) || item.GetComponent() is not null) { target = item; } } bool hasTargets = targets.Count > 0; @@ -3850,7 +3851,7 @@ namespace Barotrauma new ContextMenuOption("Editor.SelectSame", isEnabled: hasTargets, onSelected: delegate { bool doorGapSelected = targets.Any(t => t is Gap gap && gap.ConnectedDoor != null); - foreach (MapEntity match in MapEntity.mapEntityList.Where(e => e.Prefab != null && targets.Any(t => t.Prefab?.Identifier == e.Prefab.Identifier) && !MapEntity.SelectedList.Contains(e))) + foreach (MapEntity match in MapEntity.MapEntityList.Where(e => e.Prefab != null && targets.Any(t => t.Prefab?.Identifier == e.Prefab.Identifier) && !MapEntity.SelectedList.Contains(e))) { if (MapEntity.SelectedList.Contains(match)) { continue; } if (match is Gap gap) @@ -3892,7 +3893,7 @@ namespace Barotrauma new ContextMenuOption("editor.layer.createlayer", isEnabled: hasTargets, onSelected: () => { CreateNewLayer(null, targets); }), new ContextMenuOption("editor.layer.selectall", isEnabled: hasTargets, onSelected: () => { - foreach (MapEntity match in MapEntity.mapEntityList.Where(e => targets.Any(t => !string.IsNullOrWhiteSpace(t.Layer) && t.Layer == e.Layer && !MapEntity.SelectedList.Contains(e)))) + foreach (MapEntity match in MapEntity.MapEntityList.Where(e => targets.Any(t => !string.IsNullOrWhiteSpace(t.Layer) && t.Layer == e.Layer && !MapEntity.SelectedList.Contains(e)))) { if (MapEntity.SelectedList.Contains(match)) { continue; } MapEntity.SelectedList.Add(match); @@ -3965,7 +3966,7 @@ namespace Barotrauma { Layers.Remove(original); - foreach (MapEntity entity in MapEntity.mapEntityList.Where(entity => entity.Layer == original)) + foreach (MapEntity entity in MapEntity.MapEntityList.Where(entity => entity.Layer == original)) { entity.Layer = newName ?? string.Empty; } @@ -3980,7 +3981,7 @@ namespace Barotrauma private void ReconstructLayers() { ClearLayers(); - foreach (MapEntity entity in MapEntity.mapEntityList) + foreach (MapEntity entity in MapEntity.MapEntityList) { if (!string.IsNullOrWhiteSpace(entity.Layer)) { @@ -4307,15 +4308,15 @@ namespace Barotrauma static string ColorToHex(Color color) => $"#{(color.R << 16 | color.G << 8 | color.B):X6}"; } - private GUIFrame CreateWiringPanel() + public static GUIFrame CreateWiringPanel(Point offset, GUIListBox.OnSelectedHandler onWireSelected) { GUIFrame frame = new GUIFrame(new RectTransform(new Vector2(0.03f, 0.35f), GUI.Canvas) - { MinSize = new Point(120, 300), AbsoluteOffset = new Point((int)(10 * GUI.Scale), TopPanel.Rect.Height + entityCountPanel.Rect.Height + (int)(10 * GUI.Scale)) }); + { MinSize = new Point(120, 300), AbsoluteOffset = offset }); GUIListBox listBox = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.9f), frame.RectTransform, Anchor.Center)) { PlaySoundOnSelect = true, - OnSelected = SelectWire, + OnSelected = onWireSelected, CanTakeKeyBoardFocus = false }; @@ -4323,12 +4324,14 @@ namespace Barotrauma foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) { - if (itemPrefab.Name.IsNullOrEmpty() || itemPrefab.HideInMenus) { continue; } - if (!itemPrefab.Tags.Contains("wire")) { continue; } + if (itemPrefab.Name.IsNullOrEmpty() || itemPrefab.HideInMenus || itemPrefab.HideInEditors) { continue; } + if (!itemPrefab.Tags.Contains(Tags.WireItem)) { continue; } + if (CircuitBox.IsInGame() && itemPrefab.Tags.Contains(Tags.Thalamus)) { continue; } + wirePrefabs.Add(itemPrefab); } - foreach (ItemPrefab itemPrefab in wirePrefabs.OrderBy(w => !w.CanBeBought).ThenBy(w => w.UintIdentifier)) + foreach (ItemPrefab itemPrefab in wirePrefabs.OrderBy(static w => !w.CanBeBought).ThenBy(static w => w.UintIdentifier)) { GUIFrame imgFrame = new GUIFrame(new RectTransform(new Point(listBox.Content.Rect.Width, listBox.Rect.Width / 2), listBox.Content.RectTransform), style: "ListBoxElement") { @@ -4393,7 +4396,7 @@ namespace Barotrauma { if (dummyCharacter == null || itemContainer == null) { return; } - if (((itemContainer.GetComponent() is { } holdable && !holdable.Attached) || itemContainer.GetComponent() != null) && itemContainer.GetComponent() != null) + if ((itemContainer.GetComponent() is { Attached: false } || itemContainer.GetComponent() != null) && itemContainer.GetComponent() != null) { // We teleport our dummy character to the item so it appears as the entity stays still when in reality the dummy is holding it oldItemPosition = itemContainer.SimPosition; @@ -4614,9 +4617,9 @@ namespace Barotrauma public void AutoHull() { - for (int i = 0; i < MapEntity.mapEntityList.Count; i++) + for (int i = 0; i < MapEntity.MapEntityList.Count; i++) { - MapEntity h = MapEntity.mapEntityList[i]; + MapEntity h = MapEntity.MapEntityList[i]; if (h is Hull || h is Gap) { h.Remove(); @@ -4629,7 +4632,7 @@ namespace Barotrauma List mapEntityList = new List(); - foreach (MapEntity e in MapEntity.mapEntityList) + foreach (MapEntity e in MapEntity.MapEntityList) { if (e is Item it) { @@ -5086,10 +5089,12 @@ namespace Barotrauma layerGroup.Recalculate(); - new GUITextBlock(new RectTransform(new Vector2(0.6f, 1f), layerGroup.RectTransform), layer, textAlignment: Alignment.CenterLeft) + var textBlock = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1f), layerGroup.RectTransform), layer, textAlignment: Alignment.CenterLeft); + if (textBlock.TextSize.X > textBlock.Rect.Width) { - CanBeFocused = false - }; + textBlock.ToolTip = textBlock.Text; + textBlock.Text = ToolBox.LimitString(textBlock.Text, textBlock.Font, textBlock.Rect.Width); + } layerGroup.Recalculate(); layerChainLayout.Recalculate(); @@ -5212,7 +5217,7 @@ namespace Barotrauma var highlightedEntities = new List(); // ReSharper disable once LoopCanBeConvertedToQuery - foreach (Item item in MapEntity.mapEntityList.Where(entity => entity is Item).Cast()) + foreach (Item item in MapEntity.MapEntityList.Where(entity => entity is Item).Cast()) { var wire = item.GetComponent(); if (wire == null || !wire.IsMouseOn()) { continue; } @@ -5225,8 +5230,9 @@ namespace Barotrauma hullVolumeFrame.Visible = MapEntity.SelectedList.Any(s => s is Hull); hullVolumeFrame.RectTransform.AbsoluteOffset = new Point(Math.Max(showEntitiesPanel.Rect.Right, previouslyUsedPanel.Rect.Right), 0); - saveAssemblyFrame.Visible = MapEntity.SelectedList.Count > 0 && !WiringMode; - snapToGridFrame.Visible = MapEntity.SelectedList.Count > 0 && !WiringMode; + bool isCircuitBoxOpened = dummyCharacter?.SelectedItem?.GetComponent() is not null; + saveAssemblyFrame.Visible = MapEntity.SelectedList.Count > 0 && !WiringMode && !isCircuitBoxOpened; + snapToGridFrame.Visible = MapEntity.SelectedList.Count > 0 && !WiringMode && !isCircuitBoxOpened; var offset = cam.WorldView.Top - cam.ScreenToWorld(new Vector2(0, GameMain.GraphicsHeight - EntityMenu.Rect.Top)).Y; @@ -5285,7 +5291,7 @@ namespace Barotrauma { if (wiringToolPanel.GetChild() is { } listBox) { - if (!dummyCharacter.HeldItems.Any(it => it.HasTag("wire"))) + if (!dummyCharacter.HeldItems.Any(it => it.HasTag(Tags.WireItem))) { listBox.Deselect(); } @@ -5394,7 +5400,7 @@ namespace Barotrauma } else { - var selectables = MapEntity.mapEntityList.Where(entity => entity.SelectableInEditor).ToList(); + var selectables = MapEntity.MapEntityList.Where(entity => entity.SelectableInEditor).ToList(); foreach (var item in Item.ItemList) { //attached wires are not normally selectable (by clicking), @@ -5418,7 +5424,7 @@ namespace Barotrauma } else { - cam.MoveCamera((float) deltaTime, allowMove: true, allowZoom: GUI.MouseOn == null); + cam.MoveCamera((float) deltaTime, allowMove: !CircuitBox.IsCircuitBoxSelected(dummyCharacter), allowZoom: GUI.MouseOn == null); } } else @@ -5426,7 +5432,7 @@ namespace Barotrauma cam.MoveCamera((float) deltaTime, allowMove: false, allowZoom: GUI.MouseOn == null); } - if (PlayerInput.MidButtonHeld()) + if (PlayerInput.MidButtonHeld() && !CircuitBox.IsCircuitBoxSelected(dummyCharacter)) { Vector2 moveSpeed = PlayerInput.MouseSpeed * (float)deltaTime * 60.0f / cam.Zoom; moveSpeed.X = -moveSpeed.X; @@ -5460,10 +5466,7 @@ namespace Barotrauma { if (WiringMode) { - foreach (MapEntity me in MapEntity.mapEntityList) - { - me.IsHighlighted = false; - } + MapEntity.ClearHighlightedEntities(); if (dummyCharacter.SelectedItem == null) { @@ -5948,13 +5951,10 @@ namespace Barotrauma } } - if (dummyCharacter != null) + if (DrawCharacterInventory) { - if (WiringMode) - { - dummyCharacter.DrawHUD(spriteBatch, cam, false); - wiringToolPanel.DrawManually(spriteBatch); - } + dummyCharacter.DrawHUD(spriteBatch, cam, false); + wiringToolPanel.DrawManually(spriteBatch); } MapEntity.DrawEditor(spriteBatch, cam); @@ -5991,10 +5991,7 @@ 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; - } + MapEntity.ClearHighlightedEntities(); var prevScissorRect = GameMain.Instance.GraphicsDevice.ScissorRectangle; @@ -6037,16 +6034,21 @@ namespace Barotrauma private void DrawGrid(SpriteBatch spriteBatch) { // don't render at high zoom levels because it would just turn the screen white - if (cam.Zoom < 0.5f || !ShouldDrawGrid) { return; } + if (!ShouldDrawGrid) { return; } var (gridX, gridY) = Submarine.GridSize; + DrawGrid(spriteBatch, cam, gridX, gridY, zoomTreshold: true); + } + public static void DrawGrid(SpriteBatch spriteBatch, Camera cam, float sizeX, float sizeY, bool zoomTreshold) + { + if (zoomTreshold && cam.Zoom < 0.5f) { return; } int scale = Math.Max(1, GUI.IntScale(1)); float zoom = cam.Zoom / 2f; // Don't ask float lineThickness = Math.Max(1, scale / zoom); Color gridColor = gridBaseColor; - if (cam.Zoom < 1.0f) + if (zoomTreshold && cam.Zoom < 1.0f) { // fade the grid when zooming out gridColor *= Math.Max(0, (cam.Zoom - 0.5f) * 2f); @@ -6054,18 +6056,66 @@ namespace Barotrauma Rectangle camRect = cam.WorldView; - for (float x = snapX(camRect.X); x < snapX(camRect.X + camRect.Width) + gridX; x += gridX) + for (float x = snapX(camRect.X); x < snapX(camRect.X + camRect.Width) + sizeX; x += sizeX) { spriteBatch.DrawLine(new Vector2(x, -camRect.Y), new Vector2(x, -(camRect.Y - camRect.Height)), gridColor, thickness: lineThickness); } - for (float y = snapY(camRect.Y); y >= snapY(camRect.Y - camRect.Height) - gridY; y -= Submarine.GridSize.Y) + for (float y = snapY(camRect.Y); y >= snapY(camRect.Y - camRect.Height) - sizeY; y -= sizeY) { spriteBatch.DrawLine(new Vector2(camRect.X, -y), new Vector2(camRect.Right, -y), gridColor, thickness: lineThickness); } - float snapX(int x) => (float) Math.Floor(x / gridX) * gridX; - float snapY(int y) => (float) Math.Ceiling(y / gridY) * gridY; + float snapX(int x) => (float) Math.Floor(x / sizeX) * sizeX; + float snapY(int y) => (float) Math.Ceiling(y / sizeY) * sizeY; + } + + public static void DrawOutOfBoundsArea(SpriteBatch spriteBatch, Camera cam, float playableAreaSize, Color color) + { + Rectangle camRect = cam.WorldView; + + RectangleF playableArea = new RectangleF( + -playableAreaSize / 2f, + -playableAreaSize / 2f, + playableAreaSize, + playableAreaSize + ); + + RectangleF topRect = new( + camRect.Left, + -camRect.Top, + camRect.Width, + playableArea.Top + camRect.Top + ); + + // idk why camRect.Bottom doesn't work here + float camRectBottom = -camRect.Top + camRect.Height; + + RectangleF bottomRect = new( + camRect.Left, + playableArea.Bottom, + camRect.Width, + camRectBottom + playableArea.Bottom + ); + + RectangleF rightRect = new( + playableArea.Right, + playableArea.Top, + camRect.Right - playableArea.Right, + playableArea.Height + ); + + RectangleF leftRect = new( + playableArea.Left, + playableArea.Top, + camRect.Left - playableArea.Left, + playableArea.Height + ); + + GUI.DrawFilledRectangle(spriteBatch, topRect, color); + GUI.DrawFilledRectangle(spriteBatch, leftRect, color); + GUI.DrawFilledRectangle(spriteBatch, rightRect, color); + GUI.DrawFilledRectangle(spriteBatch, bottomRect, color); } public void SaveScreenShot(int width, int height, string filePath) @@ -6116,7 +6166,7 @@ namespace Barotrauma public static ImmutableHashSet GetEntitiesInSameLayer(MapEntity entity) { if (string.IsNullOrWhiteSpace(entity.Layer)) { return ImmutableHashSet.Empty; } - return MapEntity.mapEntityList.Where(me => me.Layer == entity.Layer).ToImmutableHashSet(); + return MapEntity.MapEntityList.Where(me => me.Layer == entity.Layer).ToImmutableHashSet(); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs index 7cf2b716b..115402131 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs @@ -45,7 +45,7 @@ namespace Barotrauma base.Select(); if (dummyCharacter is { Removed: false }) { - dummyCharacter?.Remove(); + dummyCharacter.Remove(); } dummyCharacter = Character.Create(CharacterPrefab.HumanSpeciesName, Vector2.Zero, "", id: Entity.DummyID, hasAi: false); @@ -54,15 +54,11 @@ namespace Barotrauma dummyCharacter.Inventory.CreateSlots(); dummyCharacter.Info.GiveExperience(999999); - miniMapItem = new Item(ItemPrefab.Find(null, "deconstructor".ToIdentifier()), Vector2.Zero, null, 1337, false); + miniMapItem = new Item(ItemPrefab.Find(null, "circuitbox".ToIdentifier()), Vector2.Zero, null, 1337, false); + miniMapItem.GetComponent().AttachToWall(); - foreach (ItemComponent component in miniMapItem.Components) - { - component.OnItemLoaded(); - } Character.Controlled = dummyCharacter; GameMain.World.ProcessChanges(); - TabMenu = new TabMenu(); } public override void AddToGUIUpdateList() @@ -78,29 +74,30 @@ namespace Barotrauma base.Update(deltaTime); TabMenu?.Update((float)deltaTime); - // if (dummyCharacter is { } dummy && miniMapItem is { } item) - // { - // if (dummy.SelectedConstruction != item) - // { - // dummy.SelectedConstruction = item; - // } - // - // dummy.SelectedConstruction?.UpdateHUD(Cam, dummy, (float)deltaTime); - // Vector2 pos = FarseerPhysics.ConvertUnits.ToSimUnits(item.Position); - // - // foreach (Limb limb in dummy.AnimController.Limbs) - // { - // limb.body.SetTransform(pos, 0.0f); - // } - // - // if (dummy.AnimController?.Collider is { } collider) - // { - // collider.SetTransform(pos, 0); - // } - // - // dummy.ControlLocalPlayer((float)deltaTime, Cam, false); - // dummy.Control((float)deltaTime, Cam); - // } + if (dummyCharacter is { } dummy && miniMapItem is { } item) + { + if (dummy.SelectedItem != item) + { + dummy.SelectedItem = item; + } + + dummy.SelectedItem?.UpdateHUD(Cam, dummy, (float)deltaTime); + item.SendSignal("1", "signal_in1"); + Vector2 pos = FarseerPhysics.ConvertUnits.ToSimUnits(item.Position); + + foreach (Limb limb in dummy.AnimController.Limbs) + { + limb.body.SetTransform(pos, 0.0f); + } + + if (dummy.AnimController?.Collider is { } collider) + { + collider.SetTransform(pos, 0); + } + + dummy.ControlLocalPlayer((float)deltaTime, Cam, false); + dummy.Control((float)deltaTime, Cam); + } } public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index 44ba1c654..8cd9300f5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -390,7 +390,7 @@ namespace Barotrauma } if (toolTip.IsNullOrEmpty()) { - toolTip = TextManager.Get($"{propertyTag}.description", $"sp.{fallbackTag}.description"); + toolTip = TextManager.Get($"{propertyTag}.description", $"{fallbackTag}.description", $"sp.{fallbackTag}.description"); } if (toolTip.IsNullOrEmpty()) { @@ -1334,9 +1334,11 @@ namespace Barotrauma } } } - + private static void TrySendNetworkUpdate(ISerializableEntity entity, SerializableProperty property) { + if (IsEntityRemoved(entity)) { return; } + if (GameMain.Client != null) { if (entity is Item item) @@ -1352,7 +1354,7 @@ namespace Barotrauma private bool SetPropertyValue(SerializableProperty property, object entity, object value) { - if (LockEditing) { return false; } + if (LockEditing || IsEntityRemoved(entity)) { return false; } object oldData = property.GetValue(entity); // some properties have null as the default string value @@ -1403,6 +1405,9 @@ namespace Barotrauma return property.TrySetValue(entity, value); } + public static bool IsEntityRemoved(object entity) + => entity is Entity { Removed: true } or ItemComponent { Item.Removed: true }; + public static void CommitCommandBuffer() { if (CommandBuffer != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs index 3214a3ff5..4d443268b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs @@ -734,10 +734,16 @@ namespace Barotrauma { OnSelected = tickBox => { + GUIMessageBox? loadingBox = null; + if (!tickBox.Selected) + { + loadingBox = GUIMessageBox.CreateLoadingBox(TextManager.Get("PleaseWait")); + } GameAnalyticsManager.SetConsent( tickBox.Selected ? GameAnalyticsManager.Consent.Ask - : GameAnalyticsManager.Consent.No); + : GameAnalyticsManager.Consent.No, + onAnswerSent: () => loadingBox?.Close()); return false; } }; @@ -766,7 +772,7 @@ namespace Barotrauma statisticsTickBox.OnSelected = null; statisticsTickBox.Selected = shouldTickBoxBeSelected; statisticsTickBox.OnSelected = prevHandler; - statisticsTickBox.Enabled = GameAnalyticsManager.UserConsented != GameAnalyticsManager.Consent.Error; + statisticsTickBox.Enabled &= GameAnalyticsManager.UserConsented != GameAnalyticsManager.Consent.Error; }); #endif } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs index 1f1dca9e1..f816a71a8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs @@ -2,21 +2,89 @@ using OpenAL; using NVorbis; using System.Collections.Generic; +using System.Threading.Tasks; using System.Xml.Linq; namespace Barotrauma.Sounds { - class OggSound : Sound + sealed class OggSound : Sound { - private VorbisReader reader; + private VorbisReader streamReader; - //key = sample rate, value = filter - private static readonly Dictionary muffleFilters = new Dictionary(); - - private static List playbackAmplitude; + private 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) { } + private short[] sampleBuffer = Array.Empty(); + private short[] muffleBuffer = Array.Empty(); + public OggSound(SoundManager owner, string filename, bool stream, XElement xElement) : base(owner, filename, + stream, true, xElement) + { + var reader = new VorbisReader(Filename); + + ALFormat = reader.Channels == 1 ? Al.FormatMono16 : Al.FormatStereo16; + SampleRate = reader.SampleRate; + + if (stream) + { + streamReader = reader; + return; + } + + TaskPool.Add( + $"LoadSamples {filename}", + LoadSamples(reader), + t => + { + reader.Dispose(); + if (!t.TryGetResult(out TaskResult result)) + { + return; + } + sampleBuffer = result.SampleBuffer; + muffleBuffer = result.MuffleBuffer; + playbackAmplitude = result.PlaybackAmplitude; + Owner.KillChannels(this); // prevents INVALID_OPERATION error + buffers?.Dispose(); buffers = null; + }); + } + + private readonly record struct TaskResult( + short[] SampleBuffer, + short[] MuffleBuffer, + List PlaybackAmplitude); + + private static async Task LoadSamples(VorbisReader reader) + { + reader.DecodedPosition = 0; + + int bufferSize = (int)reader.TotalSamples * reader.Channels; + + float[] floatBuffer = new float[bufferSize]; + var sampleBuffer = new short[bufferSize]; + var muffledBuffer = new short[bufferSize]; + + int readSamples = await Task.Run(() => reader.ReadSamples(floatBuffer, 0, bufferSize)); + + var 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, sampleBuffer, readSamples); + + MuffleBuffer(floatBuffer, reader.SampleRate); + + CastBuffer(floatBuffer, muffledBuffer, readSamples); + + return new TaskResult(sampleBuffer, muffledBuffer, playbackAmplitude); + } public override float GetAmplitudeAtPlaybackPos(int playbackPos) { @@ -27,101 +95,65 @@ namespace Barotrauma.Sounds return playbackAmplitude[index]; } + private float[] streamFloatBuffer = null; public override int FillStreamBuffer(int samplePos, short[] buffer) { if (!Stream) { throw new Exception("Called FillStreamBuffer on a non-streamed sound!"); } - if (reader == null) { throw new Exception("Called FillStreamBuffer when the reader is null!"); } + if (streamReader == null) { throw new Exception("Called FillStreamBuffer when the reader is null!"); } - if (samplePos >= reader.TotalSamples * reader.Channels * 2) return 0; + if (samplePos >= streamReader.TotalSamples * streamReader.Channels * 2) return 0; - samplePos /= reader.Channels * 2; - reader.DecodedPosition = samplePos; + samplePos /= streamReader.Channels * 2; + streamReader.DecodedPosition = samplePos; - float[] floatBuffer = new float[buffer.Length]; - int readSamples = reader.ReadSamples(floatBuffer, 0, buffer.Length / 2); + if (streamFloatBuffer is null || streamFloatBuffer.Length < buffer.Length) + { + streamFloatBuffer = new float[buffer.Length]; + } + int readSamples = streamReader.ReadSamples(streamFloatBuffer, 0, buffer.Length); //MuffleBuffer(floatBuffer, reader.Channels); - CastBuffer(floatBuffer, buffer, readSamples); + CastBuffer(streamFloatBuffer, buffer, readSamples); return readSamples; } - static void MuffleBuffer(float[] buffer, int sampleRate, int channelCount) + static void MuffleBuffer(float[] buffer, int sampleRate) { - if (!muffleFilters.TryGetValue(sampleRate, out BiQuad filter)) - { - filter = new LowpassFilter(sampleRate, 1600); - muffleFilters.Add(sampleRate, filter); - } + var filter = new LowpassFilter(sampleRate, 1600); filter.Process(buffer); } - public override void InitializeALBuffers() + public override void InitializeAlBuffers() { - base.InitializeALBuffers(); - - reader ??= new VorbisReader(Filename); - - ALFormat = reader.Channels == 1 ? Al.FormatMono16 : Al.FormatStereo16; - SampleRate = reader.SampleRate; - - if (Buffers != null && SoundBuffers.BuffersGenerated < SoundBuffers.MaxBuffers) + if (buffers != null && SoundBuffers.BuffersGenerated < SoundBuffers.MaxBuffers) { - Buffers.RequestAlBuffers(); FillBuffers(); + FillAlBuffers(); } } - public override void FillBuffers() + public override void FillAlBuffers() { - if (!Stream) + if (Stream) { return; } + if (sampleBuffer.Length == 0 || muffleBuffer.Length == 0) { return; } + buffers ??= new SoundBuffers(this); + if (!buffers.RequestAlBuffers()) { return; } + + Al.BufferData(buffers.AlBuffer, ALFormat, sampleBuffer, + sampleBuffer.Length * sizeof(short), SampleRate); + + int alError = Al.GetError(); + if (alError != Al.NoError) { - reader ??= new VorbisReader(Filename); + throw new Exception("Failed to set regular buffer data for non-streamed audio! " + Al.GetErrorString(alError)); + } - reader.DecodedPosition = 0; + Al.BufferData(buffers.AlMuffledBuffer, ALFormat, muffleBuffer, + muffleBuffer.Length * sizeof(short), SampleRate); - 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(Buffers.AlBuffer, ALFormat, shortBuffer, - readSamples * sizeof(short), SampleRate); - - int alError = Al.GetError(); - if (alError != Al.NoError) - { - throw new Exception("Failed to set regular buffer data for non-streamed audio! " + Al.GetErrorString(alError)); - } - - MuffleBuffer(floatBuffer, SampleRate, reader.Channels); - - CastBuffer(floatBuffer, shortBuffer, readSamples); - - Al.BufferData(Buffers.AlMuffledBuffer, ALFormat, shortBuffer, - readSamples * sizeof(short), SampleRate); - - alError = Al.GetError(); - if (alError != Al.NoError) - { - throw new Exception("Failed to set muffled buffer data for non-streamed audio! " + Al.GetErrorString(alError)); - } - - reader.Dispose(); reader = null; + alError = Al.GetError(); + if (alError != Al.NoError) + { + throw new Exception("Failed to set muffled buffer data for non-streamed audio! " + Al.GetErrorString(alError)); } } @@ -129,7 +161,7 @@ namespace Barotrauma.Sounds { if (Stream) { - reader?.Dispose(); + streamReader?.Dispose(); } base.Dispose(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs index d911d4ac2..8d2ea80c6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs @@ -33,7 +33,7 @@ namespace Barotrauma.Sounds } } - private SoundBuffers buffers; + protected SoundBuffers buffers; public SoundBuffers Buffers { get { return !Stream ? buffers : null; } @@ -72,8 +72,6 @@ namespace Barotrauma.Sounds BaseGain = 1.0f; BaseNear = 100.0f; BaseFar = 200.0f; - - InitializeALBuffers(); } public override string ToString() @@ -141,21 +139,11 @@ namespace Barotrauma.Sounds public abstract float GetAmplitudeAtPlaybackPos(int playbackPos); - public virtual void InitializeALBuffers() - { - if (!Stream) - { - buffers = new SoundBuffers(this); - } - else - { - buffers = null; - } - } + public virtual void InitializeAlBuffers() { } - public virtual void FillBuffers() { } + public virtual void FillAlBuffers() { } - public virtual void DeleteALBuffers() + public virtual void DeleteAlBuffers() { Owner.KillChannels(this); buffers?.Dispose(); @@ -165,7 +153,7 @@ namespace Barotrauma.Sounds { if (disposed) { return; } - DeleteALBuffers(); + DeleteAlBuffers(); Owner.RemoveSound(this); disposed = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundBuffer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundBuffer.cs index cacd1121a..8282375a9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundBuffer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundBuffer.cs @@ -7,7 +7,7 @@ using System.Text; namespace Barotrauma.Sounds { - class SoundBuffers : IDisposable + sealed class SoundBuffers : IDisposable { private static readonly HashSet bufferPool = new HashSet(); #if OSX diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs index 6f5b2efac..b08ed5862 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs @@ -312,54 +312,52 @@ namespace Barotrauma.Sounds if (!IsPlaying) { return; } - if (!IsStream) + if (IsStream) { return; } + + uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); + Al.GetSourcei(alSource, Al.SampleOffset, out int playbackPos); + int alError = Al.GetError(); + if (alError != Al.NoError) { - uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); - Al.GetSourcei(alSource, Al.SampleOffset, out int playbackPos); - int alError = Al.GetError(); - if (alError != Al.NoError) - { - DebugConsole.ThrowError("Failed to get source's playback position: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); - return; - } + DebugConsole.ThrowError("Failed to get source's playback position: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); + return; + } - Al.SourceStop(alSource); + Al.SourceStop(alSource); - alError = Al.GetError(); - if (alError != Al.NoError) - { - DebugConsole.ThrowError("Failed to stop source: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); - return; - } + alError = Al.GetError(); + if (alError != Al.NoError) + { + DebugConsole.ThrowError("Failed to stop source: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); + return; + } - if (Sound.Buffers.RequestAlBuffers()) - { - Sound.FillBuffers(); - } - Al.Sourcei(alSource, Al.Buffer, muffled ? (int)Sound.Buffers.AlMuffledBuffer : (int)Sound.Buffers.AlBuffer); + Sound.FillAlBuffers(); + if (Sound.Buffers is not { AlBuffer: not 0, AlMuffledBuffer: not 0 }) { return; } - alError = Al.GetError(); - if (alError != Al.NoError) - { - DebugConsole.ThrowError("Failed to bind buffer to source: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); - return; - } + Al.Sourcei(alSource, Al.Buffer, muffled ? (int)Sound.Buffers.AlMuffledBuffer : (int)Sound.Buffers.AlBuffer); - Al.SourcePlay(alSource); - alError = Al.GetError(); - if (alError != Al.NoError) - { - DebugConsole.ThrowError("Failed to replay source: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); - return; - } + alError = Al.GetError(); + if (alError != Al.NoError) + { + DebugConsole.ThrowError("Failed to bind buffer to source: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); + return; + } - Al.Sourcei(alSource, Al.SampleOffset, playbackPos); - alError = Al.GetError(); - if (alError != Al.NoError) - { - DebugConsole.ThrowError("Failed to reset playback position: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); - return; - } + Al.SourcePlay(alSource); + alError = Al.GetError(); + if (alError != Al.NoError) + { + DebugConsole.ThrowError("Failed to replay source: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); + return; + } + + Al.Sourcei(alSource, Al.SampleOffset, playbackPos); + alError = Al.GetError(); + if (alError != Al.NoError) + { + DebugConsole.ThrowError("Failed to reset playback position: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); + return; } } } @@ -509,10 +507,8 @@ namespace Barotrauma.Sounds throw new Exception("Failed to reset source buffer: " + debugName + ", " + Al.GetErrorString(alError)); } - if (Sound.Buffers.RequestAlBuffers()) - { - Sound.FillBuffers(); - } + Sound.FillAlBuffers(); + if (Sound.Buffers is not { AlBuffer: not 0, AlMuffledBuffer: not 0 }) { return; } uint alBuffer = sound.Owner.GetCategoryMuffle(category) || muffled ? Sound.Buffers.AlMuffledBuffer : Sound.Buffers.AlBuffer; Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, (int)alBuffer); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundFilters.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundFilters.cs index e234f0dc2..ed57ef47b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundFilters.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundFilters.cs @@ -64,14 +64,15 @@ namespace Barotrauma.Sounds /// The b2 value. /// protected double B2; + /// /// The q value. /// - private double _q; + protected readonly double _q; /// /// The gain value in dB. /// - private double _gainDB; + protected readonly double _gainDB; /// /// The z1 value. /// @@ -81,77 +82,15 @@ namespace Barotrauma.Sounds /// protected double Z2; - private double _frequency; - - /// - /// Gets or sets the frequency. - /// - /// value;The samplerate has to be bigger than 2 * frequency. - public double Frequency - { - get { return _frequency; } - set - { - if (SampleRate < value * 2) - { - throw new ArgumentOutOfRangeException("value", "The samplerate has to be bigger than 2 * frequency."); - } - _frequency = value; - CalculateBiQuadCoefficients(); - } - } + protected readonly double _frequency; /// /// Gets the sample rate. /// - public int SampleRate { get; private set; } + protected readonly int _sampleRate; - /// - /// The q value. - /// - public double Q - { - get { return _q; } - set - { - if (value <= 0) - { - throw new ArgumentOutOfRangeException("value"); - } - _q = value; - CalculateBiQuadCoefficients(); - } - } - - /// - /// Gets or sets the gain value in dB. - /// - public double GainDB - { - get { return _gainDB; } - set - { - _gainDB = value; - CalculateBiQuadCoefficients(); - } - } - - /// - /// Initializes a new instance of the class. - /// - /// The sample rate. - /// The frequency. - /// - /// sampleRate - /// or - /// frequency - /// or - /// q - /// - protected BiQuad(int sampleRate, double frequency) - : this(sampleRate, frequency, 1.0 / Math.Sqrt(2)) - { - } + protected static readonly double DefaultQ = 1.0 / Math.Sqrt(2); + protected const double DefaultGainDb = 6.0; /// /// Initializes a new instance of the class. @@ -166,18 +105,29 @@ namespace Barotrauma.Sounds /// or /// q /// - protected BiQuad(int sampleRate, double frequency, double q) + protected BiQuad(int sampleRate, double frequency, double q, double gainDb) { if (sampleRate <= 0) + { throw new ArgumentOutOfRangeException("sampleRate"); + } if (frequency <= 0) + { throw new ArgumentOutOfRangeException("frequency"); + } if (q <= 0) + { throw new ArgumentOutOfRangeException("q"); - SampleRate = sampleRate; - Frequency = frequency; - Q = q; - GainDB = 6; + } + if (sampleRate < frequency * 2) + { + throw new ArgumentOutOfRangeException("sampleRate", "The sample rate has to be greater than or equal to 2 * frequency."); + } + _sampleRate = sampleRate; + _frequency = frequency; + _q = q; + _gainDB = gainDb; + CalculateBiQuadCoefficients(); } /// @@ -215,7 +165,7 @@ namespace Barotrauma.Sounds /// /// Used to apply a lowpass-filter to a signal. /// - public class LowpassFilter : BiQuad + public sealed class LowpassFilter : BiQuad { /// /// Initializes a new instance of the class. @@ -223,7 +173,7 @@ namespace Barotrauma.Sounds /// The sample rate. /// The filter's corner frequency. public LowpassFilter(int sampleRate, double frequency) - : base(sampleRate, frequency) + : base(sampleRate, frequency, DefaultQ, DefaultGainDb) { } @@ -232,20 +182,20 @@ namespace Barotrauma.Sounds /// protected override void CalculateBiQuadCoefficients() { - double k = Math.Tan(Math.PI * Frequency / SampleRate); - var norm = 1 / (1 + k / Q + k * k); + double k = Math.Tan(Math.PI * _frequency / _sampleRate); + var norm = 1 / (1 + k / _q + k * k); A0 = k * k * norm; A1 = 2 * A0; A2 = A0; B1 = 2 * (k * k - 1) * norm; - B2 = (1 - k / Q + k * k) * norm; + B2 = (1 - k / _q + k * k) * norm; } } /// /// Used to apply a highpass-filter to a signal. /// - public class HighpassFilter : BiQuad + public sealed class HighpassFilter : BiQuad { /// /// Initializes a new instance of the class. @@ -253,7 +203,7 @@ namespace Barotrauma.Sounds /// The sample rate. /// The filter's corner frequency. public HighpassFilter(int sampleRate, double frequency) - : base(sampleRate, frequency) + : base(sampleRate, frequency, DefaultQ, DefaultGainDb) { } @@ -262,20 +212,20 @@ namespace Barotrauma.Sounds /// protected override void CalculateBiQuadCoefficients() { - double k = Math.Tan(Math.PI * Frequency / SampleRate); - var norm = 1 / (1 + k / Q + k * k); + double k = Math.Tan(Math.PI * _frequency / _sampleRate); + var norm = 1 / (1 + k / _q + k * k); A0 = 1 * norm; A1 = -2 * A0; A2 = A0; B1 = 2 * (k * k - 1) * norm; - B2 = (1 - k / Q + k * k) * norm; + B2 = (1 - k / _q + k * k) * norm; } } /// /// Used to apply a bandpass-filter to a signal. /// - public class BandpassFilter : BiQuad + public sealed class BandpassFilter : BiQuad { /// /// Initializes a new instance of the class. @@ -283,7 +233,7 @@ namespace Barotrauma.Sounds /// The sample rate. /// The filter's corner frequency. public BandpassFilter(int sampleRate, double frequency) - : base(sampleRate, frequency) + : base(sampleRate, frequency, DefaultQ, DefaultGainDb) { } @@ -292,20 +242,20 @@ namespace Barotrauma.Sounds /// protected override void CalculateBiQuadCoefficients() { - double k = Math.Tan(Math.PI * Frequency / SampleRate); - double norm = 1 / (1 + k / Q + k * k); - A0 = k / Q * norm; + double k = Math.Tan(Math.PI * _frequency / _sampleRate); + double norm = 1 / (1 + k / _q + k * k); + A0 = k / _q * norm; A1 = 0; A2 = -A0; B1 = 2 * (k * k - 1) * norm; - B2 = (1 - k / Q + k * k) * norm; + B2 = (1 - k / _q + k * k) * norm; } } /// /// Used to apply a notch-filter to a signal. /// - public class NotchFilter : BiQuad + public sealed class NotchFilter : BiQuad { /// /// Initializes a new instance of the class. @@ -313,7 +263,7 @@ namespace Barotrauma.Sounds /// The sample rate. /// The filter's corner frequency. public NotchFilter(int sampleRate, double frequency) - : base(sampleRate, frequency) + : base(sampleRate, frequency, DefaultQ, DefaultGainDb) { } @@ -322,20 +272,20 @@ namespace Barotrauma.Sounds /// protected override void CalculateBiQuadCoefficients() { - double k = Math.Tan(Math.PI * Frequency / SampleRate); - double norm = 1 / (1 + k / Q + k * k); + double k = Math.Tan(Math.PI * _frequency / _sampleRate); + double norm = 1 / (1 + k / _q + k * k); A0 = (1 + k * k) * norm; A1 = 2 * (k * k - 1) * norm; A2 = A0; B1 = A1; - B2 = (1 - k / Q + k * k) * norm; + B2 = (1 - k / _q + k * k) * norm; } } /// /// Used to apply a lowshelf-filter to a signal. /// - public class LowShelfFilter : BiQuad + public sealed class LowShelfFilter : BiQuad { /// /// Initializes a new instance of the class. @@ -344,10 +294,8 @@ namespace Barotrauma.Sounds /// The filter's corner frequency. /// Gain value in dB. public LowShelfFilter(int sampleRate, double frequency, double gainDB) - : base(sampleRate, frequency) - { - GainDB = gainDB; - } + : base(sampleRate, frequency, DefaultQ, gainDB) + { } /// /// Calculates all coefficients. @@ -355,10 +303,10 @@ namespace Barotrauma.Sounds protected override void CalculateBiQuadCoefficients() { const double sqrt2 = 1.4142135623730951; - double k = Math.Tan(Math.PI * Frequency / SampleRate); - double v = Math.Pow(10, Math.Abs(GainDB) / 20.0); + double k = Math.Tan(Math.PI * _frequency / _sampleRate); + double v = Math.Pow(10, Math.Abs(_gainDB) / 20.0); double norm; - if (GainDB >= 0) + if (_gainDB >= 0) { // boost norm = 1 / (1 + sqrt2 * k + k * k); A0 = (1 + Math.Sqrt(2 * v) * k + v * k * k) * norm; @@ -382,7 +330,7 @@ namespace Barotrauma.Sounds /// /// Used to apply a highshelf-filter to a signal. /// - public class HighShelfFilter : BiQuad + public sealed class HighShelfFilter : BiQuad { /// /// Initializes a new instance of the class. @@ -391,10 +339,8 @@ namespace Barotrauma.Sounds /// The filter's corner frequency. /// Gain value in dB. public HighShelfFilter(int sampleRate, double frequency, double gainDB) - : base(sampleRate, frequency) - { - GainDB = gainDB; - } + : base(sampleRate, frequency, DefaultQ, gainDB) + { } /// /// Calculates all coefficients. @@ -402,10 +348,10 @@ namespace Barotrauma.Sounds protected override void CalculateBiQuadCoefficients() { const double sqrt2 = 1.4142135623730951; - double k = Math.Tan(Math.PI * Frequency / SampleRate); - double v = Math.Pow(10, Math.Abs(GainDB) / 20.0); + double k = Math.Tan(Math.PI * _frequency / _sampleRate); + double v = Math.Pow(10, Math.Abs(_gainDB) / 20.0); double norm; - if (GainDB >= 0) + if (_gainDB >= 0) { // boost norm = 1 / (1 + sqrt2 * k + k * k); A0 = (v + Math.Sqrt(2 * v) * k + k * k) * norm; @@ -429,22 +375,8 @@ namespace Barotrauma.Sounds /// /// Used to apply an peak-filter to a signal. /// - public class PeakFilter : BiQuad + public sealed class PeakFilter : BiQuad { - /// - /// Gets or sets the bandwidth. - /// - public double BandWidth - { - get { return Q; } - set - { - if (value <= 0) - throw new ArgumentOutOfRangeException("value"); - Q = value; - } - } - /// /// Initializes a new instance of the class. /// @@ -453,10 +385,8 @@ namespace Barotrauma.Sounds /// The bandWidth. /// The gain value in dB. public PeakFilter(int sampleRate, double frequency, double bandWidth, double peakGainDB) - : base(sampleRate, frequency, bandWidth) - { - GainDB = peakGainDB; - } + : base(sampleRate, frequency, bandWidth, peakGainDB) + { } /// /// Calculates all coefficients. @@ -464,11 +394,11 @@ namespace Barotrauma.Sounds protected override void CalculateBiQuadCoefficients() { double norm; - double v = Math.Pow(10, Math.Abs(GainDB) / 20.0); - double k = Math.Tan(Math.PI * Frequency / SampleRate); - double q = Q; + double v = Math.Pow(10, Math.Abs(_gainDB) / 20.0); + double k = Math.Tan(Math.PI * _frequency / _sampleRate); + double q = _q; - if (GainDB >= 0) //boost + if (_gainDB >= 0) //boost { norm = 1 / (1 + 1 / q * k + k * k); A0 = (1 + v / q * k + k * k) * norm; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs index 575e822eb..e6cefeb2d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs @@ -812,7 +812,7 @@ namespace Barotrauma.Sounds { for (int i = loadedSounds.Count - 1; i >= 0; i--) { - loadedSounds[i].InitializeALBuffers(); + loadedSounds[i].InitializeAlBuffers(); } } @@ -834,7 +834,7 @@ namespace Barotrauma.Sounds { if (keepSounds) { - loadedSounds[i].DeleteALBuffers(); + loadedSounds[i].DeleteAlBuffers(); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index f53f49be0..e04b5a60f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -156,6 +156,12 @@ namespace Barotrauma insideSubFactor = 1.0f; } + if (Character.Controlled != null && Character.Controlled.PressureTimer > 0.0f && !Character.Controlled.IsDead) + { + //make the sound lerp to the "outside" sound when under pressure + insideSubFactor -= Character.Controlled.PressureTimer / 100.0f; + } + movementSoundVolume = Math.Max(movementSoundVolume, movementFactor); if (!MathUtils.IsValid(movementSoundVolume)) { @@ -183,7 +189,7 @@ namespace Barotrauma if (chn is null || !chn.IsPlaying) { if (volume < 0.01f) { return; } - if (!(chn is null)) { waterAmbienceChannels.Remove(chn); } + if (chn is not null) { waterAmbienceChannels.Remove(chn); } chn = sound.Play(volume, "waterambience"); chn.Looping = true; waterAmbienceChannels.Add(chn); @@ -195,6 +201,15 @@ namespace Barotrauma { chn.FadeOutAndDispose(); } + if (Character.Controlled != null && Character.Controlled.PressureTimer > 0.0f && !Character.Controlled.IsDead) + { + //make the sound decrease in pitch when under pressure + chn.FrequencyMultiplier = MathHelper.Clamp(Character.Controlled.PressureTimer / 200.0f, 0.75f, 1.0f); + } + else + { + chn.FrequencyMultiplier = Math.Min(chn.frequencyMultiplier + deltaTime, 1.0f); + } } } @@ -652,6 +667,7 @@ namespace Barotrauma } } + LogCurrentMusic(); updateMusicTimer = UpdateMusicInterval; } @@ -715,6 +731,26 @@ namespace Barotrauma } } + private static double lastMusicLogTime; + const double MusicLogInterval = 60.0; + private static void LogCurrentMusic() + { + if (Screen.Selected != GameMain.GameScreen) { return; } + if (Timing.TotalTime < lastMusicLogTime + MusicLogInterval) { return; } + for (int i = 0; i < musicChannel.Length; i++) + { + if (musicChannel[i] != null && + musicChannel[i].IsPlaying && + musicChannel[i].Sound?.Filename != null) + { + GameAnalyticsManager.AddDesignEvent( + "BackgroundMusic:" + + Path.GetFileNameWithoutExtension(musicChannel[i].Sound.Filename.Replace(":", string.Empty).Replace(" ", string.Empty))); + } + } + lastMusicLogTime = Timing.TotalTime; + } + private static void DisposeMusicChannel(int index) { var clip = musicClips.FirstOrDefault(m => m.Sound == musicChannel[index]?.Sound); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs index 82c294c6e..e599e9c41 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs @@ -10,6 +10,45 @@ namespace Barotrauma { public partial class Sprite { + public Identifier Identifier { get; private set; } + public static IEnumerable LoadedSprites + { + get + { + List retVal = null; + lock (list) + { + retVal = list.Select(wRef => + { + if (wRef.TryGetTarget(out Sprite spr)) + { + return spr; + } + return null; + }).Where(s => s != null).ToList(); + } + return retVal; + } + } + + private static readonly List> list = new List>(); + + static partial void AddToList(Sprite elem) + { + lock (list) + { + list.Add(new WeakReference(elem)); + } + } + + static partial void RemoveFromList(Sprite sprite) + { + lock (list) + { + list.RemoveAll(wRef => !wRef.TryGetTarget(out Sprite s) || s == sprite); + } + } + private class TextureRefCounter { public Texture2D Texture; @@ -130,6 +169,11 @@ namespace Barotrauma public void ReloadTexture() { var oldTexture = texture; + if (texture == null) + { + DebugConsole.ThrowError("Sprite: Failed to reload the texture, texture is null."); + return; + } texture.Dispose(); texture = TextureLoader.FromFile(FilePath.Value, Compress); Identifier pathKey = FullPath.ToIdentifier(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs index 831abe5b3..2748b691c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs @@ -1,5 +1,6 @@ using Barotrauma.Networking; using System; +using System.Globalization; using System.Linq; using System.Threading.Tasks; @@ -114,7 +115,7 @@ namespace Barotrauma.Steam currentLobby?.SetData("allowrespawn", serverSettings.AllowRespawn.ToString()); currentLobby?.SetData("karmaenabled", serverSettings.KarmaEnabled.ToString()); currentLobby?.SetData("friendlyfireenabled", serverSettings.AllowFriendlyFire.ToString()); - currentLobby?.SetData("traitors", serverSettings.TraitorsEnabled.ToString()); + currentLobby?.SetData("traitors", serverSettings.TraitorProbability.ToString(CultureInfo.InvariantCulture)); currentLobby?.SetData("gamestarted", GameMain.Client.GameStarted.ToString()); currentLobby?.SetData("playstyle", serverSettings.PlayStyle.ToString()); currentLobby?.SetData("gamemode", GameMain.NetLobbyScreen?.SelectedMode?.Identifier.Value ?? ""); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs index 918437918..44506e13a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs @@ -69,7 +69,7 @@ namespace Barotrauma.Steam //Steamworks is completely insane so the following needs comments: //This callback seems to take place when the item in question has not been downloaded recently - Steamworks.SteamUGC.GlobalOnItemInstalled = id => Workshop.OnItemDownloadComplete(id); + Steamworks.SteamUGC.OnItemInstalled += (appId, itemId) => Workshop.OnItemDownloadComplete(itemId); //This callback seems to take place when the item has been downloaded recently and an update //or a redownload has taken place diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs index a04a3e1bf..b7a66a8b8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs @@ -379,7 +379,7 @@ namespace Barotrauma.Steam private IEnumerable MessageBoxCoroutine(Func> subcoroutine) { - var messageBox = new GUIMessageBox("", "...", buttons: new [] { TextManager.Get("Cancel") }); + var messageBox = new GUIMessageBox("", TextManager.Get("ellipsis"), buttons: new [] { TextManager.Get("Cancel") }); messageBox.Buttons[0].OnClicked = (button, o) => { messageBox.Close(); @@ -528,10 +528,18 @@ namespace Barotrauma.Steam yield return new WaitForSeconds(0.5f); } - if (!resultItem.IsInstalled) + //there seems to sometimes be a brief delay between the download task and the item being installed, wait a bit before deeming the install as failed + DateTime waitInstallUntil = DateTime.Now + new TimeSpan(0, 0, seconds: 30); + while (!resultItem.IsInstalled || resultItem.IsDownloading) { - throw new Exception($"Failed to install item: download task ended with status {downloadTask.Status}, " + - $"exception was {downloadTask.Exception?.GetInnermost()?.ToString().CleanupStackTrace() ?? "[NULL]"}"); + if (DateTime.Now > waitInstallUntil) + { + throw new Exception($"Failed to install item: download task ended with status {downloadTask.Status}," + + $" item installed: {resultItem.IsInstalled}, " + + $" item downloading: {resultItem.IsDownloading}, " + + $"exception was {downloadTask.Exception?.GetInnermost()?.ToString().CleanupStackTrace() ?? "[NULL]"}"); + } + yield return new WaitForSeconds(0.5f); } ContentPackage? pkgToNuke diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs index fac6cd8cf..b7890da55 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs @@ -115,7 +115,7 @@ namespace Barotrauma.Steam var searchBox = new GUITextBox(new RectTransform(Vector2.One, searchHolder.RectTransform), "", createClearButton: true); var searchTitle = new GUITextBlock(new RectTransform(Vector2.One, searchHolder.RectTransform) {Anchor = Anchor.TopLeft}, textColor: Color.DarkGray * 0.6f, - text: TextManager.Get("Search") + "...", + text: TextManager.Get("Search") + TextManager.Get("ellipsis"), textAlignment: Alignment.CenterLeft) { CanBeFocused = false diff --git a/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorManager.cs new file mode 100644 index 000000000..3bad00872 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorManager.cs @@ -0,0 +1,22 @@ +#nullable enable + +using Barotrauma.Networking; + +namespace Barotrauma +{ + sealed partial class TraitorManager + { + public static void ClientRead(IReadMessage msg) + { + //unused, but could be worth keeping in the messages regardless in case a mod wants to use these for something + TraitorEvent.State state = (TraitorEvent.State)msg.ReadByte(); + Identifier eventIdentifier = msg.ReadIdentifier(); + if (GameMain.Client?.Character == null) + { + DebugConsole.ThrowError("Received a traitor update when not controlling a character."); + return; + } + GameMain.Client.Character.IsTraitor = true; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionPrefab.cs deleted file mode 100644 index c427adf4b..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionPrefab.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Microsoft.Xna.Framework; -using System; -using System.Collections.Generic; -using System.Text; -using System.Xml.Linq; - -namespace Barotrauma -{ - class TraitorMissionPrefab : Prefab - { - public static readonly PrefabCollection Prefabs = new PrefabCollection(); - - public readonly Sprite Icon; - public readonly Color IconColor; - - public TraitorMissionPrefab(ContentXElement element, TraitorMissionsFile file) : base(file, element.GetAttributeIdentifier("identifier", Identifier.Empty)) - { - foreach (var subElement in element.Elements()) - { - if (subElement.Name.ToString().Equals("icon", StringComparison.OrdinalIgnoreCase)) - { - Icon = new Sprite(subElement); - IconColor = subElement.GetAttributeColor("color", Color.White); - } - } - } - - public override void Dispose() - { - Icon?.Remove(); - } - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionResult.cs b/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionResult.cs deleted file mode 100644 index eaea08977..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionResult.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Barotrauma.Networking; -using System; - -namespace Barotrauma -{ - partial class TraitorMissionResult - { - public TraitorMissionResult(IReadMessage inc) - { - MissionIdentifier = inc.ReadIdentifier(); - 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/Utils/SpreadsheetExport.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs index 67bc81568..63473abd1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs @@ -94,8 +94,7 @@ namespace Barotrauma if (name.Equals(nameof(StatusEffect), StringComparison.OrdinalIgnoreCase)) { StatusEffect statusEffect = StatusEffect.Load(subElement, debugIdentifier); - if (statusEffect == null || !statusEffect.HasTag("medical")) { continue; } - + if (statusEffect == null || !statusEffect.HasTag(Tags.MedicalItem)) { continue; } statusEffects.Add(statusEffect); } else if (IsRequiredSkill(subElement, out Skill? skill) && skill != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs index 0ba972b3f..1fcbab5fb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs @@ -1,8 +1,7 @@ -using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; -using Barotrauma.IO; using System.Threading.Tasks; +using Barotrauma.IO; using Lidgren.Network; using Color = Microsoft.Xna.Framework.Color; @@ -45,155 +44,117 @@ namespace Barotrauma private static byte[] CompressDxt5(byte[] data, int width, int height) { - using (System.IO.MemoryStream mstream = new System.IO.MemoryStream()) - { - for (int y = 0; y < height; y += 4) + var output = new byte[width * height]; + Parallel.For( + fromInclusive: 0, + toExclusive: width * height / 16, + i => { - for (int x = 0; x < width; x += 4) - { - int offset = x * 4 + y * 4 * width; - CompressDxt5Block(data, offset, width, mstream); - } - } - return mstream.ToArray(); - } + int i4 = i * 4; + int inputOffset = (i4 % width + (i4 / width) * 4 * width) * 4; + int outputOffset = i * 16; + CompressDxt5Block(data, inputOffset, width, output, outputOffset); + }); + return output; } - private static void CompressDxt5Block(byte[] data, int offset, int width, System.IO.Stream output) + private static void CompressDxt5Block(byte[] data, int inputOffset, int width, byte[] output, int outputOffset) { int r1 = 255, g1 = 255, b1 = 255, a1 = 255; int r2 = 0, g2 = 0, b2 = 0, a2 = 0; - //determine the two colors to interpolate between: - //color 1 represents lowest luma, color 2 represents highest luma + // Determine the two colors to interpolate between: + // color 1 represents lowest luma, color 2 represents highest luma. + // Luma is also used to determine which color on the palette + // most closely resembles each pixel to compress, so we + // cache our calculations here. + int y1 = 255000; + int y2 = 0; for (int i = 0; i < 16; i++) { - int pixelOffset = offset + (4 * ((i % 4) + (width * (i >> 2)))); - int r, g, b, a; - r = data[pixelOffset + 0]; - g = data[pixelOffset + 1]; - b = data[pixelOffset + 2]; - a = data[pixelOffset + 3]; - if (r * 299 + g * 587 + b * 114 < r1 * 299 + g1 * 587 + b1 * 114) + int pixelOffset = inputOffset + (4 * ((i % 4) + (width * (i >> 2)))); + int r = data[pixelOffset + 0]; + int g = data[pixelOffset + 1]; + int b = data[pixelOffset + 2]; + int a = data[pixelOffset + 3]; + int y = r * 299 + g * 587 + b * 114; + if (y < y1) { - r1 = r; g1 = g; b1 = b; + r1 = r; g1 = g; b1 = b; y1 = y; } - if (r * 299 + g * 587 + b * 114 > r2 * 299 + g2 * 587 + b2 * 114) + if (y > y2) { - r2 = r; g2 = g; b2 = b; + r2 = r; g2 = g; b2 = b; y2 = y; } if (a < a1) { a1 = a; } if (a > a2) { a2 = a; } } //convert the colors to rgb565 (16-bit rgb) - int r1_565 = (r1 * 0x1f) / 0xff; if (r1_565 > 0x1f) { r1_565 = 0x1f; } - int g1_565 = (g1 * 0x3f) / 0xff; if (g1_565 > 0x3f) { g1_565 = 0x3f; } - int b1_565 = (b1 * 0x1f) / 0xff; if (b1_565 > 0x1f) { b1_565 = 0x1f; } + int r1_565 = r1 >> (8 - 5); + int g1_565 = g1 >> (8 - 6); + int b1_565 = b1 >> (8 - 5); - int r2_565 = (r2 * 0x1f) / 0xff; if (r2_565 > 0x1f) { r2_565 = 0x1f; } - int g2_565 = (g2 * 0x3f) / 0xff; if (g2_565 > 0x3f) { g2_565 = 0x3f; } - int b2_565 = (b2 * 0x1f) / 0xff; if (b2_565 > 0x1f) { b2_565 = 0x1f; } + int r2_565 = r2 >> (8 - 5); + int g2_565 = g2 >> (8 - 6); + int b2_565 = b2 >> (8 - 5); - //luma is also used to determine which color on the palette - //most closely resembles each pixel to compress, so we - //calculate this here - int y1 = r1 * 299 + g1 * 587 + b1 * 114; - int y2 = r2 * 299 + g2 * 587 + b2 * 114; - - byte[] newData = new byte[16]; - for (int i = 0; i < 16; i++) + int y2y1Diff = y2 - y1; + if (y2y1Diff > 0 || a1 < a2) { - int pixelOffset = offset + (4 * ((i % 4) + (width * (i >> 2)))); - int r, g, b, a; - r = data[pixelOffset + 0]; - g = data[pixelOffset + 1]; - b = data[pixelOffset + 2]; - a = data[pixelOffset + 3]; - - if (a1 < a2) + for (int i = 0; i < 16; i++) { - a -= a1; - a = (a * 0x7) / (a2 - a1); - if (a > 0x7) { a = 0x7; } + int pixelOffset = inputOffset + (4 * ((i % 4) + (width * (i >> 2)))); + int r = data[pixelOffset + 0]; + int g = data[pixelOffset + 1]; + int b = data[pixelOffset + 2]; - switch (a) + if (a1 < a2) { - case 0: - a = 1; - break; - case 1: - a = 7; - break; - case 2: - a = 6; - break; - case 3: - a = 5; - break; - case 4: - a = 4; - break; - case 5: - a = 3; - break; - case 6: - a = 2; - break; - case 7: - a = 0; - break; + int a = data[pixelOffset + 3]; + a -= a1; + a = (a * 0x7) / (a2 - a1); + if (a < 0x7) + { + a = a switch + { + 0 => 1, + 1 => 7, + _ => 8 - a + }; + NetBitWriter.WriteByte((byte)a, 3, output, (outputOffset * 8) + 16 + (i * 3)); + } } - } - else - { - a = 0; - } - NetBitWriter.WriteUInt32((uint)a, 3, newData, 16 + (i * 3)); + if (y2y1Diff <= 0) { continue; } - int y = r * 299 + g * 587 + b * 114; - - int max = y2 - y1; - int diffY = y - y1; - - int paletteIndex; - if (diffY < max / 4) - { - paletteIndex = 0; + int y = r * 299 + g * 587 + b * 114; + int diffY = y - y1; + int paletteIndex = (diffY * 4) / y2y1Diff; + paletteIndex = paletteIndex switch + { + 0 => 0, + 1 => 2, + 2 => 3, + _ => 1 + }; + output[outputOffset + 12 + (i / 4)] |= (byte)(paletteIndex << (2 * (i % 4))); } - else if (diffY < max / 2) - { - paletteIndex = 2; - } - else if (diffY < max * 3 / 4) - { - paletteIndex = 3; - } - else - { - paletteIndex = 1; - } - newData[12 + (i / 4)] |= (byte)(paletteIndex << (2 * (i % 4))); } - newData[0] = (byte)a2; - newData[1] = (byte)a1; + output[outputOffset + 0] = (byte)a2; + output[outputOffset + 1] = (byte)a1; - newData[9] = (byte)((r1_565 << 3) | (g1_565 >> 3)); - newData[8] = (byte)((g1_565 << 5) | b1_565); - newData[11] = (byte)((r2_565 << 3) | (g2_565 >> 3)); - newData[10] = (byte)((g2_565 << 5) | b2_565); - - output.Write(newData, 0, 16); + output[outputOffset + 9] = (byte)((r1_565 << 3) | (g1_565 >> 3)); + output[outputOffset + 8] = (byte)((g1_565 << 5) | b1_565); + output[outputOffset + 11] = (byte)((r2_565 << 3) | (g2_565 >> 3)); + output[outputOffset + 10] = (byte)((g2_565 << 5) | b2_565); } public static Texture2D FromFile(string path, bool compress = true, bool mipmap = false) { - using (FileStream fileStream = File.OpenRead(path)) - { - return FromStream(fileStream, path, compress, mipmap); - } + using FileStream fileStream = File.OpenRead(path); + return FromStream(fileStream, path, compress, mipmap); } public static Texture2D FromStream(System.IO.Stream stream, string path = null, bool compress = true, bool mipmap = false) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs index c5342a37c..c89d0cbfb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs @@ -142,10 +142,10 @@ namespace Barotrauma GameMain.Instance.GraphicsDevice.SetRenderTarget(rt); GameMain.Instance.GraphicsDevice.Clear(Color.Transparent); - DrawBatch(() => Submarine.DrawBack(spriteBatch, true, e => e is Structure s && (e.SpriteDepth >= 0.9f || s.Prefab.BackgroundSprite != null))); - DrawBatch(() => Submarine.DrawBack(spriteBatch, true, e => (e is not Structure || e.SpriteDepth < 0.9f))); - DrawBatch(() => Submarine.DrawDamageable(spriteBatch, null, editing: true)); - DrawBatch(() => Submarine.DrawFront(spriteBatch, editing: true)); + DrawBatch(() => Submarine.DrawBack(spriteBatch, editing: false, e => e is Structure s && (e.SpriteDepth >= 0.9f || s.Prefab.BackgroundSprite != null))); + DrawBatch(() => Submarine.DrawBack(spriteBatch, editing: false, e => (e is not Structure || e.SpriteDepth < 0.9f))); + DrawBatch(() => Submarine.DrawDamageable(spriteBatch, null, editing: false)); + DrawBatch(() => Submarine.DrawFront(spriteBatch, editing: false)); void DrawBatch(Action drawAction) { diff --git a/Barotrauma/BarotraumaClient/Content/Effects/wearableclip.xnb b/Barotrauma/BarotraumaClient/Content/Effects/wearableclip.xnb index 9951e2186b56992ef5c477c02a0f176eb5820b01..47c98fc61d39a2668da9eb84be21a61e5c5ea698 100644 GIT binary patch delta 50 zcmV-20L}mK67Uj`l?12k9y*bk9uPH4c6n+a delta 50 zcmV-20L}mK67Uj`l>|ld-inc#9uT+V8AE4HiQW-1X!nPaXFjpVY5@UNvoZn&0|His IvpEGf1VYRc#sB~S diff --git a/Barotrauma/BarotraumaClient/Content/Effects/wearableclip_opengl.xnb b/Barotrauma/BarotraumaClient/Content/Effects/wearableclip_opengl.xnb index 0426d9e092d61a1cf5eac89bb2cbcd1ffc89a00b..e09186ade6f28719606114b3a375b33101e4d6d7 100644 GIT binary patch delta 25 fcmX>jbVg{xRF?3$PgOR~T*b^%0R)@fSy)*Cj>ZYb delta 25 fcmX>jbVg{xRF)ZwDp@zqT*b^%2?U$nSy)*Ci#iE~ diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 4b5dccfa1..aa20c1261 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.0.20.1 + 1.1.14.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma @@ -62,6 +62,7 @@ + diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 6d8354723..75a4a7c97 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.0.20.1 + 1.1.14.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma @@ -64,6 +64,7 @@ + @@ -134,18 +135,13 @@ - - - - libsteam_api64.dylib - PreserveNewest - - PreserveNewest + + diff --git a/Barotrauma/BarotraumaClient/Shaders/wearableclip.fx b/Barotrauma/BarotraumaClient/Shaders/wearableclip.fx index 6d08d4460..d6a3e31ff 100644 --- a/Barotrauma/BarotraumaClient/Shaders/wearableclip.fx +++ b/Barotrauma/BarotraumaClient/Shaders/wearableclip.fx @@ -1,50 +1,50 @@ - -Texture2D xTexture; -sampler TextureSampler : register (s0) = sampler_state { Texture = ; }; - -Texture2D xStencil; -sampler StencilSampler = sampler_state { Texture = ; }; - -float aCutoff; -float4x4 wearableUvToClipperUv; -float clipperTexelSize; - -float2 stencilUVmin, stencilUVmax; - -float stencilSample(float2 texCoord, float2 offset) -{ - return xStencil.Sample( - StencilSampler, - mul(float4(texCoord.x, texCoord.y, 0, 1), wearableUvToClipperUv).xy + offset).a; -} - -float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 -{ - float4 c = xTexture.Sample(TextureSampler, texCoord) * color; - - float2 stencilUV = mul(float4(texCoord.x, texCoord.y, 0, 1), wearableUvToClipperUv).xy; - clip(stencilUV.x - stencilUVmin.x); - clip(stencilUV.y - stencilUVmin.y); - clip(stencilUVmax.y - stencilUV.x); - clip(stencilUVmax.y - stencilUV.y); - - float minStencil = stencilSample(texCoord, float2(0,0)); - minStencil = min(minStencil, stencilSample(texCoord, float2(-clipperTexelSize,0))); - minStencil = min(minStencil, stencilSample(texCoord, float2(clipperTexelSize,0))); - minStencil = min(minStencil, stencilSample(texCoord, float2(0,-clipperTexelSize))); - minStencil = min(minStencil, stencilSample(texCoord, float2(0,clipperTexelSize))); - - float aDiff = minStencil - aCutoff; - - clip(aDiff); - - return c; -} - -technique StencilShader -{ - pass Pass1 - { - PixelShader = compile ps_4_0_level_9_1 main(); - } -} + +Texture2D xTexture; +sampler TextureSampler : register (s0) = sampler_state { Texture = ; }; + +Texture2D xStencil; +sampler StencilSampler = sampler_state { Texture = ; }; + +float aCutoff; +float4x4 wearableUvToClipperUv; +float clipperTexelSize; + +float2 stencilUVmin, stencilUVmax; + +float stencilSample(float2 texCoord, float2 offset) +{ + return xStencil.Sample( + StencilSampler, + mul(float4(texCoord.x, texCoord.y, 0, 1), wearableUvToClipperUv).xy + offset).a; +} + +float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +{ + float4 c = xTexture.Sample(TextureSampler, texCoord) * color; + + float2 stencilUV = mul(float4(texCoord.x, texCoord.y, 0, 1), wearableUvToClipperUv).xy; + clip(stencilUV.x - stencilUVmin.x); + clip(stencilUV.y - stencilUVmin.y); + clip(stencilUVmax.x - stencilUV.x); + clip(stencilUVmax.y - stencilUV.y); + + float minStencil = stencilSample(texCoord, float2(0,0)); + minStencil = min(minStencil, stencilSample(texCoord, float2(-clipperTexelSize,0))); + minStencil = min(minStencil, stencilSample(texCoord, float2(clipperTexelSize,0))); + minStencil = min(minStencil, stencilSample(texCoord, float2(0,-clipperTexelSize))); + minStencil = min(minStencil, stencilSample(texCoord, float2(0,clipperTexelSize))); + + float aDiff = minStencil - aCutoff; + + clip(aDiff); + + return c; +} + +technique StencilShader +{ + pass Pass1 + { + PixelShader = compile ps_4_0_level_9_1 main(); + } +} diff --git a/Barotrauma/BarotraumaClient/Shaders/wearableclip_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/wearableclip_opengl.fx index d845e79d3..72b633bee 100644 --- a/Barotrauma/BarotraumaClient/Shaders/wearableclip_opengl.fx +++ b/Barotrauma/BarotraumaClient/Shaders/wearableclip_opengl.fx @@ -1,50 +1,50 @@ - -Texture2D xTexture; -sampler TextureSampler : register (s0) = sampler_state { Texture = ; }; - -Texture2D xStencil; -sampler StencilSampler = sampler_state { Texture = ; }; - -float aCutoff; -float4x4 wearableUvToClipperUv; -float clipperTexelSize; - -float2 stencilUVmin, stencilUVmax; - -float stencilSample(float2 texCoord, float2 offset) -{ - return tex2D( - StencilSampler, - mul(float4(texCoord.x, texCoord.y, 0, 1), wearableUvToClipperUv).xy + offset).a; -} - -float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 -{ - float4 c = tex2D(TextureSampler, texCoord) * color; - - float2 stencilUV = mul(float4(texCoord.x, texCoord.y, 0, 1), wearableUvToClipperUv).xy; - clip(stencilUV.x - stencilUVmin.x); - clip(stencilUV.y - stencilUVmin.y); - clip(stencilUVmax.y - stencilUV.x); - clip(stencilUVmax.y - stencilUV.y); - - float minStencil = stencilSample(texCoord, float2(0,0)); - minStencil = min(minStencil, stencilSample(texCoord, float2(-clipperTexelSize,0))); - minStencil = min(minStencil, stencilSample(texCoord, float2(clipperTexelSize,0))); - minStencil = min(minStencil, stencilSample(texCoord, float2(0,-clipperTexelSize))); - minStencil = min(minStencil, stencilSample(texCoord, float2(0,clipperTexelSize))); - - float aDiff = minStencil - aCutoff; - - clip(aDiff); - - return c; -} - -technique StencilShader -{ - pass Pass1 - { - PixelShader = compile ps_2_0 main(); - } -} + +Texture2D xTexture; +sampler TextureSampler : register (s0) = sampler_state { Texture = ; }; + +Texture2D xStencil; +sampler StencilSampler = sampler_state { Texture = ; }; + +float aCutoff; +float4x4 wearableUvToClipperUv; +float clipperTexelSize; + +float2 stencilUVmin, stencilUVmax; + +float stencilSample(float2 texCoord, float2 offset) +{ + return tex2D( + StencilSampler, + mul(float4(texCoord.x, texCoord.y, 0, 1), wearableUvToClipperUv).xy + offset).a; +} + +float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +{ + float4 c = tex2D(TextureSampler, texCoord) * color; + + float2 stencilUV = mul(float4(texCoord.x, texCoord.y, 0, 1), wearableUvToClipperUv).xy; + clip(stencilUV.x - stencilUVmin.x); + clip(stencilUV.y - stencilUVmin.y); + clip(stencilUVmax.x - stencilUV.x); + clip(stencilUVmax.y - stencilUV.y); + + float minStencil = stencilSample(texCoord, float2(0,0)); + minStencil = min(minStencil, stencilSample(texCoord, float2(-clipperTexelSize,0))); + minStencil = min(minStencil, stencilSample(texCoord, float2(clipperTexelSize,0))); + minStencil = min(minStencil, stencilSample(texCoord, float2(0,-clipperTexelSize))); + minStencil = min(minStencil, stencilSample(texCoord, float2(0,clipperTexelSize))); + + float aDiff = minStencil - aCutoff; + + clip(aDiff); + + return c; +} + +technique StencilShader +{ + pass Pass1 + { + PixelShader = compile ps_2_0 main(); + } +} diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index cec27c949..77b4d8ebb 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.0.20.1 + 1.1.14.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma @@ -70,6 +70,7 @@ + diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index f1b4c157c..5246e818e 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.0.20.1 + 1.1.14.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer @@ -56,6 +56,7 @@ + diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 592c084d0..1959e3b21 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.0.20.1 + 1.1.14.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer @@ -60,6 +60,7 @@ + @@ -87,12 +88,6 @@ - - - libsteam_api64.dylib - PreserveNewest - - diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index c597f599e..d61c718ca 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -376,10 +376,9 @@ namespace Barotrauma tempBuffer.WriteBoolean(aiming); tempBuffer.WriteBoolean(shoot); tempBuffer.WriteBoolean(use); - if (AnimController is HumanoidAnimController) - { - tempBuffer.WriteBoolean(((HumanoidAnimController)AnimController).Crouching); - } + + tempBuffer.WriteBoolean(AnimController is HumanoidAnimController { Crouching: true }); + tempBuffer.WriteBoolean(attack); Vector2 relativeCursorPos = cursorPosition - AimRefPosition; @@ -430,21 +429,31 @@ namespace Barotrauma if (writeStatus) { WriteStatus(tempBuffer); - AIController?.ServerWrite(tempBuffer); + tempBuffer.WriteBoolean(AIController is EnemyAIController); + if (AIController is EnemyAIController enemyAi) + { + tempBuffer.WriteByte((byte)enemyAi.State); + tempBuffer.WriteBoolean(enemyAi.PetBehavior is PetBehavior); + if (enemyAi.PetBehavior is PetBehavior petBehavior) + { + tempBuffer.WriteByte((byte)((petBehavior.Happiness / petBehavior.MaxHappiness) * byte.MaxValue)); + tempBuffer.WriteByte((byte)((petBehavior.Hunger / petBehavior.MaxHunger) * byte.MaxValue)); + } + } HealthUpdatePending = false; } } public virtual void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - if (!(extraData is IEventData eventData)) { throw new Exception($"Malformed character event: expected {nameof(Character)}.{nameof(IEventData)}, got {extraData?.GetType().Name ?? "[NULL]"}"); } + if (extraData is not IEventData eventData) { throw new Exception($"Malformed character event: expected {nameof(Character)}.{nameof(IEventData)}, got {extraData?.GetType().Name ?? "[NULL]"}"); } msg.WriteRangedInteger((int)eventData.EventType, (int)EventType.MinValue, (int)EventType.MaxValue); switch (eventData) { - case InventoryStateEventData _: + case InventoryStateEventData inventoryData: msg.WriteUInt16(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0); - Inventory.ServerEventWrite(msg, c); + Inventory.ServerEventWrite(msg, c, inventoryData); break; case ControlEventData controlEventData: Client owner = controlEventData.Owner; @@ -473,12 +482,12 @@ namespace Barotrauma case IAttackEventData attackEventData: { int attackLimbIndex = Removed ? -1 : Array.IndexOf(AnimController.Limbs, attackEventData.AttackLimb); - ushort targetEntityId = 0; + ushort targetEntityId = NullEntityID; int targetLimbIndex = -1; if (attackEventData.TargetEntity is Entity { Removed: false } targetEntity) { targetEntityId = targetEntity.ID; - if (targetEntity is Character { AnimController: { Limbs: var targetLimbsArray } }) + if (targetEntity is Character { AnimController.Limbs: var targetLimbsArray }) { targetLimbIndex = targetLimbsArray.IndexOf(attackEventData.TargetLimb); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/CircuitBox/CircuitBoxConnection.cs b/Barotrauma/BarotraumaServer/ServerSource/CircuitBox/CircuitBoxConnection.cs new file mode 100644 index 000000000..8232dd5e9 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/CircuitBox/CircuitBoxConnection.cs @@ -0,0 +1,16 @@ +#nullable enable + +using Barotrauma.Items.Components; + +namespace Barotrauma +{ + internal abstract partial class CircuitBoxConnection + { + public string Name => Connection.Name; + + private partial void InitProjSpecific(CircuitBox circuitBox) + { + Length = 100f; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index 0411715e8..fd59111ec 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -8,6 +8,7 @@ using System.Globalization; using System.Linq; using System.Text; using Barotrauma.Steam; +using Barotrauma.Extensions; namespace Barotrauma { @@ -1020,11 +1021,6 @@ namespace Barotrauma client.SpectateOnly = false; }); - AssignOnExecute("starttraitormissionimmediately", (string[] args) => - { - GameMain.Server?.TraitorManager?.SkipStartDelay(); - }); - AssignOnExecute("difficulty|leveldifficulty", (string[] args) => { if (GameMain.Server == null || args.Length < 1) return; @@ -1082,56 +1078,83 @@ namespace Barotrauma GameMain.Server?.UpdateCheatsEnabled(); }); - commands.Add(new Command("traitorlist", "traitorlist: List all the traitors and their targets.", (string[] args) => + + AssignOnExecute("triggertraitorevent", (string[] args) => { - if (GameMain.Server == null) return; - TraitorManager traitorManager = GameMain.Server.TraitorManager; - if (traitorManager == null || traitorManager.Traitors == null || !traitorManager.Traitors.Any()) + if (GameMain.Server?.TraitorManager == null) { - NewMessage("There are no traitors at the moment.", Color.Cyan); + ThrowError($"Could not start a traitor event. {nameof(TraitorManager)} hasn't been created."); return; } - foreach (Traitor t in traitorManager.Traitors) + if (args.Length > 0) { - if (t.CurrentObjective != null) + Identifier traitorEventId = args[0].ToIdentifier(); + if (EventPrefab.Prefabs.TryGet(traitorEventId, out EventPrefab prefab) && prefab is TraitorEventPrefab traitorEventPrefab) { - NewMessage(string.Format("- Traitor {0}'s current goals are:\n{1}", t.Character.Name, t.CurrentObjective.GoalInfos), Color.Cyan); + GameMain.Server?.TraitorManager?.ForceTraitorEvent(traitorEventPrefab); } else { - NewMessage(string.Format("- Traitor {0} has no current objective.", t.Character.Name), Color.Cyan); + ThrowError($"Could not find a traitor event prefab with the identifier \"{traitorEventId}\"."); + return; } } - //NewMessage("The code words are: " + traitorManager.CodeWords + ", response: " + traitorManager.CodeResponse + ".", Color.Cyan); + if (GameMain.Server?.TraitorManager is { } traitorManager) + { + traitorManager.Enabled = true; + traitorManager.SkipStartDelay(); + } + }); + + commands.Add(new Command("traitorlist", "traitorlist: List all the traitors and their current objectives.", (string[] args) => + { + if (GameMain.Server == null) { return; } + CreateTraitorList((string msg) => NewMessage(msg, Color.Cyan)); })); AssignOnClientRequestExecute("traitorlist", (Client client, Vector2 cursorPos, string[] args) => { - TraitorManager traitorManager = GameMain.Server.TraitorManager; - if (traitorManager == null || traitorManager.Traitors == null || !traitorManager.Traitors.Any()) + if (GameMain.Server == null) { return; } + CreateTraitorList((string msg) => { - GameMain.Server.SendTraitorMessage(client, "There are no traitors at the moment.", Identifier.Empty, TraitorMessageType.Console); + GameMain.Server.SendDirectChatMessage(msg, client); + }); + }); + + void CreateTraitorList(Action createMessage) + { + if (GameMain.Server == null) return; + TraitorManager traitorManager = GameMain.Server.TraitorManager; + if (traitorManager == null || traitorManager.ActiveEvents.None()) + { + createMessage("There are no traitors at the moment."); return; } - foreach (Traitor t in traitorManager.Traitors) + createMessage("Traitors:"); + foreach (var ev in traitorManager.ActiveEvents) { - if (t.CurrentObjective != null) + createMessage($" - {ev.Traitor.Name}: {ev.TraitorEvent.Prefab.Identifier} ({ev.TraitorEvent.CurrentState})"); + } + } + + AssignOnClientRequestExecute( + "debugevent", + (Client client, Vector2 cursorWorldPos, string[] args) => + { + if (GameMain.Server == null) { return; } + if (GameMain.GameSession?.EventManager is EventManager eventManager && args.Length > 0) { - var traitorGoals = TextManager.FormatServerMessage(t.CurrentObjective.GoalInfos); - var traitorGoalsStart = traitorGoals.LastIndexOf('/') + 1; - GameMain.Server.SendTraitorMessage(client, string.Join("/", new[] { - traitorGoals.Substring(0, traitorGoalsStart), - $"[traitorgoals]={traitorGoals.Substring(traitorGoalsStart)}", - $"[traitorname]={t.Character.Name}", - "Traitor [traitorname]'s current goals are:\n[traitorgoals]" - }.Where(s => !string.IsNullOrEmpty(s))), t.Mission.Identifier, TraitorMessageType.Console); - } - else - { - GameMain.Server.SendTraitorMessage(client, string.Format("- Traitor {0} has no current objective.", t.Character.Name), Identifier.Empty, TraitorMessageType.Console); + var ev = eventManager.ActiveEvents.FirstOrDefault(ev => ev.Prefab?.Identifier == args[0]); + if (ev == null) + { + GameMain.Server.SendConsoleMessage($"Event \"{args[0]}\" not found.", client); + } + else + { + GameMain.Server.SendConsoleMessage(ev.GetDebugInfo(), client); + } } } - //GameMain.Server.SendTraitorMessage(client, "The code words are: " + traitorManager.CodeWords + ", response: " + traitorManager.CodeResponse + ".", TraitorMessageType.Console); - }); + ); commands.Add(new Command("setpassword|setserverpassword|password", "setpassword [password]: Changes the password of the server that's being hosted.", (string[] args) => { @@ -2401,7 +2424,7 @@ namespace Barotrauma } })); - commands.Add(new Command("sendchatmessage", "Sends a chat message with specified type and color.", (string[] args) => + commands.Add(new Command("sendchatmessage", "sendchatmessage [sendername] [message] [type] [r] [g] [b] [a]: Sends a chat message with specified type and color.", (string[] args) => { if (args.Length < 2) { return; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs index 353ef08d8..39fa32cab 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs @@ -132,7 +132,7 @@ namespace Barotrauma else { outmsg.WriteUInt16(speaker?.ID ?? Entity.NullEntityID); - outmsg.WriteString(Text ?? string.Empty); + outmsg.WriteString(GetDisplayText()?.Value ?? string.Empty); outmsg.WriteBoolean(FadeToBlack); outmsg.WriteByte((byte)Options.Count); for (int i = 0; i < Options.Count; i++) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventLogAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventLogAction.cs new file mode 100644 index 000000000..976984e76 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventLogAction.cs @@ -0,0 +1,55 @@ +#nullable enable + +using Barotrauma.Extensions; +using Barotrauma.Networking; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma; + +partial class EventLogAction : EventAction +{ + partial void AddEntryProjSpecific(EventLog? eventLog, string displayText) + { + if (eventLog == null) { return; } + if (!TargetTag.IsEmpty) + { + List targetClients = new List(); + foreach (var target in ParentEvent.GetTargets(TargetTag)) + { + if (target is Character character) + { + var ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character); + if (ownerClient != null && eventLog != null) + { + targetClients.Add(ownerClient); + } + } + else + { + DebugConsole.AddWarning($"{target} is not a valid target for an EventLogAction. The target should be a character."); + } + } + if (eventLog.TryAddEntry(ParentEvent.Prefab.Identifier, Id, displayText, targetClients) && ShowInServerLog) + { + Log(targetClients); + } + } + else + { + if (eventLog != null && eventLog.TryAddEntry(ParentEvent.Prefab.Identifier, Id, displayText, GameMain.Server.ConnectedClients) && ShowInServerLog) + { + Log(targetClients: null); + } + } + + void Log(List? targetClients) + { + string clientStr = targetClients == null || targetClients.None() ? + string.Empty : + $" ({string.Join(", ", targetClients.Select(c => NetworkMember.ClientLogName(c)))})"; + GameServer.Log($"Event \"{ParentEvent.Prefab.Name}\"{clientStr}: " + displayText, + ParentEvent is TraitorEvent ? ServerLog.MessageType.Traitors : ServerLog.MessageType.Chat); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventObjectiveAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventObjectiveAction.cs new file mode 100644 index 000000000..9b84d31dd --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventObjectiveAction.cs @@ -0,0 +1,36 @@ +namespace Barotrauma +{ + partial class EventObjectiveAction : EventAction + { + partial void UpdateProjSpecific() + { + if (GameMain.Server == null) { return; } + EventManager.NetEventObjective objective = new EventManager.NetEventObjective( + Type, + Identifier, + ObjectiveTag, + TextTag, + ParentObjectiveId, + CanBeCompleted); + + if (TargetTag.IsEmpty) + { + foreach (var client in GameMain.Server.ConnectedClients) + { + if (client.Character == null) { continue; } + EventManager.ServerWriteObjective(client, objective); + } + } + else + { + foreach (var target in ParentEvent.GetTargets(TargetTag)) + { + if (target is not Character character) { continue; } + var ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character); + if (ownerClient == null) { continue; } + EventManager.ServerWriteObjective(ownerClient, objective); + } + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventLog.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventLog.cs new file mode 100644 index 000000000..6e8d0a8a1 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventLog.cs @@ -0,0 +1,22 @@ +#nullable enable + +using Barotrauma.Networking; +using System.Collections.Generic; + +namespace Barotrauma; + +partial class EventLog +{ + public bool TryAddEntry(Identifier eventPrefabId, Identifier entryId, string text, IEnumerable targetClients) + { + if (TryAddEntryInternal(eventPrefabId, entryId, text)) + { + foreach (var targetClient in targetClients) + { + EventManager.ServerWriteEventLog(targetClient, new EventManager.NetEventLogEntry(eventPrefabId, entryId, text)); + } + return true; + } + return false; + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs index 62b7f9882..74549b187 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs @@ -1,12 +1,29 @@ using Barotrauma.Networking; using System; -using System.Collections.Generic; using System.Linq; namespace Barotrauma { partial class EventManager { + public static void ServerWriteEventLog(Client client, NetEventLogEntry entry) + { + IWriteMessage outmsg = new WriteOnlyMessage(); + outmsg.WriteByte((byte)ServerPacketHeader.EVENTACTION); + outmsg.WriteByte((byte)NetworkEventType.EVENTLOG); + outmsg.WriteNetSerializableStruct(entry); + GameMain.Server?.ServerPeer?.Send(outmsg, client.Connection, DeliveryMethod.Reliable); + } + + public static void ServerWriteObjective(Client client, NetEventObjective entry) + { + IWriteMessage outmsg = new WriteOnlyMessage(); + outmsg.WriteByte((byte)ServerPacketHeader.EVENTACTION); + outmsg.WriteByte((byte)NetworkEventType.EVENTOBJECTIVE); + outmsg.WriteNetSerializableStruct(entry); + GameMain.Server?.ServerPeer?.Send(outmsg, client.Connection, DeliveryMethod.Reliable); + } + public void ServerRead(IReadMessage inc, Client sender) { UInt16 actionId = inc.ReadUInt16(); @@ -16,14 +33,14 @@ namespace Barotrauma { if (ev is not ScriptedEvent scriptedEvent) { continue; } - var actions = FindActions(scriptedEvent); - foreach (EventAction action in actions.Select(a => a.Item2)) + var actions = scriptedEvent.GetAllActions(); + foreach (EventAction action in actions.Select(a => a.action)) { if (action is not ConversationAction convAction || convAction.Identifier != actionId) { continue; } if (!convAction.TargetClients.Contains(sender)) { #if DEBUG || UNSTABLE - DebugConsole.ThrowError($"Client \"{sender.Name}\" tried to respond to a ConversationAction that was not targeted to them."); + DebugConsole.ThrowError($"Client \"{sender.Name}\" tried to respond to a ConversationAction that was not targeted to them ({convAction.Text})."); #endif continue; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs index 433b33075..64f8f99b6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs @@ -1,4 +1,7 @@ using Barotrauma.Networking; +using System; +using System.Collections.Generic; +using System.Linq; namespace Barotrauma { @@ -20,6 +23,99 @@ namespace Barotrauma GameServer.Log($"{TextManager.Get("MissionInfo")}: {header} - {message}", ServerLog.MessageType.ServerMessage); } + public static int DistributeRewardsToCrew(IEnumerable crew, int totalReward) + { + int remainingRewards = totalReward; + float sum = GetRewardDistibutionSum(crew); + if (MathUtils.NearlyEqual(sum, 0)) { return remainingRewards; } + foreach (Character character in crew) + { + int rewardDistribution = character.Wallet.RewardDistribution; + float rewardWeight = sum > 100 ? rewardDistribution / sum : rewardDistribution / 100f; + int reward = Math.Min(remainingRewards, (int)(totalReward * rewardWeight)); + character.Wallet.Give(reward); + remainingRewards -= reward; + if (remainingRewards <= 0) { break; } + } + + return remainingRewards; + } + + partial void DistributeExperienceToCrew(IEnumerable crew, int experienceGain) + { + Dictionary traitorExpSteal = new Dictionary(); + float totalExpSteal = 0.0f; + foreach (var traitorEvent in GameMain.Server.TraitorManager.ActiveEvents) + { + if (traitorEvent.TraitorEvent.CurrentState != TraitorEvent.State.Completed) { continue; } + if (traitorEvent.Traitor?.Character == null || !GameMain.Server.ConnectedClients.Contains(traitorEvent.Traitor)) { continue; } + + float expSteal = Math.Max(traitorEvent.TraitorEvent.Prefab.StealPercentageOfExperience, 0.0f); + AddTraitorExpSteal(traitorEvent.Traitor.Character, expSteal); + foreach (var secondaryTraitor in traitorEvent.TraitorEvent.SecondaryTraitors) + { + AddTraitorExpSteal(secondaryTraitor.Character, expSteal); + } + + void AddTraitorExpSteal(Character traitorCharacter, float expSteal) + { + if (traitorCharacter == null) { return; } + if (!traitorExpSteal.ContainsKey(traitorCharacter)) + { + traitorExpSteal.Add(traitorCharacter, 0.0f); + } + traitorExpSteal[traitorCharacter] += expSteal; + } + } + totalExpSteal = traitorExpSteal.Values.Sum(); + //if exp to steal exceeds 100%, normalize to get it back to 100% + //(e.g. two traitors who both steal 75%, they'll share 50% of all the exp gains) + if (totalExpSteal > 100.0f) + { + foreach (Character traitor in traitorExpSteal.Keys) + { + traitorExpSteal[traitor] /= totalExpSteal; + } + totalExpSteal = 100.0f; + } + if (totalExpSteal > 0) + { + GameServer.Log($"Traitors stole {(int)totalExpSteal}% of the total experience.", ServerLog.MessageType.Traitors); + } + + int nonTraitorCount = GameSession.GetSessionCrewCharacters(CharacterType.Both).Count(c => !traitorExpSteal.ContainsKey(c)); + foreach (Networking.Client c in GameMain.Server.ConnectedClients) + { + //give the experience to the stored characterinfo if the client isn't currently controlling a character + GiveMissionExperience(c.Character?.Info ?? c.CharacterInfo); + } + foreach (Character bot in GameSession.GetSessionCrewCharacters(CharacterType.Bot)) + { + GiveMissionExperience(bot.Info); + } + + void GiveMissionExperience(CharacterInfo info) + { + if (info == null) { return; } + var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f); + info.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual); + + int finalExperienceGain = (int)(experienceGain * experienceGainMultiplierIndividual.Value); + if (info.Character != null && traitorExpSteal.TryGetValue(info.Character, out float expToSteal)) + { + int stealAmount = (int)(experienceGain * nonTraitorCount * expToSteal / 100.0f); + GameServer.Log($"Traitor {info.Character} stole {stealAmount} ({(int)expToSteal}%) of the total experience.", ServerLog.MessageType.Traitors); + finalExperienceGain += stealAmount; + } + else + { + GameServer.Log($"{(int)(finalExperienceGain * totalExpSteal / 100.0f)} ({(int)totalExpSteal}%) was stolen from {info.Name}.", ServerLog.MessageType.Traitors); + finalExperienceGain -= (int)(finalExperienceGain * totalExpSteal / 100.0f); + } + info.GiveExperience(finalExperienceGain); + } + } + public virtual void ServerWriteInitial(IWriteMessage msg, Client c) { msg.WriteUInt16((ushort)State); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/NestMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/NestMission.cs index 35649469e..32a7eef1c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/NestMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/NestMission.cs @@ -4,8 +4,6 @@ namespace Barotrauma { partial class NestMission : Mission { - private Level.Cave selectedCave; - public override void ServerWriteInitial(IWriteMessage msg, Client c) { base.ServerWriteInitial(msg, c); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index f41f62988..d21e80fbd 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -327,6 +327,7 @@ namespace Barotrauma while (Timing.Accumulator >= Timing.Step) { Timing.TotalTime += Timing.Step; + Timing.TotalTimeUnpaused += Timing.Step; DebugConsole.Update(); if (GameSession?.GameMode == null || !GameSession.GameMode.Paused) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs index d99340d6b..ddc0ae1d5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs @@ -42,10 +42,13 @@ namespace Barotrauma } } - public void Refresh(Character character) + public void Refresh(Character character, bool refreshHealthData) { - healthData = new XElement("health"); - character.CharacterHealth.Save(healthData); + if (refreshHealthData) + { + healthData = new XElement("health"); + character.CharacterHealth.Save(healthData); + } if (character.Inventory != null) { itemData = new XElement("inventory"); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 7629dd08f..e1b3de615 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -260,7 +260,7 @@ namespace Barotrauma { //character still alive (or killed by Disconnect) -> save it as-is characterData.RemoveAll(cd => cd.IsDuplicate(data)); - data.Refresh(character); + data.Refresh(character, refreshHealthData: character.CauseOfDeath?.Type != CauseOfDeathType.Disconnected); characterData.Add(data); } else @@ -318,7 +318,7 @@ namespace Barotrauma discardedCharacters.Clear(); } - protected override IEnumerable DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror, List traitorResults) + protected override IEnumerable DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror) { IncrementAllLastUpdateIds(); @@ -360,7 +360,7 @@ namespace Barotrauma GameMain.GameSession.EventManager.RegisterEventHistory(); } - GameMain.GameSession.EndRound("", traitorResults, transitionType); + GameMain.GameSession.EndRound("", transitionType); //-------------------------------------- @@ -1361,6 +1361,10 @@ namespace Barotrauma modeElement.Add(Settings.Save()); modeElement.Add(SaveStats()); + if (GameMain.Server?.TraitorManager is TraitorManager traitorManager) + { + modeElement.Add(traitorManager.Save()); + } modeElement.Add(Bank.Save()); if (GameMain.GameSession?.EventManager != null) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/CharacterInventory.cs new file mode 100644 index 000000000..a00978616 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/CharacterInventory.cs @@ -0,0 +1,12 @@ +using Barotrauma.Networking; + +namespace Barotrauma +{ + partial class CharacterInventory : Inventory + { + public void ServerEventWrite(IWriteMessage msg, Client c, Character.InventoryStateEventData inventoryData) + { + SharedWrite(msg, inventoryData.SlotRange); + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs index 98c6a0fa9..0e1e1a839 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs @@ -32,6 +32,7 @@ namespace Barotrauma.Items.Components Drop(false, null); item.SetTransform(simPosition, 0.0f, findNewHull: false); AttachToWall(); + OnUsed.Invoke(new ItemUseInfo(item, c.Character)); item.CreateServerEvent(this); c.Character.Inventory?.CreateNetworkEvent(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs index 188721aa1..c1f24ccf6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs @@ -49,21 +49,35 @@ namespace Barotrauma.Items.Components msg.WriteSingle(jointAxis.Y); if (StickTarget.UserData is Structure structure) { + msg.WriteByte((byte)StickTargetType.Structure); msg.WriteUInt16(structure.ID); int bodyIndex = structure.Bodies.IndexOf(StickTarget); msg.WriteByte((byte)(bodyIndex == -1 ? 0 : bodyIndex)); } - else if (StickTarget.UserData is Entity entity) + else if (StickTarget.UserData is Item item) { - msg.WriteUInt16(entity.ID); + msg.WriteByte((byte)StickTargetType.Item); + msg.WriteUInt16(item.ID); + } + else if (StickTarget.UserData is Submarine sub) + { + msg.WriteByte((byte)StickTargetType.Submarine); + msg.WriteUInt16(sub.ID); } else if (StickTarget.UserData is Limb limb) { + msg.WriteByte((byte)StickTargetType.Limb); msg.WriteUInt16(limb.character.ID); msg.WriteByte((byte)Array.IndexOf(limb.character.AnimController.Limbs, limb)); } + else if (StickTarget.UserData is Voronoi2.VoronoiCell cell) + { + msg.WriteByte((byte)StickTargetType.LevelWall); + msg.WriteInt32(Level.Loaded.GetAllCells().IndexOf(cell)); + } else { + msg.WriteByte((byte)StickTargetType.Unknown); throw new NotImplementedException(StickTarget.UserData?.ToString() ?? "null" + " is not a valid projectile stick target."); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs index e4e5845c9..089fc03df 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs @@ -45,8 +45,7 @@ namespace Barotrauma.Items.Components public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.WriteSingle(deteriorationTimer); - msg.WriteSingle(deteriorateAlwaysResetTimer); - msg.WriteBoolean(DeteriorateAlways); + msg.WriteSingle(ForceDeteriorationTimer); msg.WriteSingle(tinkeringDuration); msg.WriteSingle(tinkeringStrength); msg.WriteBoolean(tinkeringPowersDevices); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs new file mode 100644 index 000000000..010111c87 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs @@ -0,0 +1,320 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; + +namespace Barotrauma.Items.Components +{ + internal sealed partial class CircuitBox + { + /// + /// If the server needs to initialize the circuit box to the clients + /// instead of the clients loading it from the save file. + /// + private bool needsServerInitialization; + + /// + /// When in multiplayer and the circuit box is loaded from the players inventory, + /// We only load the components from XML on server side since only the server has access to CharacterCampaignData + /// and then send a network event syncing the loaded properties. But circuit box properties are too complex to + /// sync using the existing syncing logic so we instead send the state using . + /// + public void MarkServerRequiredInitialization() + => needsServerInitialization = true; + + public partial void OnDeselected(Character c) + { + ClearAllSelectionsInternal(c.ID); + BroadcastSelectionStatus(); + } + + public void ServerRead(INetSerializableStruct data, Client c) + { + switch (data) + { + case NetCircuitBoxCursorInfo { RecordedPositions.Length: 10 } cursorInfo: + { + RelayCursorState(cursorInfo, c); + break; + } + } + } + + private void RelayCursorState(NetCircuitBoxCursorInfo data, Client sender) + { + if (GameMain.Server is null || !IsRoundRunning()) { return; } + + SendToAll(CircuitBoxOpcode.Cursor, data with { CharacterID = sender.CharacterID }, FilterClients); + + bool FilterClients(Client client) + { + // ReSharper disable once RedundantAssignment + bool isSender = client == sender; +#if DEBUG + // Shown own cursor in debug builds + isSender = false; +#endif + return !isSender && client.Character is not null && client.Character.SelectedItem == item; + } + } + + public void SendToClient(CircuitBoxOpcode opcode, INetSerializableStruct data, Client targetClient) + { + var (msg, deliveryMethod) = PrepareToSend(opcode, data); + + GameMain.Server?.ServerPeer?.Send(msg, targetClient.Connection, deliveryMethod); + } + + public void SendToAll(CircuitBoxOpcode opcode, INetSerializableStruct data, Func? predicate = null) + { + var (msg, deliveryMethod) = PrepareToSend(opcode, data); + + foreach (Client client in GameMain.Server.ConnectedClients) + { + if (predicate is not null && !predicate(client)) { continue; } + + GameMain.Server?.ServerPeer?.Send(msg, client.Connection, deliveryMethod); + } + } + + private (IWriteMessage Message, DeliveryMethod DeliveryMethod) PrepareToSend(CircuitBoxOpcode opcode, INetSerializableStruct data) + { + IWriteMessage msg = new WriteOnlyMessage().WithHeader(ServerPacketHeader.CIRCUITBOX); + + msg.WriteNetSerializableStruct(new NetCircuitBoxHeader( + Opcode: opcode, + ItemID: item.ID, + ComponentIndex: (byte)item.GetComponentIndex(this))); + + msg.WriteNetSerializableStruct(data); + + DeliveryMethod deliveryMethod = + UnrealiableOpcodes.Contains(opcode) + ? DeliveryMethod.Unreliable + : DeliveryMethod.Reliable; + + return (msg, deliveryMethod); + } + + public void CreateServerEvent(INetSerializableStruct data) + => item.CreateServerEvent(this, new CircuitBoxEventData(data)); + + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData? extraData = null) + { + if (extraData is null) { return; } + + var eventData = ExtractEventData(extraData); + msg.WriteByte((byte)eventData.Opcode); + msg.WriteNetSerializableStruct(eventData.Data); + } + + public void ServerEventRead(IReadMessage msg, Client c) + { + var header = (CircuitBoxOpcode)msg.ReadByte(); + switch (header) + { + case CircuitBoxOpcode.AddComponent: + { + var data = INetSerializableStruct.Read(msg); + if (!item.CanClientAccess(c)) { break; } + + var prefab = ItemPrefab.Prefabs.Find(p => p.UintIdentifier == data.PrefabIdentifier); + if (prefab is null) + { + ThrowError("Unable to add component because the prefab was not found.", c); + return; + } + + if (IsFull || !GetApplicableResourcePlayerHas(prefab, c.Character).TryUnwrap(out var resource)) { return; } + + ushort id = ICircuitBoxIdentifiable.FindFreeID(Components); + if (id is ICircuitBoxIdentifiable.NullComponentID) + { + ThrowError("Unable to add component because there are no available IDs left.", c); + return; + } + + bool result = AddComponentInternal(id, prefab, resource.Prefab, data.Position, it => + { + CreateServerEvent(new CircuitBoxServerCreateComponentEvent(it.ID, resource.Prefab.UintIdentifier, id, data.Position)); + }); + + if (!result) + { + ThrowError("Unable to add component because the component could not be created.", c); + return; + } + + GameServer.Log($"{NetworkMember.ClientLogName(c)} added a {prefab.Name} into a circuit box.", ServerLog.MessageType.Wiring); + RemoveItem(resource); + break; + } + case CircuitBoxOpcode.MoveComponent: + { + var data = INetSerializableStruct.Read(msg); + if (!item.CanClientAccess(c)) { break; } + + MoveNodesInternal(data.TargetIDs, data.IOs, data.MoveAmount); + CreateServerEvent(data); + break; + } + case CircuitBoxOpcode.DeleteComponent: + { + var data = INetSerializableStruct.Read(msg); + if (!data.TargetIDs.Any() || !item.CanClientAccess(c)) { break; } + + CreateRefundItemsForUsedResources(data.TargetIDs, c.Character); + GameServer.Log($"{NetworkMember.ClientLogName(c)} removed {GetLogComponentName(data.TargetIDs)} from circuit box.", ServerLog.MessageType.Wiring); + RemoveComponentInternal(data.TargetIDs); + CreateServerEvent(data); + break; + } + case CircuitBoxOpcode.SelectComponents: + { + var data = INetSerializableStruct.Read(msg); + if (!item.CanClientAccess(c)) { break; } + + SelectComponentsInternal(data.TargetIDs, c.CharacterID, data.Overwrite); + SelectInputOutputInternal(data.IOs, c.CharacterID, data.Overwrite); + BroadcastSelectionStatus(); + break; + } + case CircuitBoxOpcode.SelectWires: + { + var data = INetSerializableStruct.Read(msg); + if (!item.CanClientAccess(c)) { break; } + + SelectWiresInternal(data.TargetIDs, c.CharacterID, data.Overwrite); + BroadcastSelectionStatus(); + break; + } + case CircuitBoxOpcode.AddWire: + { + var data = INetSerializableStruct.Read(msg); + if (!item.CanClientAccess(c)) { break; } + + var prefab = ItemPrefab.Prefabs.Find(p => p.UintIdentifier == data.SelectedWirePrefabIdentifier); + if (prefab is null) + { + ThrowError($"Unable to connect wire because wire by identifier \"{data.SelectedWirePrefabIdentifier}\" was not found.", c); + break; + } + + if (data.Start.FindConnection(this).TryUnwrap(out var start) && + data.End.FindConnection(this).TryUnwrap(out var end)) + { + bool result = Connect(start, end, wire => + { + CreateServerEvent(new CircuitBoxServerCreateWireEvent(data with { Start = wire.Start, End = wire.End }, wire.ID, wire.Item.Select(static i => i.ID))); + }, prefab); + + if (!result) + { + ThrowError("Unable to connect wire because the circuit box rejected it.", c); + } + + GameServer.Log($"{NetworkMember.ClientLogName(c)} connected a wire from {start.Name} to {end.Name} in a circuit box.", ServerLog.MessageType.Wiring); + } + else + { + ThrowError($"Unable to connect wire because the start or end connection was not found. (start: {data.Start}, end: {data.End})", c); + } + + break; + } + case CircuitBoxOpcode.RemoveWire: + { + var data = INetSerializableStruct.Read(msg); + if (!data.TargetIDs.Any() || !item.CanClientAccess(c)) { break; } + + GameServer.Log($"{NetworkMember.ClientLogName(c)} removed {GetLogWireName(data.TargetIDs)} from circuit box.", ServerLog.MessageType.Wiring); + RemoveWireInternal(data.TargetIDs); + CreateServerEvent(data); + break; + } + default: + throw new ArgumentOutOfRangeException(nameof(header), header, "This opcode cannot be handled using entity events"); + } + + string GetLogComponentName(IReadOnlyList ids) + { + if (ids.Count > 1) { return $"{ids.Count} components"; } + return Components.FirstOrDefault(comp => ids.Contains(comp.ID))?.Item.Name ?? "[UNKNOWN]"; + } + + string GetLogWireName(IReadOnlyList ids) + { + if (ids.Count > 1) { return $"{ids.Count} wires"; } + if (Wires.FirstOrDefault(w => ids.Contains(w.ID)) is not { } wire) { return "[UNKNOWN]"; } + + return wire.BackingWire.TryUnwrap(out var backingWire) ? backingWire.Name : "a wire"; + } + } + + /// + /// Creates an event that overrides the state of the circuit box for all clients. + /// This is only required if the circuit box is loaded from the players inventory in multiplayer. + /// + public void CreateInitializationEvent() + { + Vector2 inputPos = Vector2.Zero, + outputPos = Vector2.Zero; + + foreach (var ioNode in InputOutputNodes) + { + switch (ioNode.NodeType) + { + case CircuitBoxInputOutputNode.Type.Input: + inputPos = ioNode.Position; + break; + case CircuitBoxInputOutputNode.Type.Output: + outputPos = ioNode.Position; + break; + } + } + + CircuitBoxInitializeStateFromServerEvent data = new( + Components: Components.Select(EventFromComponent).ToImmutableArray(), + Wires: Wires.Select(EventFromWire).ToImmutableArray(), + InputPos: inputPos, + OutputPos: outputPos); + + CreateServerEvent(data); + + static CircuitBoxServerCreateComponentEvent EventFromComponent(CircuitBoxComponent component) + => new(component.Item.ID, component.UsedResource.UintIdentifier, component.ID, component.Position); + + static CircuitBoxServerCreateWireEvent EventFromWire(CircuitBoxWire wire) + { + var backingWire = wire.BackingWire.Select(static i => i.ID); + var from = CircuitBoxConnectorIdentifier.FromConnection(wire.From); + var to = CircuitBoxConnectorIdentifier.FromConnection(wire.To); + + var request = new CircuitBoxClientAddWireEvent(wire.Color, from, to, wire.UsedItemPrefab.UintIdentifier); + return new CircuitBoxServerCreateWireEvent(request, wire.ID, backingWire); + } + } + + // we don't care about updating the view on server + public partial void OnViewUpdateProjSpecific() { } + + private void ThrowError(string message, Client c) + { + DebugConsole.ThrowError(message); + SendToClient(CircuitBoxOpcode.Error, new CircuitBoxErrorEvent(message), c); + } + + private void BroadcastSelectionStatus() + { + var nodes = Components.Select(static c => new CircuitBoxIdSelectionPair(c.ID, c.IsSelected ? Option.Some(c.SelectedBy) : Option.None)).ToImmutableArray(); + var wires = Wires.Select(static w => new CircuitBoxIdSelectionPair(w.ID, w.IsSelected ? Option.Some(w.SelectedBy) : Option.None)).ToImmutableArray(); + var ios = InputOutputNodes.Select(static n => new CircuitBoxTypeSelectionPair(n.NodeType, n.IsSelected ? Option.Some(n.SelectedBy) : Option.None)).ToImmutableArray(); + + CreateServerEvent(new CircuitBoxServerUpdateSelection(nodes, wires, ios)); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs index 9403732da..3370c6afd 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs @@ -12,7 +12,8 @@ namespace Barotrauma.Items.Components List[] wires = new List[Connections.Count]; //read wire IDs for each connection - for (int i = 0; i < Connections.Count; i++) + byte connectionCount = msg.ReadByte(); + for (int i = 0; i < Connections.Count && i < connectionCount; i++) { wires[i] = new List(); uint wireCount = msg.ReadVariableUInt32(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs index fd03a0bfd..fb2736c41 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs @@ -32,17 +32,17 @@ namespace Barotrauma.Items.Components GameServer.Log(GameServer.CharacterLogName(c.Character) + " entered \"" + newOutputValue + "\" on " + item.Name, ServerLog.MessageType.ItemInteraction); OutputValue = newOutputValue; - ShowOnDisplay(newOutputValue, addToHistory: true, TextColor); + ShowOnDisplay(newOutputValue, addToHistory: true, TextColor, isWelcomeMessage: false); item.SendSignal(newOutputValue, "signal_out"); item.CreateServerEvent(this); } } - partial void ShowOnDisplay(string input, bool addToHistory, Color color) + partial void ShowOnDisplay(string input, bool addToHistory, Color color, bool isWelcomeMessage) { if (addToHistory) { - messageHistory.Add(new TerminalMessage(input, color)); + messageHistory.Add(new TerminalMessage(input, color, isWelcomeMessage)); while (messageHistory.Count > MaxMessages) { messageHistory.RemoveAt(0); @@ -54,9 +54,11 @@ namespace Barotrauma.Items.Components { //split too long messages to multiple parts int msgIndex = 0; - foreach (var (str, _) in messageHistory) + foreach (var msg in messageHistory) { - string msgToSend = str; + //the clients create the welcome message themselves, no need to sync it + if (msg.IsWelcomeMessage) { continue; } + string msgToSend = msg.Text; if (string.IsNullOrEmpty(msgToSend)) { item.CreateServerEvent(this, new ServerEventData(msgIndex, msgToSend)); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs index 84c67ceae..a063286d5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs @@ -4,17 +4,25 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; -using Barotrauma.Extensions; namespace Barotrauma { - partial class Inventory : IServerSerializable, IClientSerializable + partial class Inventory : IClientSerializable { + private readonly Dictionary[]> receivedItemIds = new Dictionary[]>(); + public void ServerEventRead(IReadMessage msg, Client c) { List prevItems = new List(AllItems.Distinct()); - SharedRead(msg, out var newItemIDs); + if (!receivedItemIds.TryGetValue(c, out List[] receivedItemIdsFromClient)) + { + receivedItemIdsFromClient = new List[capacity]; + receivedItemIds.Add(c, receivedItemIdsFromClient); + } + + SharedRead(msg, receivedItemIdsFromClient, out bool readyToApply); + if (!readyToApply) { return; } if (c == null || c.Character == null) { return; } @@ -39,7 +47,7 @@ namespace Barotrauma CreateNetworkEvent(); for (int i = 0; i < capacity; i++) { - foreach (ushort id in newItemIDs[i]) + foreach (ushort id in receivedItemIdsFromClient[i]) { if (Entity.FindEntityByID(id) is not Item item) { continue; } item.PositionUpdateInterval = 0.0f; @@ -58,7 +66,7 @@ namespace Barotrauma { foreach (Item item in slots[i].Items.ToList()) { - if (!newItemIDs[i].Contains(item.ID)) + if (!receivedItemIdsFromClient[i].Contains(item.ID)) { Item droppedItem = item; Entity prevOwner = Owner; @@ -85,7 +93,7 @@ namespace Barotrauma } } - foreach (ushort id in newItemIDs[i]) + foreach (ushort id in receivedItemIdsFromClient[i]) { Item newItem = id == 0 ? null : Entity.FindEntityByID(id) as Item; prevItemInventories.Add(newItem?.ParentInventory); @@ -94,7 +102,7 @@ namespace Barotrauma for (int i = 0; i < capacity; i++) { - foreach (ushort id in newItemIDs[i]) + foreach (ushort id in receivedItemIdsFromClient[i]) { if (Entity.FindEntityByID(id) is not Item item || slots[i].Contains(item)) { continue; } @@ -128,7 +136,7 @@ namespace Barotrauma TryPutItem(item, i, true, true, c.Character, false); for (int j = 0; j < capacity; j++) { - if (slots[j].Contains(item) && !newItemIDs[j].Contains(item.ID)) + if (slots[j].Contains(item) && !receivedItemIdsFromClient[j].Contains(item.ID)) { slots[j].RemoveItem(item); } @@ -138,42 +146,48 @@ namespace Barotrauma EnsureItemsInBothHands(c.Character); + receivedItemIds.Remove(c); + CreateNetworkEvent(); foreach (Inventory prevInventory in prevItemInventories.Distinct()) { if (prevInventory != this) { prevInventory?.CreateNetworkEvent(); } } - foreach (Item item in AllItems.Distinct()) + foreach (Item item in AllItems.DistinctBy(it => it.Prefab)) { if (item == null) { continue; } if (!prevItems.Contains(item)) { + int amount = AllItems.Count(it => it.Prefab == item.Prefab && !prevItems.Contains(it)); + string amountText = amount > 1 ? $"x{amount} " : string.Empty; if (Owner == c.Character) { HumanAIController.ItemTaken(item, c.Character); - GameServer.Log(GameServer.CharacterLogName(c.Character) + " picked up " + item.Name, ServerLog.MessageType.Inventory); + GameServer.Log($"{GameServer.CharacterLogName(c.Character)} picked up {amountText}{item.Name}", ServerLog.MessageType.Inventory); } else { - GameServer.Log(GameServer.CharacterLogName(c.Character) + " placed " + item.Name + " in " + Owner, ServerLog.MessageType.Inventory); + GameServer.Log($"{GameServer.CharacterLogName(c.Character)} placed {amountText}{item.Name} in {Owner}", ServerLog.MessageType.Inventory); } } } - foreach (Item item in prevItems.Distinct()) + + var droppedItems = prevItems.Where(it => it != null && !AllItems.Contains(it)); + foreach (Item item in droppedItems.DistinctBy(it => it.Prefab)) { - if (item == null) { continue; } - if (!AllItems.Contains(item)) + var matchingItems = prevItems.Where(it => it.Prefab == item.Prefab && !AllItems.Contains(it)); + int amount = matchingItems.Count(); + string amountText = amount > 1 ? $"x{amount} " : string.Empty; + if (Owner == c.Character) { - if (Owner == c.Character) - { - GameServer.Log(GameServer.CharacterLogName(c.Character) + " dropped " + item.Name, ServerLog.MessageType.Inventory); - } - else - { - GameServer.Log(GameServer.CharacterLogName(c.Character) + " removed " + item.Name + " from " + Owner, ServerLog.MessageType.Inventory); - } + GameServer.Log($"{GameServer.CharacterLogName(c.Character)} dropped {amountText}{item.Name}", ServerLog.MessageType.Inventory); } + else + { + GameServer.Log($"{GameServer.CharacterLogName(c.Character)} removed {amountText}{item.Name} from {Owner}", ServerLog.MessageType.Inventory); + } + item.CreateDroppedStack(matchingItems, allowClientExecute: true); } } @@ -203,10 +217,5 @@ namespace Barotrauma bool IsSlotIndexOutOfBound(int index) => index < 0 || index >= slots.Length; } - - public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) - { - SharedWrite(msg, extraData); - } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index a48f13895..f59311120 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; @@ -18,9 +19,23 @@ namespace Barotrauma get { return base.Prefab?.Sprite; } } - partial void AssignCampaignInteractionTypeProjSpecific(CampaignMode.InteractionType interactionType) + private readonly Dictionary campaignInteractionTypePerClient = new Dictionary(); + + partial void AssignCampaignInteractionTypeProjSpecific(CampaignMode.InteractionType interactionType, IEnumerable targetClients) { - GameMain.NetworkMember.CreateEntityEvent(this, new AssignCampaignInteractionEventData()); + if (Removed) { return; } + if (targetClients == null || targetClients.None()) + { + campaignInteractionTypePerClient.Clear(); + } + else + { + foreach (Client client in targetClients) + { + campaignInteractionTypePerClient[client] = interactionType; + } + } + GameMain.NetworkMember.CreateEntityEvent(this, new AssignCampaignInteractionEventData(targetClients)); } public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) @@ -33,7 +48,7 @@ namespace Barotrauma } if (extraData is null) { throw error("event data was null"); } - if (!(extraData is IEventData itemEventData)) { throw error($"event data was of the wrong type (\"{extraData.GetType().Name}\")"); } + if (extraData is not IEventData itemEventData) { throw error($"event data was of the wrong type (\"{extraData.GetType().Name}\")"); } msg.WriteRangedInteger((int)itemEventData.EventType, (int)EventType.MinValue, (int)EventType.MaxValue); switch (itemEventData) @@ -44,7 +59,7 @@ namespace Barotrauma { throw error($"component index out of range ({componentIndex})"); } - if (!(components[componentIndex] is IServerSerializable serializableComponent)) + if (components[componentIndex] is not IServerSerializable serializableComponent) { throw error($"component \"{components[componentIndex]}\" is not server serializable"); } @@ -57,20 +72,28 @@ namespace Barotrauma { throw error($"container index out of range ({containerIndex})"); } - if (!(components[containerIndex] is ItemContainer itemContainer)) + if (components[containerIndex] is not ItemContainer itemContainer) { throw error("component \"" + components[containerIndex] + "\" is not server serializable"); } msg.WriteRangedInteger(containerIndex, 0, components.Count - 1); msg.WriteUInt16(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0); - itemContainer.Inventory.ServerEventWrite(msg, c); + itemContainer.Inventory.ServerEventWrite(msg, c, inventoryStateEventData); break; case ItemStatusEventData statusEvent: msg.WriteBoolean(statusEvent.LoadingRound); msg.WriteSingle(condition); break; - case AssignCampaignInteractionEventData _: - msg.WriteByte((byte)CampaignInteractionType); + case AssignCampaignInteractionEventData campaignInteractionData: + bool isVisibleToClient = + campaignInteractionData.TargetClients == null || + campaignInteractionData.TargetClients.IsEmpty || + campaignInteractionData.TargetClients.Contains(c); + msg.WriteBoolean(isVisibleToClient); + if (isVisibleToClient) + { + msg.WriteByte((byte)CampaignInteractionType); + } break; case ApplyStatusEffectEventData applyStatusEffectEventData: { @@ -131,6 +154,13 @@ namespace Barotrauma } } break; + case DroppedStackEventData droppedStackEventData: + msg.WriteRangedInteger(droppedStackEventData.Items.Length, 0, Inventory.MaxPossibleStackSize); + foreach (Item droppedItem in droppedStackEventData.Items) + { + msg.WriteUInt16(droppedItem.ID); + } + break; default: throw error($"Unsupported event type {itemEventData.GetType().Name}"); } @@ -257,10 +287,10 @@ namespace Barotrauma msg.WriteInt32(idCardComponent.SubmarineSpecificID); msg.WriteString(idCardComponent.OwnerName); msg.WriteString(idCardComponent.OwnerTags); - msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerBeardIndex+1)); - msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerHairIndex+1)); - msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerMoustacheIndex+1)); - msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerFaceAttachmentIndex+1)); + msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerBeardIndex + 1)); + msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerHairIndex + 1)); + msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerMoustacheIndex + 1)); + msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerFaceAttachmentIndex + 1)); msg.WriteColorR8G8B8(idCardComponent.OwnerHairColor); msg.WriteColorR8G8B8(idCardComponent.OwnerFacialHairColor); msg.WriteColorR8G8B8(idCardComponent.OwnerSkinColor); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/ItemEventData.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/ItemEventData.cs new file mode 100644 index 000000000..b2a6b0dda --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/ItemEventData.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Barotrauma; + +partial class Item +{ + private readonly struct DroppedStackEventData : IEventData + { + public EventType EventType => EventType.DroppedStack; + public readonly ImmutableArray Items; + + public DroppedStackEventData(IEnumerable items) + { + Items = items.Distinct().ToImmutableArray(); + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/ItemInventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/ItemInventory.cs new file mode 100644 index 000000000..0c87880d4 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/ItemInventory.cs @@ -0,0 +1,13 @@ +using Barotrauma.Networking; +using System; + +namespace Barotrauma +{ + partial class ItemInventory : Inventory + { + public void ServerEventWrite(IWriteMessage msg, Client c, Item.InventoryStateEventData inventoryData) + { + SharedWrite(msg, inventoryData.SlotRange); + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs index 74ca52bb7..bdd772286 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs @@ -176,6 +176,7 @@ namespace Barotrauma BackgroundSections[i].SetColor(color); }, out int sectorToUpdate); + RefreshAveragePaintedColor(); //add to pending updates to notify other clients as well pendingSectionUpdates.Add(sectorToUpdate); break; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs index c6d6559cc..f8f8d2bbd 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs @@ -42,8 +42,6 @@ namespace Barotrauma.Networking public string RejectedName; - public int RoundsSincePlayedAsTraitor; - public float KickAFKTimer; public double MidRoundSyncTimeOut; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs index 318f77ff8..0535add38 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs @@ -49,7 +49,7 @@ namespace Barotrauma.Networking { await Task.Yield(); string dir = mod.Dir; - SaveUtil.CompressDirectory(dir, GetCompressedModPath(mod), fileName => { }); + SaveUtil.CompressDirectory(dir, GetCompressedModPath(mod)); } private void DeleteDir() diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index d10033fc6..34f6cd497 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -74,13 +74,21 @@ namespace Barotrauma.Networking private bool initiatedStartGame; private CoroutineHandle startGameCoroutine; - public TraitorManager TraitorManager; - private readonly ServerEntityEventManager entityEventManager; public FileSender FileSender { get; private set; } public ModSender ModSender { get; private set; } + + private TraitorManager traitorManager; + public TraitorManager TraitorManager + { + get + { + traitorManager ??= new TraitorManager(this); + return traitorManager; + } + } #if DEBUG public void PrintSenderTransters() @@ -358,22 +366,31 @@ namespace Barotrauma.Networking for (int i = Character.CharacterList.Count - 1; i >= 0; i--) { Character character = Character.CharacterList[i]; - if (character.IsDead || !character.ClientDisconnected) { continue; } - - character.KillDisconnectedTimer += deltaTime; - character.SetStun(1.0f); + if (!character.ClientDisconnected) { continue; } Client owner = connectedClients.Find(c => (c.Character == null || c.Character == character) && character.IsClientOwner(c)); - if ((OwnerConnection == null || owner?.Connection != OwnerConnection) && character.KillDisconnectedTimer > ServerSettings.KillDisconnectedTime) + if (!character.IsDead) { - character.Kill(CauseOfDeathType.Disconnected, null); - continue; - } + character.KillDisconnectedTimer += deltaTime; + character.SetStun(1.0f); - if (owner != null && owner.InGame && !owner.NeedsMidRoundSync && - (!ServerSettings.AllowSpectating || !owner.SpectateOnly)) + if ((OwnerConnection == null || owner?.Connection != OwnerConnection) && character.KillDisconnectedTimer > ServerSettings.KillDisconnectedTime) + { + character.Kill(CauseOfDeathType.Disconnected, null); + continue; + } + if (owner != null && owner.InGame && !owner.NeedsMidRoundSync && + (!ServerSettings.AllowSpectating || !owner.SpectateOnly)) + { + SetClientCharacter(owner, character); + } + } + else if (owner != null && + character.CauseOfDeath?.Type == CauseOfDeathType.Disconnected && + character.CharacterHealth.VitalityDisregardingDeath > 0) { SetClientCharacter(owner, character); + character.Revive(removeAfflictions: false); } } @@ -412,12 +429,7 @@ namespace Barotrauma.Networking } float endRoundDelay = 1.0f; - if (TraitorManager?.ShouldEndRound ?? false) - { - endRoundDelay = 5.0f; - endRoundTimer += deltaTime; - } - else if (ServerSettings.AutoRestart && isCrewDead) + if (ServerSettings.AutoRestart && isCrewDead) { endRoundDelay = 5.0f; endRoundTimer += deltaTime; @@ -452,11 +464,7 @@ namespace Barotrauma.Networking if (endRoundTimer >= endRoundDelay) { - if (TraitorManager?.ShouldEndRound ?? false) - { - Log("Ending round (a traitor completed their mission)", ServerLog.MessageType.ServerMessage); - } - else if (ServerSettings.AutoRestart && isCrewDead) + if (ServerSettings.AutoRestart && isCrewDead) { Log("Ending round (entire crew dead)", ServerLog.MessageType.ServerMessage); } @@ -830,6 +838,9 @@ namespace Barotrauma.Networking case ClientPacketHeader.MEDICAL: ReadMedicalMessage(inc, connectedClient); break; + case ClientPacketHeader.CIRCUITBOX: + ReadCircuitBoxMessage(inc, connectedClient); + break; case ClientPacketHeader.READY_CHECK: ReadyCheck.ServerRead(inc, connectedClient); break; @@ -1299,6 +1310,22 @@ namespace Barotrauma.Networking } } + private static void ReadCircuitBoxMessage(IReadMessage inc, Client sender) + { + var header = INetSerializableStruct.Read(inc); + + INetSerializableStruct data = header.Opcode switch + { + CircuitBoxOpcode.Cursor => INetSerializableStruct.Read(inc), + _ => throw new ArgumentOutOfRangeException(nameof(header.Opcode), header.Opcode, "This data cannot be handled using direct network messages.") + }; + + if (header.FindTarget().TryUnwrap(out var box)) + { + box.ServerRead(data, sender); + } + } + private void ReadReadyToSpawnMessage(IReadMessage inc, Client sender) { sender.SpectateOnly = inc.ReadBoolean() && (ServerSettings.AllowSpectating || sender.Connection == OwnerConnection); @@ -1775,7 +1802,7 @@ namespace Barotrauma.Networking while (!c.NeedsMidRoundSync && c.PendingPositionUpdates.Count > 0) { var entity = c.PendingPositionUpdates.Peek(); - if (!(entity is IServerPositionSync entityPositionSync) || + if (entity is not IServerPositionSync entityPositionSync || entity.Removed || (entity is Item item && float.IsInfinity(item.PositionUpdateInterval))) { @@ -1957,7 +1984,8 @@ namespace Barotrauma.Networking outmsg.WriteBoolean(ServerSettings.AllowSpectating); - outmsg.WriteRangedInteger((int)ServerSettings.TraitorsEnabled, 0, 2); + outmsg.WriteSingle(ServerSettings.TraitorProbability); + outmsg.WriteRangedInteger(ServerSettings.TraitorDangerLevel, TraitorEventPrefab.MinDangerLevel, TraitorEventPrefab.MaxDangerLevel); outmsg.WriteRangedInteger((int)GameMain.NetLobbyScreen.MissionType, 0, (int)MissionType.All); @@ -2212,6 +2240,7 @@ namespace Barotrauma.Networking //don't instantiate a new gamesession if we're playing a campaign if (campaign == null || GameMain.GameSession == null) { + traitorManager = new TraitorManager(this); GameMain.GameSession = new GameSession(selectedSub, "", selectedMode, settings, GameMain.NetLobbyScreen.LevelSeed, missionType: GameMain.NetLobbyScreen.MissionType); } else @@ -2509,16 +2538,8 @@ namespace Barotrauma.Networking } } - TraitorManager = null; - if (ServerSettings.TraitorsEnabled == YesNoMaybe.Yes || - (ServerSettings.TraitorsEnabled == YesNoMaybe.Maybe && Rand.Range(0.0f, 1.0f) < 0.5f)) - { - if (!(GameMain.GameSession?.GameMode is CampaignMode)) - { - TraitorManager = new TraitorManager(); - TraitorManager.Start(this); - } - } + TraitorManager.Initialize(GameMain.GameSession.EventManager, Level.Loaded); + TraitorManager.Enabled = Rand.Range(0.0f, 1.0f) < ServerSettings.TraitorProbability; GameAnalyticsManager.AddDesignEvent("Traitors:" + (TraitorManager == null ? "Disabled" : "Enabled")); @@ -2562,7 +2583,6 @@ namespace Barotrauma.Networking msg.WriteBoolean(ServerSettings.AllowRewiring); msg.WriteBoolean(ServerSettings.AllowFriendlyFire); msg.WriteBoolean(ServerSettings.LockAllDefaultWires); - msg.WriteBoolean(ServerSettings.AllowRagdollButton); msg.WriteBoolean(ServerSettings.AllowLinkingWifiToChat); msg.WriteInt32(ServerSettings.MaximumMoneyTransferRequest); msg.WriteBoolean(IsUsingRespawnShuttle()); @@ -2685,13 +2705,12 @@ namespace Barotrauma.Networking } string endMessage = TextManager.FormatServerMessage("RoundSummaryRoundHasEnded"); - var traitorResults = TraitorManager?.GetEndResults() ?? new List(); - List missions = GameMain.GameSession.Missions.ToList(); if (GameMain.GameSession is { IsRunning: true }) { - GameMain.GameSession.EndRound(endMessage, traitorResults); + GameMain.GameSession.EndRound(endMessage); } + TraitorManager.TraitorResults? traitorResults = traitorManager?.GetEndResults() ?? null; endRoundTimer = 0.0f; @@ -2736,10 +2755,10 @@ namespace Barotrauma.Networking } msg.WriteByte(GameMain.GameSession?.WinningTeam == null ? (byte)0 : (byte)GameMain.GameSession.WinningTeam); - msg.WriteByte((byte)traitorResults.Count); - foreach (var traitorResult in traitorResults) + msg.WriteBoolean(traitorResults.HasValue); + if (traitorResults.HasValue) { - traitorResult.ServerWrite(msg); + msg.WriteNetSerializableStruct(traitorResults.Value); } foreach (Client client in connectedClients) @@ -2788,13 +2807,14 @@ namespace Barotrauma.Networking if (c == null || string.IsNullOrEmpty(newName) || !NetIdUtils.IdMoreRecent(nameId, c.NameId)) { return false; } var timeSinceNameChange = DateTime.Now - c.LastNameChangeTime; - if (timeSinceNameChange < Client.NameChangeCoolDown) + if (timeSinceNameChange < Client.NameChangeCoolDown && newName != c.Name) { //only send once per second at most to prevent using this for spamming if (timeSinceNameChange.TotalSeconds > 1) { var coolDownRemaining = Client.NameChangeCoolDown - timeSinceNameChange; SendDirectChatMessage($"ServerMessage.NameChangeFailedCooldownActive~[seconds]={(int)coolDownRemaining.TotalSeconds}", c); + LastClientListUpdateID++; } c.NameId = nameId; c.RejectedName = newName; @@ -3558,14 +3578,9 @@ namespace Barotrauma.Networking serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } - public void SendTraitorMessage(Client client, string message, Identifier missionIdentifier, TraitorMessageType messageType) + public void SendTraitorMessage(WriteOnlyMessage msg, Client client) { - if (client == null) { return; } - var msg = new WriteOnlyMessage(); - msg.WriteByte((byte)ServerPacketHeader.TRAITOR_MESSAGE); - msg.WriteByte((byte)messageType); - msg.WriteIdentifier(missionIdentifier); - msg.WriteString(message); + if (client == null) { return; }; serverPeer.Send(msg, client.Connection, DeliveryMethod.ReliableOrdered); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs index 737e9555d..8c0fceec9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs @@ -240,19 +240,20 @@ namespace Barotrauma { Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == inventory.Owner); - Character yoinkerCharacter = yoinker?.Character; + Character thiefCharacter = yoinker?.Character; Character targetCharacter = inventory.Owner as Character; - if (yoinker == null || item == null || yoinkerCharacter == null || targetCharacter == null || yoinkerCharacter == targetCharacter) { return; } + if (yoinker == null || item == null || thiefCharacter == null || targetCharacter == null || thiefCharacter == targetCharacter) { return; } if (targetClient == null && (!DangerousItemStealBots || targetCharacter.AIController == null)) { return; } // Only if the target is alive and they are stunned, unconscious or handcuffed if (targetCharacter.IsDead || targetCharacter.Removed || !(targetCharacter.Stun > 0) && !targetCharacter.IsUnconscious && !targetCharacter.LockHands) { return; } - if (GameMain.Server.TraitorManager?.Traitors != null) + if (GameMain.Server.TraitorManager != null) { - if (GameMain.Server.TraitorManager.Traitors.Any(t => t.Character == targetCharacter || t.Character == yoinkerCharacter)) + if (GameMain.Server.TraitorManager.IsTraitor(targetCharacter) || + GameMain.Server.TraitorManager.IsTraitor(thiefCharacter)) { // Don't penalize traitors return; @@ -299,7 +300,7 @@ namespace Barotrauma } // Name tag doesn't belong to anyone in particular or we own the ID card - if (name == null || name == yoinkerCharacter.Name) { return; } + if (name == null || name == thiefCharacter.Name) { return; } } if (MathUtils.NearlyEqual(DangerousItemStealKarmaDecrease, 0)) { return; } @@ -329,7 +330,7 @@ namespace Barotrauma karmaDecrease *= 0.5f; } - AdjustKarma(yoinkerCharacter, -karmaDecrease, "Stolen dangerous item"); + AdjustKarma(thiefCharacter, -karmaDecrease, "Stolen dangerous item"); } public void OnCharacterHealthChanged(Character target, Character attacker, float damage, float stun, IEnumerable appliedAfflictions = null) @@ -341,27 +342,23 @@ namespace Barotrauma if (target.IsDead || target.Removed) { return; } bool isEnemy = target.AIController is EnemyAIController || target.TeamID != attacker.TeamID; - if (GameMain.Server.TraitorManager?.Traitors != null) + if (GameMain.Server.TraitorManager != null) { - if (GameMain.Server.TraitorManager.Traitors.Any(t => t.Character == target)) + if (GameMain.Server.TraitorManager.IsTraitor(target)) { //traitors always count as enemies isEnemy = true; } - if (GameMain.Server.TraitorManager.Traitors.Any(t => - t.Character == attacker && - t.CurrentObjective != null && - t.CurrentObjective.IsEnemy(target))) + if (GameMain.Server.TraitorManager.IsTraitor(attacker)) { - //target counts as an enemy to the traitor + //others count as an enemies to the traitor isEnemy = true; } } - bool targetIsHusk = target.CharacterHealth?.GetAffliction(AfflictionPrefab.HuskInfectionType)?.State == AfflictionHusk.InfectionState.Active; - bool attackerIsHusk = attacker.CharacterHealth?.GetAffliction(AfflictionPrefab.HuskInfectionType)?.State == AfflictionHusk.InfectionState.Active; + static bool IsHusk(Character c) => c.IsHusk || c.IsHuskInfected; //huskified characters count as enemies to healthy characters and vice versa - if (targetIsHusk != attackerIsHusk) { isEnemy = true; } + if (IsHusk(attacker) != IsHusk(target)) { isEnemy = true; } if (appliedAfflictions != null) { @@ -478,14 +475,11 @@ namespace Barotrauma if (damageAmount > 0) { if (StructureDamageKarmaDecrease <= 0.0f) { return; } - if (GameMain.Server.TraitorManager?.Traitors != null) + if (GameMain.Server.TraitorManager != null) { - if (GameMain.Server.TraitorManager.Traitors.Any(t => - t.Character == attacker && - t.CurrentObjective != null && - t.CurrentObjective.IsAllowedToDamage(structure))) + if (GameMain.Server.TraitorManager.IsTraitor(attacker)) { - //traitor tasked to flood the sub -> damaging structures is ok + //traitors are allowed to damage structures return; } } @@ -580,7 +574,7 @@ namespace Barotrauma public void OnItemContained(Item containedItem, Item container, Character character) { if (containedItem == null || container == null || character == null || character.IsTraitor) { return; } - if (container.Prefab.Identifier == "weldingtool" && containedItem.HasTag("oxygensource")) + if (container.Prefab.Identifier == Tags.WeldingFuel && containedItem.HasTag(Tags.OxygenSource)) { var client = GameMain.Server.ConnectedClients.Find(c => c.Character == character); if (client == null) { return; } @@ -614,7 +608,7 @@ namespace Barotrauma if (amount < 0.0f) { - float? herpesStrength = client.Character?.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.SpaceHerpesType); + float? herpesStrength = client.Character?.CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.SpaceHerpesType); var clientMemory = GetClientMemory(client); clientMemory.KarmaDecreasesInPastMinute.RemoveAll(ta => ta.Time + 60.0f < Timing.TotalTime); float aggregate = clientMemory.KarmaDecreasesInPastMinute.Select(ta => ta.Amount).DefaultIfEmpty().Aggregate((a, b) => a + b); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs index b60d34661..eaaf5e1dc 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs @@ -27,7 +27,8 @@ namespace Barotrauma.Networking AutoExpandMTU = false, MaximumConnections = NetConfig.MaxPlayers * 2, EnableUPnP = serverSettings.EnableUPnP, - Port = serverSettings.Port + Port = serverSettings.Port, + DualStack = GameSettings.CurrentConfig.UseDualModeSockets }; netPeerConfiguration.DisableMessageType( diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index bc268f9dc..187f0f1af 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -433,9 +433,9 @@ namespace Barotrauma.Networking } //tell the respawning client they're no longer a traitor - if (GameMain.Server.TraitorManager?.Traitors != null && clients[i].Character != null) + if (GameMain.Server.TraitorManager != null && clients[i].Character != null) { - if (GameMain.Server.TraitorManager.Traitors.Any(t => t.Character == clients[i].Character)) + if (GameMain.Server.TraitorManager.IsTraitor(clients[i].Character)) { GameMain.Server.SendDirectChatMessage(TextManager.FormatServerMessage("TraitorRespawnMessage"), clients[i], ChatMessageType.ServerMessageBox); } @@ -537,18 +537,7 @@ namespace Barotrauma.Networking } //add the ID card tags they should've gotten when spawning in the shuttle - foreach (Item item in character.Inventory.AllItems.Distinct()) - { - if (item.GetComponent() == null) { continue; } - foreach (string s in shuttleSpawnPoints[i].IdCardTags) - { - item.AddTag(s); - } - if (!string.IsNullOrWhiteSpace(shuttleSpawnPoints[i].IdCardDesc)) - { - item.Description = shuttleSpawnPoints[i].IdCardDesc; - } - } + character.GiveIdCardTags(shuttleSpawnPoints[i], requireSpawnPointTagsNotGiven: false, createNetworkEvent: true); } } @@ -559,7 +548,7 @@ namespace Barotrauma.Networking { var skillPrefab = characterInfo.Job.Prefab.Skills.Find(s => skill.Identifier == s.Identifier); if (skillPrefab == null || skill.Level < skillPrefab.LevelRange.End) { continue; } - skill.Level = MathHelper.Lerp(skill.Level, skillPrefab.LevelRange.End, SkillReductionOnDeath); + skill.Level = MathHelper.Lerp(skill.Level, skillPrefab.LevelRange.End, SkillLossPercentageOnDeath / 100.0f); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index 185b5aad5..ec0e8e101 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -215,11 +215,15 @@ namespace Barotrauma.Networking int orBits = incMsg.ReadRangedInteger(0, (int)Barotrauma.MissionType.All) & (int)Barotrauma.MissionType.All; int andBits = incMsg.ReadRangedInteger(0, (int)Barotrauma.MissionType.All) & (int)Barotrauma.MissionType.All; GameMain.NetLobbyScreen.MissionType = (MissionType)(((int)GameMain.NetLobbyScreen.MissionType | orBits) & andBits); - - int traitorSetting = (int)TraitorsEnabled + incMsg.ReadByte() - 1; - if (traitorSetting < 0) { traitorSetting = 2; } - if (traitorSetting > 2) { traitorSetting = 0; } - TraitorsEnabled = (YesNoMaybe)traitorSetting; + + bool changedTraitorProbability = incMsg.ReadBoolean(); + float traitorProbability = incMsg.ReadSingle(); + if (changedTraitorProbability) + { + TraitorProbability = traitorProbability; + } + //the byte indicates the direction we're changing the value, subtract one to get negative values from a byte + TraitorDangerLevel = TraitorDangerLevel + incMsg.ReadByte() - 1; int botCount = BotCount + incMsg.ReadByte() - 1; if (botCount < 0) { botCount = MaxBotCount; } @@ -347,7 +351,7 @@ namespace Barotrauma.Networking selectedLevelDifficulty = doc.Root.GetAttributeFloat("LevelDifficulty", 20.0f); GameMain.NetLobbyScreen.SetLevelDifficulty(selectedLevelDifficulty); - GameMain.NetLobbyScreen.SetTraitorsEnabled(traitorsEnabled); + GameMain.NetLobbyScreen.SetTraitorProbability(traitorProbability); HiddenSubs.UnionWith(doc.Root.GetAttributeStringArray("HiddenSubs", Array.Empty())); if (HiddenSubs.Any()) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs index c6520b8fa..5afb8d321 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs @@ -337,6 +337,20 @@ namespace Barotrauma sender.SetVote(voteType, (int)inc.ReadByte()); } break; + case VoteType.Traitor: + int clientId = inc.ReadInt32(); + if (sender.InGame && sender.Character != null) + { + var client = GameMain.Server.ConnectedClients.FirstOrDefault(c => c.SessionId == clientId); + sender.SetVote(voteType, client); + if (client?.Character != null) + { + GameMain.Server.SendChatMessage( + TextManager.GetWithVariable("traitor.blamebutton.dialog", "[name]", client.Character.DisplayName).Value, + ChatMessageType.Radio, senderClient: sender, senderCharacter: sender.Character); + } + } + break; } inc.ReadPadBits(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs index 56b57530c..e0967195b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System.Globalization; +using System.Linq; using Barotrauma.Networking; namespace Barotrauma.Steam @@ -71,7 +72,7 @@ namespace Barotrauma.Steam Steamworks.SteamServer.SetKey("voicechatenabled", server.ServerSettings.VoiceChatEnabled.ToString()); Steamworks.SteamServer.SetKey("allowspectating", server.ServerSettings.AllowSpectating.ToString()); Steamworks.SteamServer.SetKey("allowrespawn", server.ServerSettings.AllowRespawn.ToString()); - Steamworks.SteamServer.SetKey("traitors", server.ServerSettings.TraitorsEnabled.ToString()); + Steamworks.SteamServer.SetKey("traitors", server.ServerSettings.TraitorProbability.ToString(CultureInfo.InvariantCulture)); Steamworks.SteamServer.SetKey("friendlyfireenabled", server.ServerSettings.AllowFriendlyFire.ToString()); Steamworks.SteamServer.SetKey("karmaenabled", server.ServerSettings.KarmaEnabled.ToString()); Steamworks.SteamServer.SetKey("gamestarted", server.GameStarted.ToString()); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Goal.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Goal.cs deleted file mode 100644 index 37894b947..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Goal.cs +++ /dev/null @@ -1,64 +0,0 @@ -using Barotrauma.Networking; -using System.Collections.Generic; -using System.Linq; -using Microsoft.SqlServer.Server; - -namespace Barotrauma -{ - partial class Traitor - { - public abstract class Goal - { - public HashSet Traitors { get; } = new HashSet(); - public TraitorMission Mission { get; internal set; } - - public virtual string StatusTextId { get; set; } = "TraitorGoalStatusTextFormat"; - - public virtual string InfoTextId { get; set; } = null; - - public virtual string CompletedTextId { get; set; } = null; - - public virtual string StatusValueTextId => IsCompleted ? "complete" : "inprogress"; - - public virtual IEnumerable StatusTextKeys => new [] { "[infotext]", "[status]" }; - public virtual IEnumerable StatusTextValues(Traitor traitor) => new [] { InfoText(traitor), TextManager.FormatServerMessage(StatusValueTextId) }; - - public virtual IEnumerable InfoTextKeys => new string[] { }; - public virtual IEnumerable InfoTextValues(Traitor traitor) => new string[] { }; - - public virtual IEnumerable CompletedTextKeys => new string[] { }; - public virtual IEnumerable CompletedTextValues(Traitor traitor) => new string[] { }; - - protected virtual string FormatText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) - => TextManager.FormatServerMessageWithPronouns(traitor.Character.Info, textId, keys.Zip(values, (k,v) => (k,v)).ToArray()); - - protected internal virtual string GetStatusText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) => FormatText(traitor, textId, keys, values); - protected internal virtual string GetInfoText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) => FormatText(traitor, textId, keys, values); - protected internal virtual string GetCompletedText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) => FormatText(traitor, textId, keys, values); - - public virtual string StatusText(Traitor traitor) => GetStatusText(traitor, StatusTextId, StatusTextKeys, StatusTextValues(traitor)); - public virtual string InfoText(Traitor traitor) => GetInfoText(traitor, InfoTextId, InfoTextKeys, InfoTextValues(traitor)); - - public virtual string CompletedText(Traitor traitor) => CompletedTextId != null ? GetCompletedText(traitor, CompletedTextId, CompletedTextKeys, CompletedTextValues(traitor)) : StatusText(traitor); - - public abstract bool IsCompleted { get; } - public virtual bool IsStarted(Traitor traitor) => Traitors.Contains(traitor); - public virtual bool CanBeCompleted(ICollection traitors) => !Traitors.Any(traitor => traitor.Character?.IsDead ?? true); - public virtual bool IsEnemy(Character character) => false; - public virtual bool IsAllowedToDamage(Structure structure) => false; - public virtual bool Start(Traitor traitor) - { - Traitors.Add(traitor); - return true; - } - - public virtual void Update(float deltaTime) - { - } - - protected Goal() - { - } - } - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalDestroyItemsWithTag.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalDestroyItemsWithTag.cs deleted file mode 100644 index 6fd116862..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalDestroyItemsWithTag.cs +++ /dev/null @@ -1,107 +0,0 @@ -using Barotrauma.Networking; -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma -{ - partial class Traitor - { - public sealed class GoalDestroyItemsWithTag : Goal - { - private readonly string tag; - private readonly bool matchIdentifier; - private readonly bool matchTag; - private readonly bool matchInventory; - - public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[percentage]", "[tag]" }); - public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { string.Format("{0:0}", DestroyPercent * 100.0f), tagPrefabName ?? "" }); - - private readonly float destroyPercent; - private float DestroyPercent => destroyPercent; - - private bool isCompleted = false; - public override bool IsCompleted => isCompleted; - - private int totalCount = 0; - private int targetCount = 0; - private string tagPrefabName = null; - - private int CountMatchingItems() - { - int result = 0; - foreach (var item in Item.ItemList) - { - if (!matchInventory && Traitors.All(traitor => item.FindParentInventory(inventory => inventory.Owner is Character && inventory.Owner != traitor.Character) != null)) - { - continue; - } - - if (item.Submarine == null) - { - //items outside the sub don't count as destroyed if they're still in the traitor's inventory - bool carriedByTraitor = Traitors.Any(traitor => item.IsOwnedBy(traitor.Character)); - if (!carriedByTraitor) { continue; } - } - else - { - if (Traitors.All(traitor => item.Submarine.TeamID != traitor.Character.TeamID)) { continue; } - } - - if (item.Condition <= 0.0f) - { - continue; - } - var identifierMatches = matchIdentifier && ((MapEntity)item).Prefab.Identifier == tag; - if (identifierMatches && tagPrefabName == null) - { - var textId = item.Prefab.GetItemNameTextId(); - tagPrefabName = textId != null ? TextManager.FormatServerMessage(textId) : item.Prefab.Name.Value; - } - if (identifierMatches || (matchTag && item.HasTag(tag))) - { - ++result; - } - } - - // Quick fix - if (tagPrefabName == null && matchIdentifier) - { - tagPrefabName = TextManager.FormatServerMessage($"entityname.{tag}"); - } - - return result; - } - - public override void Update(float deltaTime) - { - base.Update(deltaTime); - isCompleted = CountMatchingItems() <= targetCount; - } - - public override bool Start(Traitor traitor) - { - if (!base.Start(traitor)) - { - return false; - } - totalCount = CountMatchingItems(); - if (totalCount <= 0) - { - return false; - } - targetCount = (int)((1.0f - destroyPercent) * totalCount - 0.5f); - return true; - } - - public GoalDestroyItemsWithTag(string tag, float destroyPercent, bool matchTag, bool matchIdentifier, bool matchInventory) : base() - { - InfoTextId = "TraitorGoalDestroyItems"; - this.tag = tag; - this.destroyPercent = destroyPercent; - this.matchTag = matchTag; - this.matchIdentifier = matchIdentifier; - this.matchInventory = matchInventory; - } - } - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalEntityTransformation.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalEntityTransformation.cs deleted file mode 100644 index e9977fa73..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalEntityTransformation.cs +++ /dev/null @@ -1,162 +0,0 @@ -using Barotrauma.Networking; -using Microsoft.Xna.Framework; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma -{ - partial class Traitor - { - public sealed class GoalEntityTransformation : Goal - { - public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[catalystitem]" }); - public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { catalystItemName }); - - private bool isCompleted; - public override bool IsCompleted => isCompleted; - - private string catalystItemIdentifier, catalystItemName; - - private Vector2 activeEntitySavedPosition; - private Entity activeEntity; - private int activeEntityIndex; - private const float gracePeriod = 1f; - private const float graceDistance = 200f; - private float graceTimer; - private double transformationTime; - - private enum EntityTypes { Character, Item } - - private Identifier[] entities; - private EntityTypes[] entityTypes; - - public override void Update(float deltaTime) - { - base.Update(deltaTime); - isCompleted = HasTransformed(deltaTime); - } - - public override bool CanBeCompleted(ICollection traitors) - { - return graceTimer <= gracePeriod; - } - - private bool HasTransformed(float deltaTime) - { - if (activeEntity != null && !activeEntity.Removed) - { - activeEntitySavedPosition = activeEntity.WorldPosition; - } - else - { - if (transformationTime == 0) - { - graceTimer = 0.0f; - activeEntityIndex++; - transformationTime = Timing.TotalTime; - } - graceTimer += deltaTime; - - switch (entityTypes[activeEntityIndex]) - { - case EntityTypes.Character: - foreach (Character character in Character.CharacterList) - { - if (character.Submarine == null || Traitors.All(t => character.Submarine.TeamID != t.Character.TeamID) || character.SpawnTime + gracePeriod < transformationTime) - { - continue; - } - if (character.SpeciesName == entities[activeEntityIndex] && Vector2.Distance(activeEntitySavedPosition, character.WorldPosition) < graceDistance) - { - activeEntity = character; - transformationTime = 0.0; - return activeEntityIndex == entities.Length - 1; - } - } - break; - case EntityTypes.Item: - foreach (Item item in Item.ItemList) - { - if (item.Submarine == null || Traitors.All(t => item.Submarine.TeamID != t.Character.TeamID) || item.SpawnTime + gracePeriod < transformationTime) - { - continue; - } - if (((MapEntity)item).Prefab.Identifier == entities[activeEntityIndex] && Vector2.Distance(activeEntitySavedPosition, item.WorldPosition) < graceDistance) - { - activeEntity = item; - transformationTime = 0.0; - return activeEntityIndex == entities.Length - 1; - } - } - break; - } - } - - return false; - } - - public override bool Start(Traitor traitor) - { - if (!base.Start(traitor)) - { - return false; - } - - catalystItemName = TextManager.FormatServerMessage($"entityname.{catalystItemIdentifier}"); - - activeEntity = null; - activeEntityIndex = 0; - - switch (entityTypes[activeEntityIndex]) - { - case EntityTypes.Character: - foreach (Character character in Character.CharacterList) - { - if (character.Submarine == null || Traitors.All(t => character.Submarine.TeamID != t.Character.TeamID)) - { - continue; - } - if (character.SpeciesName == entities[activeEntityIndex]) - { - activeEntity = character; - break; - } - } - break; - case EntityTypes.Item: - foreach (Item item in Item.ItemList) - { - if (item.Submarine == null || Traitors.All(t => item.Submarine.TeamID != t.Character.TeamID)) - { - continue; - } - if (((MapEntity)item).Prefab.Identifier == entities[0]) - { - activeEntity = item; - break; - } - } - break; - } - - graceTimer = 0.0f; - return activeEntity != null; - } - - public GoalEntityTransformation(string[] entities, string[] entityTypes, string catalystItemIdentifier) : base() - { - this.entities = entities.ToIdentifiers().ToArray(); - - this.entityTypes = new EntityTypes[entityTypes.Length]; - - for (int i = 0; i < this.entityTypes.Length; i++) - { - this.entityTypes[i] = (EntityTypes)Enum.Parse(typeof(EntityTypes), entityTypes[i], true); - } - - this.catalystItemIdentifier = catalystItemIdentifier; - } - } - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFindItem.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFindItem.cs deleted file mode 100644 index 57871e224..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFindItem.cs +++ /dev/null @@ -1,239 +0,0 @@ -using Barotrauma.Items.Components; -using Barotrauma.Networking; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma -{ - partial class Traitor - { - public class GoalFindItem : HumanoidGoal - { - private readonly TraitorMission.CharacterFilter filter; - private readonly string identifier; - private readonly bool preferNew; - private readonly bool allowNew; - private readonly bool allowExisting; - private readonly HashSet allowedContainerIdentifiers = new HashSet(); - - private ItemPrefab targetPrefab; - private ItemPrefab containedPrefab; - private Item targetContainer; - private Item target; - private HashSet existingItems = new HashSet(); - private string targetNameText; - private string targetContainerNameText; - private string targetHullNameText; - private float percentage; - private int spawnAmount = 1; - - private const string itemContainerId = "toolbox"; - - public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[identifier]", "[target]", "[targethullname]" }); - public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { targetNameText ?? "", targetContainerNameText ?? "", targetHullNameText ?? "" }); - - public override bool IsCompleted => target != null && Traitors.Any(traitor => traitor.Character.HasItem(target)); - public override bool CanBeCompleted(ICollection traitors) - { - if (!base.CanBeCompleted(traitors)) - { - return false; - } - if (target == null) - { - var targetPrefabCandidate = FindItemPrefab(identifier); - return targetPrefabCandidate != null && FindTargetContainer(traitors, targetPrefabCandidate) != null; - } - if (target.Removed) - { - return false; - } - if (target.Submarine == null) - { - if (!(target.ParentInventory?.Owner is Character)) - { - return false; - } - } - else - { - if (Traitors.All(traitor => target.Submarine.TeamID != traitor.Character.TeamID)) - { - return false; - } - } - return true; - } - - public override bool IsEnemy(Character character) => base.IsEnemy(character) || (target != null && target.FindParentInventory(inventory => inventory == character.Inventory) != null); - - protected ItemPrefab FindItemPrefab(string identifier) - { - return (ItemPrefab)MapEntityPrefab.List.FirstOrDefault(prefab => prefab is ItemPrefab && prefab.Identifier == identifier); - } - - protected Item FindRandomContainer(ICollection traitors, ItemPrefab targetPrefabCandidate, bool includeNew, bool includeExisting) - { - List suitableItems = new List(); - foreach (Item item in Item.ItemList) - { - if (item.HiddenInGame || item.NonInteractable || item.NonPlayerTeamInteractable) { continue; } - if (item.Submarine == null || traitors.All(traitor => item.Submarine.TeamID != traitor.Character.TeamID)) - { - continue; - } - if (item.GetComponent() != null && allowedContainerIdentifiers.Contains(((MapEntity)item).Prefab.Identifier)) - { - if ((includeNew && !item.OwnInventory.IsFull()) || (includeExisting && item.OwnInventory.FindItemByIdentifier(targetPrefabCandidate.Identifier) != null)) - { - suitableItems.Add(item); - } - } - } - - if (suitableItems.Count == 0) { return null; } - return suitableItems[TraitorManager.RandomInt(suitableItems.Count)]; - } - - protected Item FindTargetContainer(ICollection traitors, ItemPrefab targetPrefabCandidate) - { - Item result = null; - if (preferNew) - { - result = FindRandomContainer(traitors, targetPrefabCandidate, true, false); - } - if (result == null) - { - result = FindRandomContainer(traitors, targetPrefabCandidate, allowNew, allowExisting); - } - if (result == null) - { - return null; - } - if (allowNew && !result.OwnInventory.IsFull()) - { - return result; - } - if (allowExisting && result.OwnInventory.FindItemByIdentifier(targetPrefabCandidate.Identifier) != null) - { - return result; - } - return null; - } - - public override bool Start(Traitor traitor) - { - if (!base.Start(traitor)) - { - return false; - } - if (targetPrefab != null) - { - return true; - } - - string targetPrefabTextId; - - if (percentage > 0f) - { - spawnAmount = (int)Math.Floor(Character.CharacterList.FindAll(c => c.TeamID == traitor.Character.TeamID && c != traitor.Character && !c.IsDead && (filter == null || filter(c))).Count * percentage); - } - - if (spawnAmount > 1 && allowNew) - { - containedPrefab = FindItemPrefab(identifier); - targetPrefab = FindItemPrefab(itemContainerId); - - if (containedPrefab == null || targetPrefab == null) - { - return false; - } - - targetPrefabTextId = containedPrefab.GetItemNameTextId(); - } - else - { - spawnAmount = 1; - containedPrefab = null; - targetPrefab = FindItemPrefab(identifier); - - if (targetPrefab == null) - { - return false; - } - - targetPrefabTextId = targetPrefab.GetItemNameTextId(); - } - - targetNameText = targetPrefabTextId != null ? TextManager.FormatServerMessage(targetPrefabTextId) : targetPrefab.Name.Value; - targetContainer = FindTargetContainer(Traitors, targetPrefab); - if (targetContainer == null) - { - targetPrefab = null; - targetContainer = null; - return false; - } - var containerPrefabTextId = targetContainer.Prefab.GetItemNameTextId(); - targetContainerNameText = containerPrefabTextId != null ? TextManager.FormatServerMessage(containerPrefabTextId) : targetContainer.Prefab.Name.Value; - var targetHullTextId = targetContainer.CurrentHull?.Prefab.GetHullNameTextId(); - targetHullNameText = targetHullTextId != null ? TextManager.FormatServerMessage(targetHullTextId) : targetContainer?.CurrentHull?.DisplayName.Value ?? ""; - if (allowNew && !targetContainer.OwnInventory.IsFull()) - { - existingItems.Clear(); - foreach (var item in targetContainer.OwnInventory.AllItems.Distinct()) - { - existingItems.Add(item); - } - Entity.Spawner.AddItemToSpawnQueue(targetPrefab, targetContainer.OwnInventory, onSpawned: item => - { - item.AddTag("traitormissionitem"); - }); - target = null; - } - else if (allowExisting) - { - target = targetContainer.OwnInventory.FindItemByIdentifier(targetPrefab.Identifier); - } - else - { - targetPrefab = null; - targetContainer = null; - return false; - } - return true; - } - - public override void Update(float deltaTime) - { - base.Update(deltaTime); - if (target == null) - { - target = targetContainer.ContainedItems.FirstOrDefault(item => item.Prefab.Identifier == (containedPrefab != null ? itemContainerId : identifier) && !existingItems.Contains(item)); - if (target != null) - { - if (containedPrefab != null) - { - for (int i = 0; i < spawnAmount; i++) - { - Entity.Spawner.AddItemToSpawnQueue(containedPrefab, target.OwnInventory); - } - } - existingItems.Clear(); - } - } - } - - public GoalFindItem(TraitorMission.CharacterFilter filter, string identifier, bool preferNew, bool allowNew, bool allowExisting, float percentage, params Identifier[] allowedContainerIdentifiers) - { - this.filter = filter; - this.identifier = identifier; - this.preferNew = preferNew; - this.allowNew = allowNew; - this.allowExisting = allowExisting; - this.percentage = percentage / 100f; - this.allowedContainerIdentifiers.UnionWith(allowedContainerIdentifiers); - } - } - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFloodPercentOfSub.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFloodPercentOfSub.cs deleted file mode 100644 index 31aad8c96..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFloodPercentOfSub.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Barotrauma.Networking; -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma -{ - partial class Traitor - { - public sealed class GoalFloodPercentOfSub : Goal - { - private readonly float minimumFloodingAmount; - - public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[percentage]" }); - public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { string.Format("{0:0}", minimumFloodingAmount * 100.0f) }); - - private bool isCompleted = false; - public override bool IsCompleted => isCompleted; - - public override void Update(float deltaTime) - { - base.Update(deltaTime); - var validHullsCount = 0; - var floodingAmount = 0.0f; - foreach (Hull hull in Hull.HullList) - { - if (hull.Submarine == null || hull.Submarine.Info.IsOutpost || Traitors.All(traitor => hull.Submarine.TeamID != traitor.Character.TeamID)) { continue; } - if (hull.Submarine == GameMain.Server?.RespawnManager?.RespawnShuttle) { continue; } - ++validHullsCount; - floodingAmount += hull.WaterVolume / hull.Volume; - } - if (validHullsCount > 0) - { - floodingAmount /= validHullsCount; - } - isCompleted = floodingAmount >= minimumFloodingAmount; - } - - public GoalFloodPercentOfSub(float minimumFloodingAmount) : base() - { - InfoTextId = "TraitorGoalFloodPercentOfSub"; - this.minimumFloodingAmount = minimumFloodingAmount; - } - } - - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalInjectTarget.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalInjectTarget.cs deleted file mode 100644 index 10cc8f56a..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalInjectTarget.cs +++ /dev/null @@ -1,84 +0,0 @@ -using Barotrauma.Networking; -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma -{ - partial class Traitor - { - public sealed class GoalInjectTarget : Goal - { - public TraitorMission.CharacterFilter Filter { get; private set; } - public List Targets { get; private set; } - - public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[targetname]", "[poison]" }); - public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { traitor.Mission.GetTargetNames(Targets) ?? "(unknown)", poisonName }); - - private bool isCompleted = false; - public override bool IsCompleted => isCompleted; - - public override bool IsEnemy(Character character) => base.IsEnemy(character) || (!isCompleted && Targets.Contains(character)); - - private string poisonId; - private string afflictionId; - private string poisonName; - private int targetCount; - private float targetPercentage; - private bool[] targetWasInfected; - - public override void Update(float deltaTime) - { - base.Update(deltaTime); - isCompleted = WereAllTargetsInfected(); - } - - private bool WereAllTargetsInfected() - { - if (targetWasInfected == null) { return false; } - - for (int i = 0; i < targetWasInfected.Length; i++) - { - if (targetWasInfected[i]) continue; - targetWasInfected[i] = Targets[i].CharacterHealth.GetAffliction(afflictionId) != null; - } - - return targetWasInfected.All(t => t == true); - } - - public override bool Start(Traitor traitor) - { - if (!base.Start(traitor)) - { - return false; - } - poisonName = TextManager.FormatServerMessage(poisonId) ?? poisonId; - - Targets = traitor.Mission.FindKillTarget(traitor.Character, Filter, targetCount, targetPercentage); - if (Targets == null) - { - return false; - } - targetWasInfected = new bool[Targets.Count]; - return !Targets.All(t => t.IsDead); - } - - public GoalInjectTarget(TraitorMission.CharacterFilter filter, string poisonId, string afflictionId, int targetCount, float targetPercentage) : base() - { - Filter = filter; - this.poisonId = poisonId; - this.afflictionId = afflictionId; - this.targetCount = targetCount; - this.targetPercentage = targetPercentage / 100f; - - if (this.targetPercentage < 1.0f) - { - InfoTextId = "traitorgoalpoisoninfo"; - } - else - { - InfoTextId = "traitorgoalpoisoneveryoneinfo"; - } - } - } - } -} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKeepTransformedAlive.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKeepTransformedAlive.cs deleted file mode 100644 index 32b024770..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKeepTransformedAlive.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma -{ - partial class Traitor - { - public sealed class GoalKeepTransformedAlive : Goal - { - public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[speciesname]" }); - public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { targetCharacterName }); - - public override bool IsCompleted => isCompleted; - private bool isCompleted; - - private const float gracePeriod = 1f; - private Identifier speciesId; - private string targetCharacterName; - private Character targetCharacter; - private float timer; - - public override bool CanBeCompleted(ICollection traitors) - { - return timer < gracePeriod || targetCharacter != null && !targetCharacter.IsDead; - } - - public override void Update(float deltaTime) - { - base.Update(deltaTime); - - if (timer <= gracePeriod) - { - timer += deltaTime; - } - - isCompleted = targetCharacter != null && !targetCharacter.IsDead && timer >= gracePeriod; - } - - public override bool Start(Traitor traitor) - { - if (!base.Start(traitor)) - { - return false; - } - - var startTime = Timing.TotalTime; - - foreach (Character character in Character.CharacterList) - { - if (character.Submarine == null || Traitors.All(t => character.Submarine.TeamID != t.Character.TeamID) || character.SpawnTime + gracePeriod < startTime) - { - continue; - } - if (character.SpeciesName == speciesId) - { - targetCharacter = character; - break; - } - } - - targetCharacterName = TextManager.FormatServerMessage($"character.{speciesId}").ToLowerInvariant(); - - return targetCharacter != null; - } - - public GoalKeepTransformedAlive(Identifier speciesId) : base() - { - this.speciesId = speciesId; - } - } - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKillTarget.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKillTarget.cs deleted file mode 100644 index 6cb4c70e6..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKillTarget.cs +++ /dev/null @@ -1,163 +0,0 @@ -using Barotrauma.Networking; -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma -{ - partial class Traitor - { - public sealed class GoalKillTarget : Goal - { - public TraitorMission.CharacterFilter Filter { get; private set; } - public List Targets { get; private set; } - - public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[targetname]", "[causeofdeath]", "[targethullname]" }); - public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] - { traitor.Mission.GetTargetNames(Targets) ?? "(unknown)", GetCauseOfDeath().Value, targetHull != null ? TextManager.Get($"roomname.{targetHull}").Value : string.Empty }); - - private bool isCompleted = false; - public override bool IsCompleted => isCompleted; - - public override bool IsEnemy(Character character) => base.IsEnemy(character) || (!isCompleted && Targets.Contains(character)); - - private CauseOfDeathType requiredCauseOfDeath; - private string afflictionId; - private string targetHull; - private int targetCount; - private float targetPercentage; - - public override void Update(float deltaTime) - { - base.Update(deltaTime); - isCompleted = DoesDeathMatchCriteria(); - } - - private bool DoesDeathMatchCriteria() - { - if (Targets == null || Targets.Any(t => !t.IsDead)) return false; - - bool typeMatch = false; - - for (int i = 0; i < Targets.Count; i++) - { - // No specified cause of death required or missing cause of death - if (requiredCauseOfDeath == CauseOfDeathType.Unknown || Targets[i].CauseOfDeath == null) - { - typeMatch = true; - } - else - { - switch (Targets[i].CauseOfDeath.Type) - { - // If a cause of death is labeled as unknown, side with the traitor and accept this regardless of the required type - case CauseOfDeathType.Unknown: - typeMatch = true; - break; - case CauseOfDeathType.Pressure: - case CauseOfDeathType.Suffocation: - case CauseOfDeathType.Drowning: - typeMatch = requiredCauseOfDeath == Targets[i].CauseOfDeath.Type; - break; - case CauseOfDeathType.Affliction: - typeMatch = Targets[i].CauseOfDeath.Type == requiredCauseOfDeath && Targets[i].CauseOfDeath.Affliction.Identifier == afflictionId; - break; - case CauseOfDeathType.Disconnected: - typeMatch = false; - break; - } - } - - if (targetHull != null) - { - if (Targets[i].CurrentHull != null) - { - if (typeMatch && Targets[i].CurrentHull.RoomName == targetHull || Targets[i].CurrentHull.RoomName.Contains(targetHull)) - { - continue; - } - else - { - return false; - } - } - else - { - // Outside the submarine, not supported for now - return false; - } - } - else - { - if (typeMatch) - { - continue; - } - else - { - return false; - } - } - } - - return true; - } - - private LocalizedString GetCauseOfDeath() - { - if (requiredCauseOfDeath != CauseOfDeathType.Affliction || afflictionId == string.Empty) - { - return requiredCauseOfDeath.ToString().ToLower(); - } - else - { - return TextManager.Get($"afflictionname.{afflictionId}").ToLower(); - } - } - - public override bool Start(Traitor traitor) - { - if (!base.Start(traitor)) - { - return false; - } - - Targets = traitor.Mission.FindKillTarget(traitor.Character, Filter, targetCount, targetPercentage); - return Targets != null && !Targets.All(t => t.IsDead); - } - - public GoalKillTarget(TraitorMission.CharacterFilter filter, CauseOfDeathType requiredCauseOfDeath, string afflictionId, string targetHull, int targetCount, float targetPercentage) : base() - { - Filter = filter; - this.requiredCauseOfDeath = requiredCauseOfDeath; - this.afflictionId = afflictionId; - this.targetHull = targetHull; - this.targetCount = targetCount; - this.targetPercentage = targetPercentage / 100f; - - if (this.targetPercentage < 1f) - { - if (this.requiredCauseOfDeath == CauseOfDeathType.Unknown && targetHull == null) - { - InfoTextId = "traitorgoalkilltargetinfo"; - } - else if (this.requiredCauseOfDeath != CauseOfDeathType.Unknown && targetHull == null) - { - InfoTextId = "traitorgoalkilltargetinfowithcause"; - } - else if (this.requiredCauseOfDeath == CauseOfDeathType.Unknown && targetHull != null) - { - InfoTextId = "traitorgoalkilltargetinfowithhull"; - } - else if (this.requiredCauseOfDeath != CauseOfDeathType.Unknown && targetHull != null) - { - InfoTextId = "traitorgoalkilltargetinfowithcauseandhull"; - } - } - else - { - InfoTextId = "traitorgoalkilleveryoneinfo"; - } - } - } - } -} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalReachDistanceFromSub.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalReachDistanceFromSub.cs deleted file mode 100644 index 8db03ab3c..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalReachDistanceFromSub.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using FarseerPhysics; -using Microsoft.Xna.Framework; - -namespace Barotrauma -{ - partial class Traitor - { - public sealed class GoalReachDistanceFromSub : Goal - { - private readonly float requiredDistance; - private readonly float requiredDistanceSqr; - private float requiredDistanceInMeters; - - public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[distance]" }); - public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { $"{requiredDistanceInMeters:0.00}" }); - - public override bool IsCompleted - { - get - { - return Traitors.Any(traitor => - { - Submarine ownSub = null; - - for (int i = 0; i < Submarine.MainSubs.Length; i++) - { - if (Submarine.MainSubs[i] != null && Submarine.MainSubs[i].TeamID == traitor.Character.TeamID) - { - ownSub = Submarine.MainSubs[i]; - break; - } - } - - if (ownSub == null) return false; - - var characterPosition = traitor.Character.WorldPosition; - var submarinePosition = ownSub.WorldPosition; - var distance = Vector2.DistanceSquared(characterPosition, submarinePosition); - return distance >= requiredDistanceSqr; - }); - } - } - - public GoalReachDistanceFromSub(float requiredDistance) : base() - { - InfoTextId = "TraitorGoalReachDistanceFromSub"; - requiredDistanceInMeters = requiredDistance; - this.requiredDistance = requiredDistance / Physics.DisplayToRealWorldRatio; - requiredDistanceSqr = this.requiredDistance * this.requiredDistance; - } - } - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalReplaceInventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalReplaceInventory.cs deleted file mode 100644 index fe4edd2d4..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalReplaceInventory.cs +++ /dev/null @@ -1,70 +0,0 @@ -using Barotrauma.Networking; -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma -{ - partial class Traitor - { - public class GoalReplaceInventory : HumanoidGoal - { - private readonly HashSet sabotageContainerIds = new HashSet(); - private readonly HashSet validReplacementIds = new HashSet(); - - private readonly float replaceAmount; - - private bool isCompleted = false; - public override bool IsCompleted => isCompleted; - - public override IEnumerable StatusTextKeys => base.StatusTextKeys.Concat(new string[] { "[percentage]" }); - public override IEnumerable StatusTextValues(Traitor traitor) => base.StatusTextValues(traitor).Concat(new string[] { string.Format("{0:0}", replaceAmount * 100.0f) }); - - public override void Update(float deltaTime) - { - base.Update(deltaTime); - int totalAmount = 0, replacedAmount = 0; - foreach (var item in Item.ItemList) - { - if (item.Submarine == null || Traitors.All(traitor => item.Submarine.TeamID != traitor.Character.TeamID)) - { - continue; - } - if (item.FindParentInventory(inventory => inventory.Owner is Character) != null) - { - continue; - } - if (sabotageContainerIds.Contains(((MapEntity)item).Prefab.Identifier)) - { - ++totalAmount; - if (item.OwnInventory.AllItems.All(containedItem => !validReplacementIds.Contains(containedItem.Prefab.Identifier))) - { - continue; - } - ++replacedAmount; - } - } - isCompleted = replacedAmount >= (int)(replaceAmount * totalAmount + 0.5f); - } - - public override bool Start(Traitor traitor) - { - if (!base.Start(traitor)) - { - return false; - } - if (sabotageContainerIds.Count <= 0 || validReplacementIds.Count <= 0) - { - return false; - } - return true; - } - - public GoalReplaceInventory(Identifier[] containerIds, Identifier[] replacementIds, float replaceAmount) - { - sabotageContainerIds.UnionWith(containerIds); - validReplacementIds.UnionWith(replacementIds); - this.replaceAmount = replaceAmount; - } - } - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalSabotageItems.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalSabotageItems.cs deleted file mode 100644 index 0175429b9..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalSabotageItems.cs +++ /dev/null @@ -1,62 +0,0 @@ -using Barotrauma.Networking; -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma -{ - partial class Traitor - { - public sealed class GoalSabotageItems : HumanoidGoal - { - private readonly string tag; - private readonly float conditionThreshold; - - public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[tag]", "[target]", "[threshold]" }); - public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { tag ?? "", targetItemPrefabName ?? "", string.Format("{0:0}", conditionThreshold) }); - - private bool isCompleted = false; - public override bool IsCompleted => isCompleted; - - private readonly List targetItems = new List(); - private string targetItemPrefabName = null; - - public override bool Start(Traitor traitor) - { - if (!base.Start(traitor)) - { - return false; - } - foreach (var item in Item.ItemList) - { - if (item.Submarine == null || Traitors.All(t => item.Submarine.TeamID != t.Character.TeamID)) - { - continue; - } - if (item.Condition > conditionThreshold && (item.Prefab?.Identifier == tag || item.HasTag(tag))) - { - targetItems.Add(item); - } - } - if (targetItems.Count > 0) - { - var textId = targetItems[0].Prefab.GetItemNameTextId(); - targetItemPrefabName = TextManager.FormatServerMessage(textId) ?? targetItems[0].Prefab.Name.Value; - } - return targetItems.Count > 0; - } - - public override void Update(float deltaTime) - { - base.Update(deltaTime); - isCompleted = targetItems.All(item => item.Condition <= conditionThreshold); - } - - public GoalSabotageItems(string tag, float conditionThreshold) : base() - { - this.tag = tag; - this.conditionThreshold = conditionThreshold; - InfoTextId = "TraitorGoalSabotageInfo"; - } - } - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalUnwiring.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalUnwiring.cs deleted file mode 100644 index 2f1314d49..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalUnwiring.cs +++ /dev/null @@ -1,96 +0,0 @@ -using Barotrauma.Items.Components; -using Barotrauma.Networking; -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma -{ - partial class Traitor - { - public sealed class GoalUnwiring : HumanoidGoal - { - private readonly string tag; - - public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[targetname]", "[connectionname]" }); - public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { targetItemPrefabName ?? "", targetConnectionDisplayName ?? targetConnectionName }); - - private bool isCompleted = false; - public override bool IsCompleted => isCompleted; - - private readonly List targetConnectionPanels = new List(); - private string targetItemPrefabName; - private string targetConnectionName; - private string targetConnectionDisplayName; - - public override bool Start(Traitor traitor) - { - if (!base.Start(traitor)) - { - return false; - } - foreach (var item in Item.ItemList) - { - if (item.Submarine == null || Traitors.All(t => item.Submarine.TeamID != t.Character.TeamID)) - { - continue; - } - if (item.Prefab?.Identifier == tag || item.HasTag(tag)) - { - var connectionPanel = item.GetComponent(); - if (connectionPanel != null) - { - targetConnectionPanels.Add(connectionPanel); - } - } - } - if (targetConnectionPanels.Count > 0) - { - var textId = targetConnectionPanels[0].Item.Prefab.GetItemNameTextId(); - targetItemPrefabName = TextManager.FormatServerMessage(textId) ?? targetConnectionPanels[0].Item.Prefab.Name.Value; - } - - return targetConnectionPanels.Count > 0; - } - - public override void Update(float deltaTime) - { - base.Update(deltaTime); - isCompleted = AreTargetsUnwired(); - } - - private bool AreTargetsUnwired() - { - for (int i = 0; i < targetConnectionPanels.Count; i++) - { - for (int j = 0; j < targetConnectionPanels[i].Connections.Count; j++) - { - if (targetConnectionPanels[i].Connections[j] == null || targetConnectionPanels[i].Connections[j].Wires == null) continue; - if (targetConnectionName != string.Empty) - { - if (targetConnectionPanels[i].Connections[j].Name != targetConnectionName) continue; - } - if (!targetConnectionPanels[i].Connections[j].Wires.All(w => w == null)) return false; - } - } - - return true; - } - - public GoalUnwiring(string tag, string targetConnectionName, string targetConnectionDisplayTag) : base() - { - this.tag = tag; - this.targetConnectionName = targetConnectionName; - - if (targetConnectionDisplayTag != string.Empty) - { - targetConnectionDisplayName = TextManager.FormatServerMessage(targetConnectionDisplayTag); - InfoTextId = "TraitorGoalUnwireInfo"; - } - else - { - InfoTextId = "TraitorGoalUnwireAllInfo"; - } - } - } - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalWaitForTraitors.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalWaitForTraitors.cs deleted file mode 100644 index 02e344392..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalWaitForTraitors.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma -{ - partial class Traitor - { - public sealed class GoalWaitForTraitors : Goal - { - private readonly int requiredCount; - private int count = 0; - - public override bool IsCompleted => count >= requiredCount; - - public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[remaining]", "[count]" }); - public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { $"{requiredCount - count}", $"{requiredCount}" }); - - public override bool Start(Traitor traitor) - { - if (!base.Start(traitor)) - { - return false; - } - ++count; - return true; - } - - public GoalWaitForTraitors(int requiredCount) : base() - { - this.requiredCount = requiredCount; - InfoTextId = "TraitorGoalWaitForTraitorsInfoText"; - } - } - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/HumanoidGoal.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/HumanoidGoal.cs deleted file mode 100644 index 06261de00..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/HumanoidGoal.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Barotrauma -{ - partial class Traitor - { - public abstract class HumanoidGoal : Goal - { - public override bool Start(Traitor traitor) - { - if (!base.Start(traitor)) - { - return false; - } - return traitor?.Character?.IsHumanoid ?? false; - } - } - } -} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalHasDuration.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalHasDuration.cs deleted file mode 100644 index f4b5d894d..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalHasDuration.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; - -namespace Barotrauma -{ - partial class Traitor - { - public sealed class GoalHasDuration : Modifier - { - private readonly float requiredDuration; - private readonly bool countTotalDuration; - private readonly string durationInfoTextId; - - public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[duration]" }); - - public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { requiredDuration.ToString(CultureInfo.InvariantCulture) }); - - protected internal override string GetInfoText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) - { - var infoText = base.GetInfoText(traitor, textId, keys, values); - return !string.IsNullOrEmpty(durationInfoTextId) && !infoText.Contains("[duration]") ? TextManager.FormatServerMessage(durationInfoTextId, - ("[infotext]", infoText), ("[duration]", requiredDuration.ToString(CultureInfo.InvariantCulture))) : infoText; - } - - private bool isCompleted = false; - public override bool IsCompleted => isCompleted; - - private float remainingDuration = float.NaN; - - public override void Update(float deltaTime) - { - base.Update(deltaTime); - if (Goal.IsCompleted) - { - if (!float.IsNaN(remainingDuration)) - { - remainingDuration -= deltaTime; - } - else - { - remainingDuration = requiredDuration; - } - isCompleted |= remainingDuration <= 0.0f; - } - else if (!countTotalDuration) - { - remainingDuration = float.NaN; - } - } - - public GoalHasDuration(Goal goal, float requiredDuration, bool countTotalDuration, string durationInfoTextId) : base(goal) - { - this.requiredDuration = requiredDuration; - this.countTotalDuration = countTotalDuration; - this.durationInfoTextId = durationInfoTextId; - } - } - } -} - diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalHasTimeLimit.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalHasTimeLimit.cs deleted file mode 100644 index f2603e594..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalHasTimeLimit.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma -{ - partial class Traitor - { - public sealed class GoalHasTimeLimit : Modifier - { - private readonly float timeLimit; - private readonly string timeLimitInfoTextId; - - public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[timelimit]" }); - public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { $"{TimeSpan.FromSeconds(timeLimit):g}" }); - - protected internal override string GetInfoText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) - { - var infoText = base.GetInfoText(traitor, textId, keys, values); - return !string.IsNullOrEmpty(timeLimitInfoTextId) ? TextManager.FormatServerMessage(timeLimitInfoTextId, ("[infotext]", infoText), ("[timelimit]", $"{TimeSpan.FromSeconds(timeLimit):g}")) : infoText; - } - - public override bool CanBeCompleted(ICollection traitors) => base.CanBeCompleted(traitors) && (!Traitors.Any(IsStarted) || timeRemaining > 0.0f); - - private float timeRemaining; - - public override void Update(float deltaTime) - { - base.Update(deltaTime); - timeRemaining = System.Math.Max(0.0f, timeRemaining - deltaTime); - } - - public override bool Start(Traitor traitor) - { - if (!base.Start(traitor)) - { - return false; - } - timeRemaining = timeLimit; - return true; - } - - public GoalHasTimeLimit(Goal goal, float timeLimit, string timeLimitInfoTextId) : base(goal) - { - this.timeLimit = timeLimit; - this.timeLimitInfoTextId = timeLimitInfoTextId; - } - } - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalIsOptional.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalIsOptional.cs deleted file mode 100644 index 22b5a0a74..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalIsOptional.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma -{ - partial class Traitor - { - public sealed class GoalIsOptional : Modifier - { - private readonly string optionalInfoTextId; - - public override string StatusValueTextId => (Traitors.Any(IsStarted) && !base.CanBeCompleted(Traitors)) ? "failed" : base.StatusValueTextId; - - public override IEnumerable StatusTextValues(Traitor traitor) - { - var values = base.StatusTextValues(traitor).ToArray(); - values[1] = TextManager.FormatServerMessage(StatusValueTextId); - return values; - } - - public override bool IsCompleted => base.IsCompleted || (Traitors.Any(IsStarted) && !base.CanBeCompleted(Traitors)); - public override bool CanBeCompleted(ICollection traitors) => true; - - protected internal override string GetInfoText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) - { - var infoText = base.GetInfoText(traitor, textId, keys, values); - return !string.IsNullOrEmpty(optionalInfoTextId) ? TextManager.FormatServerMessage(optionalInfoTextId, ("[infotext]", infoText)) : infoText; - } - - public GoalIsOptional(Goal goal, string optionalInfoTextId) : base(goal) - { - this.optionalInfoTextId = optionalInfoTextId; - } - } - } -} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/Modifier.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/Modifier.cs deleted file mode 100644 index 2a48fe8ae..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/Modifier.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma -{ - partial class Traitor - { - public abstract class Modifier : Goal - { - protected Goal Goal { get; } - - public override string StatusValueTextId => Goal.StatusValueTextId; - - public override string StatusTextId - { - get => Goal.StatusTextId; - set => Goal.StatusTextId = value; - } - - public override string InfoTextId - { - get => Goal.InfoTextId; - set => Goal.InfoTextId = value; - } - - public override string CompletedTextId - { - get => Goal.CompletedTextId; - set => Goal.CompletedTextId = value; - } - - public override IEnumerable StatusTextKeys => Goal.StatusTextKeys; - public override IEnumerable StatusTextValues(Traitor traitor) => new string[] { InfoText(traitor), TextManager.FormatServerMessage(StatusValueTextId) }; - - public override IEnumerable InfoTextKeys => Goal.InfoTextKeys; - public override IEnumerable InfoTextValues(Traitor traitor) => Goal.InfoTextValues(traitor); - - public override IEnumerable CompletedTextKeys => Goal.CompletedTextKeys; - public override IEnumerable CompletedTextValues(Traitor traitor) => Goal.CompletedTextValues(traitor); - - protected internal override string GetStatusText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) => Goal.GetStatusText(traitor, textId, keys, values); - protected internal override string GetInfoText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) => Goal.GetInfoText(traitor, textId, keys, values); - protected internal override string GetCompletedText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) => Goal.GetCompletedText(traitor, textId, keys, values); - - public override string StatusText(Traitor traitor) => GetStatusText(traitor, StatusTextId, StatusTextKeys, StatusTextValues(traitor)); - public override string InfoText(Traitor traitor) => GetInfoText(traitor, InfoTextId, InfoTextKeys, InfoTextValues(traitor)); - public override string CompletedText(Traitor traitor) => CompletedTextId != null ? GetCompletedText(traitor, CompletedTextId, CompletedTextKeys, CompletedTextValues(traitor)) : StatusText(traitor); - - public override bool IsCompleted => Goal.IsCompleted; - public override bool IsStarted(Traitor traitor) => base.IsStarted(traitor) && Goal.IsStarted(traitor); - public override bool CanBeCompleted(ICollection traitors) => base.CanBeCompleted(traitors) && Goal.CanBeCompleted(traitors); - - public override bool IsEnemy(Character character) => base.IsEnemy(character) || Goal.IsEnemy(character); - - public override void Update(float deltaTime) - { - base.Update(deltaTime); - Goal.Update(deltaTime); - } - - public override bool Start(Traitor traitor) - { - if (!base.Start(traitor)) - { - return false; - } - if (!Goal.Start(traitor)) - { - return false; - } - return true; - } - - protected Modifier(Goal goal) : base() - { - Goal = goal; - } - } - } -} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Objective.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Objective.cs deleted file mode 100644 index 8c09c73ef..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Objective.cs +++ /dev/null @@ -1,194 +0,0 @@ -using Barotrauma.Networking; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma -{ - partial class Traitor - { - public class Objective - { - public Traitor Traitor { get; private set; } - - private int shuffleGoalsCount; - - private readonly List allGoals = new List(); - private readonly List activeGoals = new List(); - private readonly List pendingGoals = new List(); - private readonly List completedGoals = new List(); - - public bool IsCompleted => pendingGoals.Count <= 0; - public bool IsPartiallyCompleted => completedGoals.Count > 0; - public bool IsStarted { get; private set; } = false; - public bool CanBeStarted(ICollection traitors) => !IsStarted && allGoals.Any(goal => goal.CanBeCompleted(traitors)); - public bool CanBeCompleted => !IsStarted || pendingGoals.All(goal => goal.CanBeCompleted(goal.Traitors)); - - public bool IsEnemy(Character character) => pendingGoals.Any(goal => goal.IsEnemy(character)); - public bool IsAllowedToDamage(Structure structure) => pendingGoals.Any(goal => goal.IsAllowedToDamage(structure)); - - public readonly HashSet Roles = new HashSet(); - - public string InfoText { get; private set; } - - public virtual string GoalInfoFormatId { get; set; } = "TraitorObjectiveGoalInfoFormat"; - - public string GoalInfos => - string.Join("/", - string.Join("/", activeGoals.Select((goal, index) => - { - var statusText = goal.StatusText(Traitor); - var startIndex = statusText.LastIndexOf('/') + 1; - return $"{statusText.Substring(0, startIndex)}[{index}.st]={statusText.Substring(startIndex)}/[{index}.sl]={TextManager.FormatServerMessage(GoalInfoFormatId, ("[statustext]", $"[{index}.st]"))}"; - }).ToArray()), - string.Join("", activeGoals.Select((goal, index) => $"[{index}.sl]").ToArray())); - - public string AllGoalInfos => - string.Join("/", - string.Join("/", allGoals.Select((goal, index) => - { - var statusText = goal.StatusText(Traitor); - var startIndex = statusText.LastIndexOf('/') + 1; - return $"{statusText.Substring(0, startIndex)}[{index}.st]={statusText.Substring(startIndex)}/[{index}.sl]={TextManager.FormatServerMessage(GoalInfoFormatId, ("[statustext]", $"[{index}.st]"))}"; - }).ToArray()), - string.Join("", allGoals.Select((goal, index) => $"[{index}.sl]").ToArray())); - - public virtual string StartMessageTextId { get; set; } = "TraitorObjectiveStartMessage"; - public virtual IEnumerable StartMessageKeys => new string[] { "[traitorgoalinfos]" }; - public virtual IEnumerable StartMessageValues => new string[] { GoalInfos }; - - public virtual LocalizedString StartMessageText - => TextManager.FormatServerMessageWithPronouns(Traitor.Character.Info, StartMessageTextId, StartMessageKeys.Zip(StartMessageValues, (k,v) => (k,v)).ToArray()); - - public virtual string StartMessageServerTextId { get; set; } = "TraitorObjectiveStartMessageServer"; - public virtual IEnumerable StartMessageServerKeys => StartMessageKeys.Concat(new string[] { "[traitorname]" }); - public virtual IEnumerable StartMessageServerValues => StartMessageValues.Concat(new string[] { Traitor?.Character?.Name ?? "(unknown)" }); - - public virtual LocalizedString StartMessageServerText => TextManager.FormatServerMessageWithPronouns(Traitor.Character.Info, StartMessageServerTextId, StartMessageServerKeys.Zip(StartMessageServerValues, (k,v) => (k,v)).ToArray()); - - public virtual string EndMessageSuccessTextId { get; set; } = "TraitorObjectiveEndMessageSuccess"; - public virtual string EndMessageSuccessDeadTextId { get; set; } = "TraitorObjectiveEndMessageSuccessDead"; - public virtual string EndMessageSuccessDetainedTextId { get; set; } = "TraitorObjectiveEndMessageSuccessDetained"; - public virtual string EndMessageFailureTextId { get; set; } = "TraitorObjectiveEndMessageFailure"; - public virtual string EndMessageFailureDeadTextId { get; set; } = "TraitorObjectiveEndMessageFailureDead"; - public virtual string EndMessageFailureDetainedTextId { get; set; } = "TraitorObjectiveEndMessageFailureDetained"; - - public virtual IEnumerable EndMessageKeys => new string[] { "[traitorname]", "[traitorgoalinfos]" }; - public virtual IEnumerable EndMessageValues => new string[] { Traitor?.Character?.Name ?? "(unknown)", GoalInfos }; - public virtual string EndMessageText - { - get - { - var traitorIsDead = Traitor.Character.IsDead; - var traitorIsDetained = Traitor.Character.LockHands; - var messageId = IsCompleted - ? (traitorIsDead ? EndMessageSuccessDeadTextId : traitorIsDetained ? EndMessageSuccessDetainedTextId : EndMessageSuccessTextId) - : (traitorIsDead ? EndMessageFailureDeadTextId : traitorIsDetained ? EndMessageFailureDetainedTextId : EndMessageFailureTextId); - return TextManager.FormatServerMessageWithPronouns(Traitor.Character.Info, messageId, EndMessageKeys.Zip(EndMessageValues, (k,v)=>(k,v)).ToArray()); - } - } - - public bool Start(Traitor traitor) - { - Traitor = traitor; - - activeGoals.Clear(); - pendingGoals.Clear(); - completedGoals.Clear(); - - var allGoalsCount = allGoals.Count; - var indices = allGoals.Select((goal, index) => index).ToArray(); - if (shuffleGoalsCount > 0) - { - for (var i = allGoalsCount; i > 1;) - { - int j = TraitorManager.RandomInt(i--); - var temp = indices[j]; - indices[j] = indices[i]; - indices[i] = temp; - } - } - - for (var i = 0; i < allGoalsCount; ++i) - { - var goal = allGoals[indices[i]]; - if (goal.Start(traitor)) - { - activeGoals.Add(goal); - pendingGoals.Add(goal); - if (shuffleGoalsCount > 0 && pendingGoals.Count >= shuffleGoalsCount) - { - break; - } - } - else - { - completedGoals.Add(goal); - } - } - - if (pendingGoals.Count <= 0 && completedGoals.Count < allGoals.Count) - { - return false; - } - - IsStarted = true; - - traitor.SendChatMessageBox(StartMessageText.Value, traitor.Mission.Identifier); - traitor.UpdateCurrentObjective(GoalInfos, traitor.Mission.Identifier); - - return true; - } - - public void StartMessage() - { - Traitor.SendChatMessage(StartMessageText.Value, Traitor.Mission.Identifier); - } - - public void EndMessage() - { - Traitor.SendChatMessageBox(EndMessageText, Traitor.Mission.Identifier); - Traitor.SendChatMessage(EndMessageText, Traitor.Mission.Identifier); - } - - public void Update(float deltaTime) - { - if (!IsStarted) - { - return; - } - for (int i = 0; i < pendingGoals.Count;) - { - var goal = pendingGoals[i]; - goal.Update(deltaTime); - if (!goal.IsCompleted) - { - ++i; - } - else - { - completedGoals.Add(goal); - pendingGoals.RemoveAt(i); - if (GameMain.Server != null) - { - Traitor.SendChatMessage(goal.CompletedText(Traitor), Traitor.Mission.Identifier); - if (pendingGoals.Count > 0) - { - Traitor.SendChatMessageBox(goal.CompletedText(Traitor), Traitor.Mission.Identifier); - } - Traitor.UpdateCurrentObjective(GoalInfos, Traitor.Mission.Identifier); - } - } - } - } - - public Objective(string infoText, int shuffleGoalsCount, ICollection roles, ICollection goals) - { - InfoText = infoText; - this.shuffleGoalsCount = shuffleGoalsCount; - Roles.UnionWith(roles); - allGoals.AddRange(goals); - } - } - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs deleted file mode 100644 index f8ce8c1d8..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Barotrauma.Networking; - -namespace Barotrauma -{ - partial class Traitor - { - public readonly Character Character; - - public string Role { get; } - public TraitorMission Mission { get; } - public Objective CurrentObjective => Mission.GetCurrentObjective(this); - - public Traitor(TraitorMission mission, string role, Character character) - { - Mission = mission; - Role = role; - Character = character; - Character.IsTraitor = true; - GameMain.NetworkMember.CreateEntityEvent(Character, new Character.CharacterStatusEventData()); - } - - public delegate void MessageSender(string message); - - public void SendChatMessage(string serverText, Identifier iconIdentifier) - { - Client traitorClient = GameMain.Server.ConnectedClients.Find(c => c.Character == Character); - GameMain.Server.SendTraitorMessage(traitorClient, serverText, iconIdentifier, TraitorMessageType.Server); - } - - public void SendChatMessageBox(string serverText, Identifier iconIdentifier) - { - Client traitorClient = GameMain.Server.ConnectedClients.Find(c => c.Character == Character); - GameMain.Server.SendTraitorMessage(traitorClient, serverText, iconIdentifier, TraitorMessageType.ServerMessageBox); - } - - public void UpdateCurrentObjective(string objectiveText, Identifier iconIdentifier) - { - Client traitorClient = GameMain.Server.ConnectedClients.Find(c => c.Character == Character); - Character.TraitorCurrentObjective = objectiveText; - GameMain.Server.SendTraitorMessage(traitorClient, Character.TraitorCurrentObjective.Value, iconIdentifier, TraitorMessageType.Objective); - } - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs index fe1c89f73..e17a1de58 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs @@ -1,207 +1,505 @@ -// #define DISABLE_MISSIONS +#nullable enable -using System; +using Barotrauma.Extensions; using Barotrauma.Networking; -using Lidgren.Network; -using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; using System.Linq; +using System.Xml.Linq; namespace Barotrauma { - partial class TraitorManager - { - public static readonly Random Random = new Random((int)DateTime.UtcNow.Ticks); + sealed partial class TraitorManager + { + const int MaxPreviousEventHistory = 10; - // All traitor related functionality should use the following interface for generating random values - public static int RandomInt(int n) => Random.Next(n); + const int StartDelayMin = 60; + const int StartDelayMax = 200; - // All traitor related functionality should use the following interface for generating random values - public static double RandomDouble() => Random.NextDouble(); + private float startTimer; + private bool started = false; - public readonly Dictionary Missions = new Dictionary(); + private TraitorResults? results = null; - public string GetCodeWords(CharacterTeamType team) => Missions.TryGetValue(team, out var mission) ? mission.CodeWords : ""; - public string GetCodeResponse(CharacterTeamType team) => Missions.TryGetValue(team, out var mission) ? mission.CodeResponse : ""; - - public IEnumerable Traitors => Missions.Values.SelectMany(mission => mission.Traitors.Values); - - private float startCountdown = 0.0f; - private GameServer server; - - public bool ShouldEndRound + public class PreviousTraitorEvent { - get; - set; + public TraitorEventPrefab TraitorEvent { get; } + public TraitorEvent.State State { get; } + + public Client Traitor => + GameMain.Server.ConnectedClients.Find(c => traitorAccountId.IsSome() && traitorAccountId == c.AccountId) ?? + GameMain.Server.ConnectedClients.Find(c => traitorAddress == c.Connection.Endpoint.Address); + + private readonly Address traitorAddress; + private readonly Option traitorAccountId; + + public PreviousTraitorEvent(TraitorEventPrefab traitorEvent, TraitorEvent.State state, Client traitor) + { + TraitorEvent = traitorEvent; + State = state; + traitorAddress = traitor.Connection.Endpoint.Address; + traitorAccountId = traitor.AccountId; + } + + private PreviousTraitorEvent(TraitorEventPrefab traitorEvent, TraitorEvent.State state, Option accountId, Address address) + { + TraitorEvent = traitorEvent; + State = state; + traitorAddress = address; + traitorAccountId = accountId; + } + + public void Save(XElement parentElement) + { + parentElement.Add( + new XElement(nameof(PreviousTraitorEvent), + new XAttribute("id", TraitorEvent.Identifier), + new XAttribute("state", State), + new XAttribute("accountid", traitorAccountId), + new XAttribute("address", traitorAddress))); + } + + public static PreviousTraitorEvent? Load(XElement subElement) + { + var traitorEventId = subElement.GetAttributeIdentifier("id", Identifier.Empty); + var state = subElement.GetAttributeEnum("state", Barotrauma.TraitorEvent.State.Failed); + var accountId = Networking.AccountId.Parse( + subElement.GetAttributeString("accountid", null) + ?? subElement.GetAttributeString("steamid", "")); + var address = Address.Parse( + subElement.GetAttributeString("address", null) + ?? subElement.GetAttributeString("endpoint", "")) + .Fallback(new UnknownAddress()); + if (EventPrefab.Prefabs.TryGet(traitorEventId, out EventPrefab? prefab) && prefab is TraitorEventPrefab traitorEventPrefab) + { + return new PreviousTraitorEvent(traitorEventPrefab, state, accountId, address); + } + else + { + DebugConsole.ThrowError($"Error when loading {nameof(TraitorManager)}: could not find a traitor event prefab with the identifier \"{traitorEventId}\"."); + return null; + } + } } + public readonly record struct ActiveTraitorEvent( + Client Traitor, + TraitorEvent TraitorEvent); + + private readonly List previousTraitorEvents = new List(); + + private readonly List activeEvents = new List(); + + public IEnumerable ActiveEvents => activeEvents; + + private readonly GameServer server; + + private EventManager? eventManager; + private Level? level; + + public bool Enabled; + public bool IsTraitor(Character character) { - if (Traitors == null) + return activeEvents.Any(e => e.Traitor.Character == character); + } + + public TraitorManager(GameServer server) + { + this.server = server; + } + + public void Initialize(EventManager eventManager, Level level) + { + this.eventManager = eventManager; + this.level = level; + startTimer = Rand.Range(StartDelayMin, StartDelayMax); + started = false; + results = null; + } + + private bool TryCreateTraitorEvents(EventManager eventManager, Level level) + { + var eventPrefabs = EventPrefab.Prefabs.Where(p => p is TraitorEventPrefab).Cast(); + if (!eventPrefabs.Any()) { + DebugConsole.AddWarning("No traitor event available in any of the enabled content packages."); return false; } - return Traitors.Any(traitor => traitor.Character == character); + + if (server.ConnectedClients.Count(IsClientViableTraitor) < server.ServerSettings.TraitorsMinPlayerCount) + { + DebugConsole.AddWarning("Not enough clients to create a traitor event. Active traitor events: " + activeEvents.Count); +#if DEBUG + DebugConsole.AddWarning("Starting a traitor event anyway because this is a debug build."); +#else + return false; +#endif + + } + + int maxDangerLevel = server.ServerSettings.TraitorDangerLevel; + + int playerCount = server.ConnectedClients.Count(c => c.Character != null && !c.Character.Removed); + + var campaign = GameMain.GameSession?.Campaign; + var suitablePrefabs = eventPrefabs + .Where(e => EventManager.IsLevelSuitable(e, level)) + .Where(e => e.ReputationRequirementsMet(campaign)) + .Where(e => e.MissionRequirementsMet(GameMain.GameSession)) + .Where(e => e.LevelRequirementsMet(level)) + .Where(e => e.DangerLevel <= maxDangerLevel) + .Where(e => playerCount >= e.MinPlayerCount) + .ToList(); + + var minDangerLevelPrefabs = suitablePrefabs.FindAll(e => e.DangerLevel == TraitorEventPrefab.MinDangerLevel); + + if (!suitablePrefabs.Any()) + { + //this is normal, there e.g. might be no missions for an abandoned outpost or end level + DebugConsole.Log("No suitable traitor missions available for the level."); + return false; + } + + foreach (var previousEvent in previousTraitorEvents.DistinctBy(e => e.Traitor).Reverse()) + { + if (previousEvent.State == TraitorEvent.State.Completed && + previousEvent.TraitorEvent.DangerLevel < maxDangerLevel && + previousEvent.TraitorEvent.IsChainable && + IsClientViableTraitor(previousEvent.Traitor)) + { + GameServer.Log($"{NetworkMember.ClientLogName(previousEvent.Traitor)} successfully completed a traitor event on a previous round. Attempting to give choose them a new, more dangerous event...", ServerLog.MessageType.Traitors); + + var suitablePrefab = + //try finding an event that's continuation from the previous one (= requires the previous one to be completed 1st) + suitablePrefabs.GetRandomUnsynced(p => p.RequiredCompletedTags.Any(t => + previousEvent.TraitorEvent.Identifier == t || previousEvent.TraitorEvent.Tags.Contains(t))); + + suitablePrefab ??= + //otherwise try finding some higher-difficult event for the same faction + suitablePrefabs.GetRandomUnsynced(p => + p.RequiredCompletedTags.None() && + p.DangerLevel > previousEvent.TraitorEvent.DangerLevel && + p.Faction == previousEvent.TraitorEvent.Faction); + + if (suitablePrefab != null) + { + CreateTraitorEvent(eventManager, suitablePrefab, previousEvent.Traitor); + return true; + } + else + { + GameServer.Log($"Could not find a suitable, more difficult traitor event for {NetworkMember.ClientLogName(previousEvent.Traitor)}.", ServerLog.MessageType.Traitors); + } + } + } + + TraitorEventPrefab selectedPrefab; + if (GameMain.GameSession?.Campaign == null) + { + selectedPrefab = suitablePrefabs.GetRandomByWeight(GetTraitorEventPrefabCommonness, Rand.RandSync.Unsynced); + } + else + { + selectedPrefab = minDangerLevelPrefabs.GetRandomByWeight(GetTraitorEventPrefabCommonness, Rand.RandSync.Unsynced); + if (selectedPrefab == null) + { + GameServer.Log($"Could not find a suitable danger level {TraitorEventPrefab.MinDangerLevel} traitor event. Choosing a random event instead.", ServerLog.MessageType.Traitors); + selectedPrefab = suitablePrefabs.GetRandomByWeight(GetTraitorEventPrefabCommonness, Rand.RandSync.Unsynced); + } + } + + if (selectedPrefab != null) + { + var selectedTraitor = SelectRandomTraitor(); + if (selectedTraitor == null) + { + DebugConsole.ThrowError($"Could not find a suitable traitor for the event \"{selectedPrefab.Identifier}\"."); + return false; + } + CreateTraitorEvent(eventManager, selectedPrefab, selectedTraitor); + return true; + } + + return false; } - public string GetTraitorRole(Character character) + private Client? SelectRandomTraitor() { - var traitor = Traitors.FirstOrDefault(candidate => candidate.Character == character); + if (GameSettings.CurrentConfig.VerboseLogging) + { + GameServer.Log( + $"Choosing a random traitor... Available traitors:" + + string.Concat(server.ConnectedClients.Where(IsClientViableTraitor).Select(c => $"\n - {c.Name} ({(int)(GetTraitorProbability(c) * 100)}%)")), + ServerLog.MessageType.Traitors); + } + return server.ConnectedClients.Where(IsClientViableTraitor).GetRandomByWeight(GetTraitorProbability, Rand.RandSync.Unsynced); + } + + private IEnumerable SelectSecondaryTraitors(TraitorEvent traitorEvent, Client mainTraitor) + { + if (traitorEvent.Prefab.SecondaryTraitorPercentage <= 0.0f && + traitorEvent.Prefab.SecondaryTraitorAmount <= 0) + { + return Enumerable.Empty(); + } + + var viableTraitors = server.ConnectedClients.Where(c => c != mainTraitor && IsClientViableTraitor(c)).ToList(); + + int amountToChoose = (int)Math.Ceiling(viableTraitors.Count * (traitorEvent.Prefab.SecondaryTraitorPercentage / 100.0f)); + amountToChoose = Math.Max(amountToChoose, traitorEvent.Prefab.SecondaryTraitorAmount); + + if (amountToChoose > viableTraitors.Count) + { + DebugConsole.ThrowError( + $"Error in traitor event {traitorEvent.Prefab.Identifier}. Not enough players to choose {amountToChoose} secondary traitors."+ + $"Make sure the {nameof(traitorEvent.Prefab.MinPlayerCount)} of the event is high enough to support to desired amount of secondary traitors."); + amountToChoose = viableTraitors.Count; + } + + List traitors = new List(); + for (int i = 0; i < amountToChoose; i++) + { + var traitor = viableTraitors.GetRandomUnsynced(); + viableTraitors.Remove(traitor); + traitors.Add(traitor); + } + return traitors; + } + + private bool IsClientViableTraitor(Client client) + { + return + client != null && + server.ConnectedClients.Contains(client) && + client.Character != null && + !client.Character.IsIncapacitated && !client.Character.Removed && + activeEvents.None(e => e.Traitor == client); + } + + private float GetTraitorEventPrefabCommonness(TraitorEventPrefab prefab) + { + int? roundsSinceLastSelected = GetRoundsSinceLastSelected(e => e.TraitorEvent == prefab); + if (roundsSinceLastSelected.HasValue) + { + float normalizedRoundsSinceLastSelected = MathUtils.InverseLerp(0, MaxPreviousEventHistory, roundsSinceLastSelected.Value); + //exponentially decreasing commonness the closer the last time the event was selected + return prefab.Commonness * normalizedRoundsSinceLastSelected * normalizedRoundsSinceLastSelected; + } + else + { + return prefab.Commonness; + } + } + + private float GetTraitorProbability(Client client) + { + int? roundsSinceLastSelected = GetRoundsSinceLastSelected(e => e.Traitor == client); + if (roundsSinceLastSelected.HasValue) + { + float normalizedRoundsSinceLastSelected = MathUtils.InverseLerp(0, MaxPreviousEventHistory, roundsSinceLastSelected.Value); + //exponentially decreasing commonness the closer the last time the event was selected + return normalizedRoundsSinceLastSelected * normalizedRoundsSinceLastSelected; + } + else + { + return 1.0f; + } + } + + private int? GetRoundsSinceLastSelected(Func condition) + { + //most recent events are at the end of the list, start from there + for (int i = previousTraitorEvents.Count - 1; i >= 0; i--) + { + if (condition(previousTraitorEvents[i])) + { + return previousTraitorEvents.Count - i; + } + } + return null; + } + + private void CreateTraitorEvent(EventManager eventManager, TraitorEventPrefab selectedPrefab, Client traitor) + { + if (selectedPrefab.TryCreateInstance(out var newEvent)) + { + var secondaryTraitors = SelectSecondaryTraitors(newEvent, traitor); + + string logMessage = $"{NetworkMember.ClientLogName(traitor)} was selected as a traitor. Selected event: {selectedPrefab.Name}"; + if (secondaryTraitors.Any()) + { + logMessage += $", secondary traitors: {string.Join(", ", secondaryTraitors.Select(c => NetworkMember.ClientLogName(c)))}"; + } + GameServer.Log(logMessage, ServerLog.MessageType.Traitors); + + newEvent.OnStateChanged += () => SendCurrentState(newEvent); + activeEvents.Add(new ActiveTraitorEvent(traitor, newEvent)); + newEvent.SetTraitor(traitor); + newEvent.SetSecondaryTraitors(secondaryTraitors); + eventManager.ActivateEvent(newEvent); + SendCurrentState(newEvent); + } + else + { + DebugConsole.ThrowError($"Failed to create an instance of the traitor event prefab \"{selectedPrefab.Identifier}\"!"); + } + } + + public void ForceTraitorEvent(TraitorEventPrefab traitorEventPrefab) + { + if (eventManager == null) + { + throw new InvalidOperationException("EventManager was null. TraitorManager may not have been initialized properly."); + } + var traitor = SelectRandomTraitor(); if (traitor == null) { - return ""; + DebugConsole.ThrowError($"Could not find a suitable traitor for the event \"{traitorEventPrefab.Identifier}\"."); + return; } - return traitor.Role; - } - - public TraitorManager() - { - } - - public void Start(GameServer server) - { -#if DISABLE_MISSIONS - return; -#endif - if (server == null) { return; } - - ShouldEndRound = false; - - this.server = server; - startCountdown = MathHelper.Lerp(server.ServerSettings.TraitorsMinStartDelay, server.ServerSettings.TraitorsMaxStartDelay, (float)RandomDouble()); + CreateTraitorEvent(eventManager, traitorEventPrefab, traitor); } public void SkipStartDelay() { - startCountdown = 0.01f; + startTimer = 0.0f; + if (activeEvents.Any()) { started = true; } } public void Update(float deltaTime) { - if (ShouldEndRound) { return; } - -#if DISABLE_MISSIONS - return; -#endif - if (Missions.Any()) + if (!Enabled) { return; } + if (!started) { - bool missionCompleted = false; - bool gameShouldEnd = false; - CharacterTeamType winningTeam = CharacterTeamType.None; - foreach (var mission in Missions) + if (level?.LevelData is { Type: LevelData.LevelType.LocationConnection }) { - mission.Value.Update(deltaTime, () => + if (Submarine.MainSub.WorldPosition.X > level.Size.X / 2) { - switch (mission.Key) - { - case CharacterTeamType.Team1: - winningTeam = (winningTeam == CharacterTeamType.None) ? CharacterTeamType.Team2 : CharacterTeamType.None; - break; - case CharacterTeamType.Team2: - winningTeam = (winningTeam == CharacterTeamType.None) ? CharacterTeamType.Team1 : CharacterTeamType.None; - break; - default: - break; - } - gameShouldEnd = true; - }); - if (!gameShouldEnd && mission.Value.IsCompleted) - { - missionCompleted = true; - foreach (var traitor in mission.Value.Traitors.Values) - { - traitor.UpdateCurrentObjective("", mission.Value.Identifier); - } + //try starting ASAP if the submarine is already half-way through the level + //(brief delay regardless, because otherwise we might retry every frame if finding a suitable event fails below) + startTimer = Math.Min(startTimer, 10.0f); } } - if (gameShouldEnd) + startTimer -= deltaTime; + if (startTimer >= 0.0f) { return; } + if (eventManager == null) { - GameMain.GameSession.WinningTeam = winningTeam; - ShouldEndRound = true; - return; + throw new InvalidOperationException("EventManager was null. TraitorManager may not have been initialized properly."); } - if (missionCompleted) + if (level == null) { - Missions.Clear(); - startCountdown = MathHelper.Lerp(server.ServerSettings.TraitorsMinRestartDelay, server.ServerSettings.TraitorsMaxRestartDelay, (float)RandomDouble()); + throw new InvalidOperationException("Level was null. TraitorManager may not have been initialized properly."); } + if (TryCreateTraitorEvents(eventManager, level)) + { + started = true; + } + else + { + //restart timer, we might be able to start a mission later if more clients join + startTimer = Rand.Range(StartDelayMin, StartDelayMax); + } } - else if (startCountdown > 0.0f && server.GameStarted) + } + + public void EndRound() + { + Client? votedAsTraitor = GetClientAccusedAsTraitor(); + foreach (var activeEvent in activeEvents) { - startCountdown -= deltaTime; - if (startCountdown <= 0.0f) + if (results != null) { - int playerCharactersCount = server.ConnectedClients.Sum(client => client.Character != null && !client.Character.IsDead ? 1 : 0); - if (playerCharactersCount < server.ServerSettings.TraitorsMinPlayerCount) + DebugConsole.AddWarning("Multiple traitor events active during the round, only displaying the results for the last one."); + } + results = new TraitorResults(votedAsTraitor, activeEvent.TraitorEvent); + if (results.Value.MoneyPenalty > 0) + { + GameMain.GameSession?.Campaign?.Bank?.TryDeduct(results.Value.MoneyPenalty); + } + + if (activeEvent.TraitorEvent.CurrentState != TraitorEvent.State.Completed) + { + activeEvent.TraitorEvent.CurrentState = TraitorEvent.State.Failed; + } + GameServer.Log( + NetworkMember.ClientLogName(activeEvent.Traitor) + + (activeEvent.TraitorEvent.CurrentState == TraitorEvent.State.Completed ? + " completed their traitor objective successfully." : " failed to complete their traitor objective."), + ServerLog.MessageType.Traitors); + + //consider the event failed if the traitor was identifier correctly, + //so the traitor doesn't get rewards or get assigned a follow-up event + if (results.Value.VotedCorrectTraitor) + { + GameServer.Log( + NetworkMember.ClientLogName(activeEvent.Traitor) + " was correctly identified as the traitor, and will not receive any rewards.", + ServerLog.MessageType.Traitors); + activeEvent.TraitorEvent.CurrentState = TraitorEvent.State.Failed; + } + previousTraitorEvents.Add(new PreviousTraitorEvent( + activeEvent.TraitorEvent.Prefab, + activeEvent.TraitorEvent.CurrentState, + activeEvent.Traitor)); + } + if (previousTraitorEvents.Count > MaxPreviousEventHistory) + { + previousTraitorEvents.RemoveRange(0, previousTraitorEvents.Count - MaxPreviousEventHistory); + } + activeEvents.Clear(); + } + + public Client? GetClientAccusedAsTraitor() + { + Client? votedAsTraitor = Voting.HighestVoted(VoteType.Traitor, server.ConnectedClients.Where(c => c.Character is { IsDead: false }), out int voteCount); + if (voteCount < server.ConnectedClients.Count * server.ServerSettings.MinPercentageOfPlayersForTraitorAccusation / 100.0f) + { + //at least x% of the players must've voted for the same player + votedAsTraitor = null; + } + return votedAsTraitor; + } + + public TraitorResults? GetEndResults() + { + return results; + } + + public XElement Save() + { + var element = new XElement(nameof(TraitorManager), + new XAttribute("version", GameMain.Version.ToString())); + foreach (var previousEvent in previousTraitorEvents) + { + previousEvent.Save(element); + } + return element; + } + + public void Load(XElement traitorManagerElement) + { + previousTraitorEvents.Clear(); + foreach (XElement subElement in traitorManagerElement.Elements()) + { + if (subElement.Name.ToIdentifier() == nameof(PreviousTraitorEvent)) + { + var previousTraitorEvent = PreviousTraitorEvent.Load(subElement); + if (previousTraitorEvent != null) { - startCountdown = MathHelper.Lerp(server.ServerSettings.TraitorsMinRestartDelay, server.ServerSettings.TraitorsMaxRestartDelay, (float)RandomDouble()); - return; + previousTraitorEvents.Add(previousTraitorEvent); } - if (Character.CharacterList.Count(c => !c.IsDead && c.TeamID == CharacterTeamType.Team1 || c.TeamID == CharacterTeamType.Team2) <= 1) - { - return; - } - if (GameMain.GameSession.Missions.Any(m => m is CombatMission)) - { - var teamIds = new[] { CharacterTeamType.Team1, CharacterTeamType.Team2 }; - foreach (var teamId in teamIds) - { - if (server.ConnectedClients.Count(c => c.Character != null && !c.Character.IsDead && c.TeamID == teamId) < 2) - { - continue; - } - var mission = TraitorMissionPrefab.RandomPrefab()?.Instantiate(); - if (mission != null) - { - Missions.Add(teamId, mission); - } - } - var canBeStartedCount = Missions.Sum(mission => mission.Value.CanBeStarted(server, this, mission.Key) ? 1 : 0); - if (canBeStartedCount >= Missions.Count) - { - var startSuccessCount = Missions.Sum(mission => mission.Value.Start(server, this, mission.Key) ? 1 : 0); - if (startSuccessCount >= Missions.Count) - { - return; - } - } - } - else - { - var mission = TraitorMissionPrefab.RandomPrefab()?.Instantiate(); - if (mission != null) - { - if (mission.CanBeStarted(server, this, CharacterTeamType.None)) - { - if (mission.Start(server, this, CharacterTeamType.None)) - { - Missions.Add(CharacterTeamType.None, mission); - return; - } - } - } - } - Missions.Clear(); - startCountdown = MathHelper.Lerp(server.ServerSettings.TraitorsMinRestartDelay, server.ServerSettings.TraitorsMaxRestartDelay, (float)RandomDouble()); } } } - public List GetEndResults() + public void SendCurrentState(TraitorEvent ev) { - List results = new List(); - -#if DISABLE_MISSIONS - return results; -#endif - if (GameMain.Server == null || !Missions.Any()) { return results; } - - foreach (var mission in Missions) - { - results.Add(new TraitorMissionResult(mission.Value)); - } - - return results; + if (ev?.Traitor == null) { return; } + var msg = new WriteOnlyMessage(); + msg.WriteByte((byte)ServerPacketHeader.TRAITOR_MESSAGE); + msg.WriteByte((byte)ev.CurrentState); + msg.WriteIdentifier(ev.Prefab.Identifier); + server.SendTraitorMessage(msg, ev.Traitor); } } -} +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs deleted file mode 100644 index cbaeabea5..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs +++ /dev/null @@ -1,394 +0,0 @@ -//#define ALLOW_SOLO_TRAITOR -//#define ALLOW_NONHUMANOID_TRAITOR - -using System; -using Barotrauma.Networking; -using Lidgren.Network; -using System.Collections.Generic; -using Barotrauma.IO; -using System.Linq; -using System.Security.Cryptography; -using Barotrauma.Extensions; - -namespace Barotrauma -{ - partial class Traitor - { - public class TraitorMission - { - private static string wordsTxt = Path.Combine("Content", "CodeWords.txt"); - - private readonly List allObjectives = new List(); - private readonly List pendingObjectives = new List(); - private readonly List completedObjectives = new List(); - - /// - /// Has the mission been completed (does not mean that the traitor necessarily won, the mission is considered completed if the traitor fails for whatever reason) - /// - public bool IsCompleted => pendingObjectives.Count <= 0; - - public readonly Dictionary Traitors = new Dictionary(); - - public delegate bool RoleFilter(Character character); - public readonly Dictionary Roles = new Dictionary(); - - public string StartText { get; private set; } - public string CodeWords { get; private set; } - public string CodeResponse { get; private set; } - - public string GlobalEndMessageSuccessTextId { get; private set; } - public string GlobalEndMessageSuccessDeadTextId { get; private set; } - public string GlobalEndMessageSuccessDetainedTextId { get; private set; } - public string GlobalEndMessageFailureTextId { get; private set; } - public string GlobalEndMessageFailureDeadTextId { get; private set; } - public string GlobalEndMessageFailureDetainedTextId { get; private set; } - - public readonly Identifier Identifier; - - public virtual IEnumerable GlobalEndMessageKeys => new string[] { "[traitorname]", "[traitorgoalinfos]" }; - public virtual IEnumerable GlobalEndMessageValues { - get { - var isSuccess = completedObjectives.Count >= allObjectives.Count; - return new string[] { - string.Join(", ", Traitors.Values.Select(traitor => traitor.Character?.Name ?? "(unknown)")), - (isSuccess ? completedObjectives.LastOrDefault() : pendingObjectives.FirstOrDefault())?.GoalInfos ?? "" - }; - } - } - - public string GlobalEndMessage - { - get - { - if (Traitors.Any() && allObjectives.Count > 0) - { - return TextManager.JoinServerMessages("\n", - Traitors.Values.Select(traitor => - { - var isSuccess = completedObjectives.Count >= allObjectives.Count; - var traitorIsDead = traitor.Character.IsDead; - var traitorIsDetained = traitor.Character.LockHands; - var messageId = isSuccess - ? (traitorIsDead ? GlobalEndMessageSuccessDeadTextId : traitorIsDetained ? GlobalEndMessageSuccessDetainedTextId : GlobalEndMessageSuccessTextId) - : (traitorIsDead ? GlobalEndMessageFailureDeadTextId : traitorIsDetained ? GlobalEndMessageFailureDetainedTextId : GlobalEndMessageFailureTextId); - return TextManager.FormatServerMessageWithPronouns(traitor.Character.Info, messageId, GlobalEndMessageKeys.Zip(GlobalEndMessageValues).ToArray()); - }).ToArray()); - } - return ""; - } - } - - public Objective GetCurrentObjective(Traitor traitor) - { - if (!Traitors.ContainsValue(traitor) || pendingObjectives.Count <= 0) - { - return null; - } - return pendingObjectives.Find(objective => objective.Roles.Contains(traitor.Role)); - } - - protected List> FindTraitorCandidates(GameServer server, CharacterTeamType team, RoleFilter traitorRoleFilter) - { - var traitorCandidates = new List>(); - foreach (Client c in server.ConnectedClients) - { - if (c.Character == null || c.Character.IsDead || c.Character.Removed || !traitorRoleFilter(c.Character) || - (team != CharacterTeamType.None && c.Character.TeamID != team)) - { - continue; - } -#if !ALLOW_NONHUMANOID_TRAITOR - if (!c.Character.IsHumanoid) { continue; } -#endif - traitorCandidates.Add(Tuple.Create(c, c.Character)); - } - return traitorCandidates; - } - - protected List FindCharacters() - { - List characters = new List(); - foreach (var character in Character.CharacterList) - { - characters.Add(character); - } - return characters; - } - - protected List>> AssignTraitors(GameServer server, TraitorManager traitorManager, CharacterTeamType team) - { - List characters = FindCharacters(); -#if !ALLOW_SOLO_TRAITOR - if (characters.Count < 2) - { - return null; - } -#endif - var roleCandidates = new Dictionary>>(); - foreach (var role in Roles) - { - roleCandidates.Add(role.Key, new HashSet>(FindTraitorCandidates(server, team, role.Value))); - if (roleCandidates[role.Key].Count <= 0) - { - return null; - } - } - var candidateRoleCounts = new Dictionary, int>(); - foreach (var candidateEntry in roleCandidates) - { - foreach (var candidate in candidateEntry.Value) - { - candidateRoleCounts[candidate] = candidateRoleCounts.TryGetValue(candidate, out var count) ? count + 1 : 1; - } - } - var unassignedRoles = new List(roleCandidates.Keys); - unassignedRoles.Sort((a, b) => roleCandidates[a].Count - roleCandidates[b].Count); - var assignedCandidates = new List>>(); - while (unassignedRoles.Count > 0) - { - var currentRole = unassignedRoles[0]; - var availableCandidates = roleCandidates[currentRole].ToList(); - if (availableCandidates.Count <= 0) - { - break; - } - unassignedRoles.RemoveAt(0); - availableCandidates.Sort((a, b) => candidateRoleCounts[b] - candidateRoleCounts[a]); - unassignedRoles.Sort((a, b) => roleCandidates[a].Count - roleCandidates[b].Count); - - int numCandidates = 1; - for (int i = 1; i < availableCandidates.Count && candidateRoleCounts[availableCandidates[i]] == candidateRoleCounts[availableCandidates[0]]; ++i) - { - ++numCandidates; - } - - var selected = ToolBox.SelectWeightedRandom(availableCandidates, availableCandidates.Select(c => Math.Max(c.Item1.RoundsSincePlayedAsTraitor, 0.1f)).ToList(), TraitorManager.Random); - assignedCandidates.Add(Tuple.Create(currentRole, selected)); - foreach (var candidate in roleCandidates.Values) - { - candidate.Remove(selected); - } - } - if (unassignedRoles.Count > 0) - { - return null; - } - return assignedCandidates; - } - - public bool CanBeStarted(GameServer server, TraitorManager traitorManager, CharacterTeamType team) - { - foreach (var role in Roles) - { - var candidates = FindTraitorCandidates(server, team, role.Value); - if (candidates.Count <= 0) - { - return false; - } - } - return AssignTraitors(server, traitorManager, team) != null; - } - - public bool Start(GameServer server, TraitorManager traitorManager, CharacterTeamType team) - { - var assignedCandidates = AssignTraitors(server, traitorManager, team); - if (assignedCandidates == null) - { - return false; - } - - foreach (Client client in server.ConnectedClients) - { - client.RoundsSincePlayedAsTraitor++; - } - - Traitors.Clear(); - foreach (var candidate in assignedCandidates) - { - var traitor = new Traitor(this, candidate.Item1, candidate.Item2.Item1.Character); - Traitors.Add(candidate.Item1, traitor); - candidate.Item2.Item1.RoundsSincePlayedAsTraitor = 0; - } - CodeWords = ToolBox.GetRandomLine(wordsTxt) + ", " + ToolBox.GetRandomLine(wordsTxt); - CodeResponse = ToolBox.GetRandomLine(wordsTxt) + ", " + ToolBox.GetRandomLine(wordsTxt); - - if (pendingObjectives.Count <= 0 || !pendingObjectives[0].CanBeStarted(Traitors.Values)) - { - Traitors.Clear(); - return false; - } - - var pendingMessages = new Dictionary>(); - pendingMessages.Clear(); - foreach (var traitor in Traitors.Values) - { - pendingMessages.Add(traitor, new List()); - } - pendingMessages.ForEach(traitor => traitor.Value.ForEach(message => traitor.Key.SendChatMessage(message, Identifier))); - pendingMessages.ForEach(traitor => traitor.Value.ForEach(message => traitor.Key.SendChatMessageBox(message, Identifier))); - - Update(0.0f, () => { GameMain.Server.TraitorManager.ShouldEndRound = true; }); -#if SERVER - foreach (var traitor in Traitors.Values) - { - GameServer.Log($"{GameServer.CharacterLogName(traitor.Character)} is a traitor and the current goals are:\n{(traitor.CurrentObjective?.GoalInfos != null ? TextManager.GetServerMessage(traitor.CurrentObjective?.GoalInfos) : "(empty)")}", ServerLog.MessageType.ServerMessage); - } -#endif - return true; - } - - public delegate void TraitorWinHandler(); - - public void Update(float deltaTime, TraitorWinHandler winHandler) - { - if (pendingObjectives.Count <= 0 || Traitors.Count <= 0) - { - return; - } - if (Traitors.Values.Any(traitor => traitor.Character == null || traitor.Character.IsDead || traitor.Character.Removed)) - { - Traitors.Values.ForEach(traitor => traitor.UpdateCurrentObjective("", Identifier)); - pendingObjectives.Clear(); - Traitors.Clear(); - return; - } - var startedObjectives = new List(); - foreach (var traitor in Traitors.Values) - { - startedObjectives.Clear(); - while (pendingObjectives.Count > 0) - { - var objective = GetCurrentObjective(traitor); - if (objective == null) - { - // No more objectives left for traitor or waiting for another traitor's objective. - break; - } - if (!objective.IsStarted) - { - if (!objective.Start(traitor)) - { - //the mission fails if an objective cannot be started - if (completedObjectives.Count > 0) - { - objective.EndMessage(); - } - pendingObjectives.Clear(); - break; - } - startedObjectives.Add(objective); - } - objective.Update(deltaTime); - if (objective.IsCompleted) - { - pendingObjectives.Remove(objective); - completedObjectives.Add(objective); - objective.EndMessage(); - continue; - } - if (objective.IsStarted && !objective.CanBeCompleted) - { - objective.EndMessage(); - pendingObjectives.Clear(); - } - break; - } - if (pendingObjectives.Count > 0) - { - startedObjectives.ForEach(objective => objective.StartMessage()); - } - } - if (completedObjectives.Count >= allObjectives.Count) - { - foreach (var traitor in Traitors) - { - SteamAchievementManager.OnTraitorWin(traitor.Value.Character); - } - winHandler(); - } - } - - public delegate bool CharacterFilter(Character character); - public List FindKillTarget(Character traitor, CharacterFilter filter, int count = -1, float percentage = -1f) - { - if (traitor == null) { return null; } - - List validCharacters = Character.CharacterList.FindAll(c => c.TeamID == traitor.TeamID && - c != traitor && !c.IsDead && - (filter == null || filter(c))); - - int targetCount = 1; - if (count > 0) - { - targetCount = count; - } - else if (percentage > 0f) - { - targetCount = (int)Math.Max(1, Math.Floor(validCharacters.Count * percentage)); - } - - List targetCharacters = new List(); - - if (validCharacters.Count > 0) - { - for (int i = 0; i < targetCount; i++) - { - if (validCharacters.Count == 0) break; - Character character = validCharacters[TraitorManager.RandomInt(validCharacters.Count)]; - targetCharacters.Add(character); - validCharacters.Remove(character); - } - return targetCharacters; - } - -#if ALLOW_SOLO_TRAITOR - targetCharacters.Add(traitor); - return targetCharacters; -#else - return null; -#endif - } - - public string GetTargetNames(List targets) - { - string names = string.Empty; - for (int i = 0; i < targets.Count; i++) - { - names += targets[i].Name; - - if (i < targets.Count - 1) - { - names += ", "; - } - } - - if (names.Length > 0) - { - return names; - } - else - { - return TextManager.FormatServerMessage("unknown"); - } - } - - public TraitorMission(Identifier identifier, string startText, string globalEndMessageSuccessTextId, string globalEndMessageSuccessDeadTextId, string globalEndMessageSuccessDetainedTextId, string globalEndMessageFailureTextId, string globalEndMessageFailureDeadTextId, string globalEndMessageFailureDetainedTextId, IEnumerable> roles, ICollection objectives) - { - Identifier = identifier; - StartText = startText; - GlobalEndMessageSuccessTextId = globalEndMessageSuccessTextId; - GlobalEndMessageSuccessDeadTextId = globalEndMessageSuccessDeadTextId; - GlobalEndMessageSuccessDetainedTextId = globalEndMessageSuccessDetainedTextId; - GlobalEndMessageFailureTextId = globalEndMessageFailureTextId; - GlobalEndMessageFailureDeadTextId = globalEndMessageFailureDeadTextId; - GlobalEndMessageFailureDetainedTextId = globalEndMessageFailureDetainedTextId; - foreach (var role in roles) - { - Roles.Add(role.Key, role.Value); - } - allObjectives.AddRange(objectives); - pendingObjectives.AddRange(objectives); - } - } - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMissionPrefab.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMissionPrefab.cs deleted file mode 100644 index a9aa33810..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMissionPrefab.cs +++ /dev/null @@ -1,664 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Xml.Linq; -using System.Linq; -using Barotrauma.Networking; - -namespace Barotrauma -{ - class TraitorMissionPrefab - { - public class TraitorMissionEntry : Prefab - { - public static PrefabCollection Prefabs => TraitorMissionPrefab.Prefabs; - - public readonly TraitorMissionPrefab Prefab; - public float SelectedWeight; - - public TraitorMissionEntry(ContentXElement element, TraitorMissionsFile file) : base(file, element) - { - Prefab = new TraitorMissionPrefab(element); - } - - public override void Dispose() { } - } - public static readonly PrefabCollection Prefabs = new PrefabCollection(); - - public static TraitorMissionPrefab RandomPrefab() - { - var selected = ToolBox.SelectWeightedRandom(Prefabs.ToList(), Prefabs.Select(mission => Math.Max(mission.SelectedWeight, 0.1f)).ToList(), TraitorManager.Random); - //the weight of the missions that didn't get selected keeps growing the make them more likely to get picked - foreach (var mission in Prefabs) - { - mission.SelectedWeight += 10; - } - selected.SelectedWeight = 0.0f; - return selected.Prefab; - } - - private class AttributeChecker : IDisposable - { - private readonly XElement element; - private readonly HashSet required = new HashSet(); - private readonly HashSet optional = new HashSet(); - - public void Optional(params string[] names) - { - optional.UnionWith(names); - } - - public void Required(params string[] names) - { - required.UnionWith(names); - } - - public void Dispose() - { - foreach (var requiredName in required) - { - if (element.Attributes().All(attribute => attribute.Name != requiredName)) - { - GameServer.Log($"Required attribute \"{requiredName}\" is missing in \"{element.Name}\"", ServerLog.MessageType.Error); - } - } - foreach (var attribute in element.Attributes()) - { - var attributeName = attribute.Name.ToString(); - if (!required.Contains(attributeName) && !optional.Contains(attributeName)) - { - GameServer.Log($"Unsupported attribute \"{attributeName}\" in \"{element.Name}\"", ServerLog.MessageType.Error); - } - } - } - - public AttributeChecker(XElement element) - { - this.element = element; - } - } - - public class Goal - { - public readonly string Type; - public readonly XElement Config; - - public Goal(string type, XElement config) - { - Type = type; - Config = config; - } - - private delegate bool TargetFilter(string value, Character character); - private static Dictionary targetFilters = new Dictionary() - { - { "job", (value, character) => value == character.Info.Job.Prefab.Identifier }, - { "role", (value, character) => value.Equals(GameMain.Server.TraitorManager.GetTraitorRole(character), StringComparison.OrdinalIgnoreCase) } - }; - - public Traitor.Goal Instantiate() - { - Traitor.Goal goal = null; - using (var checker = new AttributeChecker(Config)) - { - checker.Required("type"); - var goalType = Config.GetAttributeString("type", ""); - switch (goalType.ToLowerInvariant()) - { - case "killtarget": - { - checker.Optional(targetFilters.Keys.ToArray()); - checker.Optional("causeofdeath"); - checker.Optional("affliction"); - checker.Optional("roomname"); - checker.Optional("targetcount"); - checker.Optional("targetpercentage"); - List killFilters = new List(); - foreach (var attribute in Config.Attributes()) - { - if (targetFilters.TryGetValue(attribute.Name.ToString().ToLower(System.Globalization.CultureInfo.InvariantCulture), out var filter)) - { - killFilters.Add((character) => filter(attribute.Value, character)); - } - } - goal = new Traitor.GoalKillTarget((character) => killFilters.All(f => f(character)), - (CauseOfDeathType)Enum.Parse(typeof(CauseOfDeathType), Config.GetAttributeString("causeofdeath", "Unknown"), true), - Config.GetAttributeString("affliction", null), Config.GetAttributeString("targethull", null), Config.GetAttributeInt("targetcount", -1), - Config.GetAttributeFloat("targetpercentage", -1f)); - break; - } - case "destroyitems": - { - checker.Required("tag"); - checker.Optional("percentage", "matchIdentifier", "matchTag", "matchInventory"); - var tag = Config.GetAttributeString("tag", null); - if (tag != null) - { - goal = new Traitor.GoalDestroyItemsWithTag( - tag, - Config.GetAttributeFloat("percentage", 100.0f) / 100.0f, - Config.GetAttributeBool("matchIdentifier", true), - Config.GetAttributeBool("matchTag", true), - Config.GetAttributeBool("matchInventory", false)); - } - break; - } - case "sabotage": - { - checker.Required("tag"); - checker.Optional("threshold"); - var tag = Config.GetAttributeString("tag", null); - if (tag != null) - { - goal = new Traitor.GoalSabotageItems(tag, Config.GetAttributeFloat("threshold", 20.0f)); - } - break; - } - case "floodsub": - checker.Optional("percentage"); - goal = new Traitor.GoalFloodPercentOfSub(Config.GetAttributeFloat("percentage", 100.0f) / 100.0f); - break; - case "finditem": - checker.Required("identifier"); - checker.Optional("preferNew", "allowNew", "allowExisting", "allowedContainers", "percentage"); - List itemCountFilters = new List(); - foreach (var attribute in Config.Attributes()) - { - if (targetFilters.TryGetValue(attribute.Name.ToString().ToLower(System.Globalization.CultureInfo.InvariantCulture), out var filter)) - { - itemCountFilters.Add((character) => filter(attribute.Value, character)); - } - } - goal = new Traitor.GoalFindItem((character) => itemCountFilters.All(f => f(character)), - Config.GetAttributeString("identifier", - null), - Config.GetAttributeBool("preferNew", - true), - Config.GetAttributeBool("allowNew", - true), - Config.GetAttributeBool("allowExisting", - true), - Config.GetAttributeFloat("percentage", - -1f), - Config.GetAttributeIdentifierArray("allowedContainers", - new string[] - { - "steelcabinet", - "mediumsteelcabinet", - "suppliescabinet" - }.ToIdentifiers())); - break; - case "replaceinventory": - checker.Required("containers", "replacements"); - checker.Optional("percentage"); - goal = new Traitor.GoalReplaceInventory(Config.GetAttributeIdentifierArray("containers", new Identifier[] { }), Config.GetAttributeIdentifierArray("replacements", new Identifier[] { }), Config.GetAttributeFloat("percentage", 100.0f) / 100.0f); - break; - case "reachdistancefromsub": - checker.Optional("distance"); - goal = new Traitor.GoalReachDistanceFromSub(Config.GetAttributeFloat("distance", 125f)); - break; - case "injectpoison": - checker.Optional(targetFilters.Keys.ToArray()); - checker.Required("poison"); - checker.Required("affliction"); - checker.Optional("targetcount"); - checker.Optional("targetpercentage"); - List poisonFilters = new List(); - foreach (var attribute in Config.Attributes()) - { - if (targetFilters.TryGetValue(attribute.Name.ToString().ToLower(System.Globalization.CultureInfo.InvariantCulture), out var filter)) - { - poisonFilters.Add((character) => filter(attribute.Value, character)); - } - } - goal = new Traitor.GoalInjectTarget((character) => poisonFilters.All(f => f(character)), Config.GetAttributeString("poison", null), - Config.GetAttributeString("affliction", null), Config.GetAttributeInt("targetcount", -1), Config.GetAttributeFloat("targetpercentage", -1f)); - break; - case "unwire": - checker.Required("tag"); - checker.Optional("connectionname"); - checker.Optional("connectiondisplayname"); - goal = new Traitor.GoalUnwiring(Config.GetAttributeString("tag", null), Config.GetAttributeString("connectionname", null), Config.GetAttributeString("connectiondisplayname)", null)); - break; - case "transformentity": - checker.Required("entities", "entitytypes"); - checker.Optional("catalystid"); - goal = new Traitor.GoalEntityTransformation(Config.GetAttributeStringArray("entities", null), Config.GetAttributeStringArray("entitytypes", null), Config.GetAttributeString("catalystid", null)); - break; - case "keeptransformedalive": - checker.Required("speciesname"); - goal = new Traitor.GoalKeepTransformedAlive(Config.GetAttributeIdentifier("speciesname", Identifier.Empty)); - break; - default: - GameServer.Log($"Unrecognized goal type \"{goalType}\".", ServerLog.MessageType.Error); - break; - } - } - if (goal == null) - { - return null; - } - foreach (var element in Config.Elements()) - { - switch (element.Name.ToString().ToLowerInvariant()) - { - case "modifier": - { - using (var checker = new AttributeChecker(element)) - { - checker.Required("type"); - var modifierType = element.GetAttributeString("type", ""); - switch (modifierType) - { - case "duration": - { - checker.Optional("cumulative", "duration", "infotext"); - var isCumulative = element.GetAttributeBool("cumulative", false); - goal = new Traitor.GoalHasDuration(goal, element.GetAttributeFloat("duration", 5.0f), isCumulative, element.GetAttributeString("infotext", isCumulative ? "TraitorGoalWithCumulativeDurationInfoText" : "TraitorGoalWithDurationInfoText")); - break; - } - case "timelimit": - checker.Optional("timelimit", "infotext"); - goal = new Traitor.GoalHasTimeLimit(goal, element.GetAttributeFloat("timelimit", 180.0f), element.GetAttributeString("infotext", "TraitorGoalWithTimeLimitInfoText")); - break; - case "optional": - checker.Optional("infotext"); - goal = new Traitor.GoalIsOptional(goal, element.GetAttributeString("infotext", "TraitorGoalIsOptionalInfoText")); - break; - default: - GameServer.Log($"Unrecognized modifier type \"{modifierType}\".", ServerLog.MessageType.Error); - break; - } - } - break; - } - } - } - foreach (var element in Config.Elements()) - { - var elementName = element.Name.ToString().ToLowerInvariant(); - switch (elementName) - { - case "modifier": - // loaded above - break; - case "infotext": - { - using (var checker = new AttributeChecker(element)) - { - checker.Required("id"); - var id = element.GetAttributeString("id", null); - if (id != null) - { - goal.InfoTextId = id; - } - } - break; - } - case "completedtext": - { - using (var checker = new AttributeChecker(element)) - { - checker.Required("id"); - var id = element.GetAttributeString("id", null); - if (id != null) - { - goal.CompletedTextId = id; - } - } - break; - } - default: - GameServer.Log($"Unrecognized element \"{element.Name}\" in goal.", ServerLog.MessageType.Error); - break; - } - } - return goal; - } - } - - - public abstract class ObjectiveBase - { - public HashSet Roles { get; } = new HashSet(); - - public abstract void InstantiateGoals(); - public abstract Traitor.Objective Instantiate(IEnumerable roles); - } - - protected class Objective : ObjectiveBase - { - public string InfoText { get; internal set; } - public string StartMessageTextId { get; internal set; } - public string StartMessageServerTextId { get; internal set; } - public string EndMessageSuccessTextId { get; internal set; } - public string EndMessageSuccessDeadTextId { get; internal set; } - public string EndMessageSuccessDetainedTextId { get; internal set; } - public string EndMessageFailureTextId { get; internal set; } - public string EndMessageFailureDeadTextId { get; internal set; } - public string EndMessageFailureDetainedTextId { get; internal set; } - public int ShuffleGoalsCount { get; internal set; } - - public readonly List Goals = new List(); - - private List goalInstances = null; - - public override void InstantiateGoals() - { - goalInstances = Goals.ConvertAll(goal => - { - var instance = goal.Instantiate(); - if (instance == null) - { - GameServer.Log($"Failed to instantiate goal \"{goal.Type}\".", ServerLog.MessageType.Error); - } - return instance; - }).FindAll(goal => goal != null); - } - - public override Traitor.Objective Instantiate(IEnumerable roles) - { - var result = new Traitor.Objective(InfoText, ShuffleGoalsCount, roles.ToArray(), goalInstances); - if (StartMessageTextId != null) - { - result.StartMessageTextId = StartMessageTextId; - } - if (StartMessageServerTextId != null) - { - result.StartMessageServerTextId = StartMessageServerTextId; - } - if (EndMessageSuccessTextId != null) - { - result.EndMessageSuccessTextId = EndMessageSuccessTextId; - } - if (EndMessageSuccessDeadTextId != null) - { - result.EndMessageSuccessDeadTextId = EndMessageSuccessDeadTextId; - } - if (EndMessageSuccessDetainedTextId != null) - { - result.EndMessageSuccessDetainedTextId = EndMessageSuccessDetainedTextId; - } - if (EndMessageFailureTextId != null) - { - result.EndMessageFailureTextId = EndMessageFailureTextId; - } - if (EndMessageFailureDeadTextId != null) - { - result.EndMessageFailureDeadTextId = EndMessageFailureDeadTextId; - } - if (EndMessageFailureDetainedTextId != null) - { - result.EndMessageFailureDetainedTextId = EndMessageFailureDetainedTextId; - } - return result; - } - } - - protected class WaitObjective : ObjectiveBase - { - private Traitor.GoalWaitForTraitors sharedGoal; - - public override void InstantiateGoals() - { - sharedGoal = new Traitor.GoalWaitForTraitors(Roles.Count); - } - - public override Traitor.Objective Instantiate(IEnumerable roles) - { - return new Traitor.Objective("TraitorObjectiveInfoTextWaitForOtherTraitors", -1, roles.ToArray(), new[] { sharedGoal }); - } - - public WaitObjective(ICollection roles) - { - Roles.UnionWith(roles); - } - } - - public class Role - { - public readonly Traitor.TraitorMission.RoleFilter Filter; - - public Role(IEnumerable filters) - { - Filter = character => filters.All(filter => filter(character)); - } - - public Role() - { - Filter = character => true; - } - } - public readonly Dictionary Roles = new Dictionary(); - - public readonly Identifier Identifier; - public readonly string StartText; - public readonly string EndMessageSuccessText; - public readonly string EndMessageSuccessDeadText; - public readonly string EndMessageSuccessDetainedText; - public readonly string EndMessageFailureText; - public readonly string EndMessageFailureDeadText; - public readonly string EndMessageFailureDetainedText; - - public readonly List Objectives = new List(); - - public Traitor.TraitorMission Instantiate() - { - var objectivesWithSync = new List(); - var objectivesCount = Objectives.Count; - if (objectivesCount > 0) - { - var pendingRoles = new HashSet(); - var pendingCount = 1; - objectivesWithSync.Add(Objectives[0]); - pendingRoles.UnionWith(Objectives[0].Roles); - for (var i = 1; i < objectivesCount; ++i) - { - var objective = Objectives[i]; - if (pendingRoles.IsSupersetOf(objective.Roles)) - { - if (pendingCount > 1) - { - objectivesWithSync.Add(new WaitObjective(objective.Roles)); - } - pendingRoles.Clear(); - pendingCount = 0; - } - objectivesWithSync.Add(objective); - pendingRoles.UnionWith(objective.Roles); - ++pendingCount; - } - if (pendingCount > 1 && pendingRoles.IsSubsetOf(Roles.Keys)) - { - // TODO: If last objective includes only one traitor, other traitors will get the wrong end message. - objectivesWithSync.Add(new WaitObjective(Roles.Keys)); - } - } - - return new Traitor.TraitorMission( - Identifier, - StartText ?? "TraitorMissionStartMessage", - EndMessageSuccessText ?? "TraitorObjectiveEndMessageSuccess", - EndMessageSuccessDeadText ?? "TraitorObjectiveEndMessageSuccessDead", - EndMessageSuccessDetainedText ?? "TraitorObjectiveEndMessageSuccessDetained", - EndMessageFailureText ?? "TraitorObjectiveEndMessageFailure", - EndMessageFailureDeadText ?? "TraitorObjectiveEndMessageFailureDead", - EndMessageFailureDetainedText ?? "TraitorObjectiveEndMessageFailureDetained", - Roles.ToDictionary(kv => kv.Key, kv => kv.Value.Filter), - objectivesWithSync.SelectMany(objective => - { - objective.InstantiateGoals(); - return objective.Roles.Select(role => objective.Instantiate(new[] { role })); - }).ToArray()); - } - - protected Goal LoadGoal(XElement goalRoot) - { - var goalType = goalRoot.GetAttributeString("type", ""); - return new Goal(goalType, goalRoot); - } - - protected Objective LoadObjective(XElement objectiveRoot, string[] allRoles) - { - var allRolesSet = new HashSet(allRoles); - var result = new Objective - { - ShuffleGoalsCount = objectiveRoot.GetAttributeInt("shuffleGoalsCount", -1) - }; - var objectiveRoles = objectiveRoot.GetAttributeStringArray("roles", allRoles); - if (!allRolesSet.IsSupersetOf(objectiveRoles)) - { - var unrecognized = new HashSet(objectiveRoles); - unrecognized.ExceptWith(allRoles); - GameServer.Log($"Undefined role(s) \"{string.Join(", ", unrecognized)}\" set for Objective.", ServerLog.MessageType.Error); - } - result.Roles.UnionWith(allRolesSet.Intersect(objectiveRoles)); - - foreach (var element in objectiveRoot.Elements()) - { - using (var checker = new AttributeChecker(element)) - { - switch (element.Name.ToString().ToLowerInvariant()) - { - case "infotext": - checker.Required("id"); - result.InfoText = element.GetAttributeString("id", null); - break; - case "startmessage": - checker.Required("id"); - result.StartMessageTextId = element.GetAttributeString("id", null); - break; - case "startmessageserver": - checker.Required("id"); - result.StartMessageServerTextId = element.GetAttributeString("id", null); - break; - case "endmessagesuccess": - checker.Required("id"); - result.EndMessageSuccessTextId = element.GetAttributeString("id", null); - break; - case "endmessagesuccessdead": - checker.Required("id"); - result.EndMessageSuccessDeadTextId = element.GetAttributeString("id", null); - break; - case "endmessagesuccessdetained": - checker.Required("id"); - result.EndMessageSuccessDetainedTextId = element.GetAttributeString("id", null); - break; - case "endmessagefailure": - checker.Required("id"); - result.EndMessageFailureTextId = element.GetAttributeString("id", null); - break; - case "endmessagefailuredead": - checker.Required("id"); - result.EndMessageFailureDeadTextId = element.GetAttributeString("id", null); - break; - case "endmessagefailuredetained": - checker.Required("id"); - result.EndMessageFailureDetainedTextId = element.GetAttributeString("id", null); - break; - case "goal": - { - var goal = LoadGoal(element); - if (goal != null) - { - result.Goals.Add(goal); - } - break; - } - default: - GameServer.Log($"Unrecognized element \"{element.Name}\" under Objective.", ServerLog.MessageType.Error); - break; - } - } - } - return result; - } - - protected Role LoadRole(XElement roleRoot) - { - var filters = new List(); - var jobs = roleRoot.GetAttributeStringArray("jobs", null); - if (jobs != null) - { - var jobsSet = new HashSet(jobs.Select(job => job.ToLower(CultureInfo.InvariantCulture))); - filters.Add(character => character.Info?.Job != null && jobsSet.Contains(character.Info.Job.Name.ToLower().Value)); - } - return new Role(filters); - } - - public TraitorMissionPrefab(ContentXElement missionRoot) - { - Identifier = missionRoot.GetAttributeIdentifier("identifier", Identifier.Empty); - foreach (var element in missionRoot.Elements()) - { - using (var checker = new AttributeChecker(element)) - { - switch (element.Name.ToString().ToLowerInvariant()) - { - case "role": - checker.Required("id"); - checker.Optional("jobs"); - Roles.Add(element.GetAttributeString("id", null), LoadRole(element)); - break; - } - } - } - if (!Roles.Any()) - { - Roles.Add("traitor", new Role()); - } - foreach (var element in missionRoot.Elements()) - { - using (var checker = new AttributeChecker(element)) - { - switch (element.Name.ToString().ToLowerInvariant()) - { - case "role": - // handled above - break; - case "startinfotext": - checker.Required("id"); - StartText = element.GetAttributeString("id", null); - break; - case "endmessagesuccess": - checker.Required("id"); - EndMessageSuccessText = element.GetAttributeString("id", null); - break; - case "endmessagesuccessdead": - checker.Required("id"); - EndMessageSuccessDeadText = element.GetAttributeString("id", null); - break; - case "endmessagesuccessdetained": - checker.Required("id"); - EndMessageSuccessDetainedText = element.GetAttributeString("id", null); - break; - case "endmessagefailure": - checker.Required("id"); - EndMessageFailureText = element.GetAttributeString("id", null); - break; - case "endmessagefailuredead": - checker.Required("id"); - EndMessageFailureDeadText = element.GetAttributeString("id", null); - break; - case "endmessagefailuredetained": - checker.Required("id"); - EndMessageFailureDetainedText = element.GetAttributeString("id", null); - break; - case "objective": - { - var objective = LoadObjective(element, Roles.Keys.ToArray()); - if (objective != null) - { - Objectives.Add(objective); - } - break; - } - default: - GameServer.Log($"Unrecognized element \"{element.Name}\"under TraitorMission.", ServerLog.MessageType.Error); - break; - } - } - } - } - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMissionResult.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMissionResult.cs deleted file mode 100644 index 6fe640c28..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMissionResult.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Barotrauma.Networking; - -namespace Barotrauma -{ - partial class TraitorMissionResult - { - public TraitorMissionResult(Traitor.TraitorMission mission) - { - MissionIdentifier = mission.Identifier; - EndMessage = mission.GlobalEndMessage; - Success = mission.IsCompleted; - foreach (Traitor traitor in mission.Traitors.Values) - { - Characters.Add(traitor.Character); - } - } - - public void ServerWrite(IWriteMessage msg) - { - msg.WriteIdentifier(MissionIdentifier); - msg.WriteString(EndMessage); - msg.WriteBoolean(Success); - msg.WriteByte((byte)Characters.Count); - foreach (Character character in Characters) - { - msg.WriteUInt16(character.ID); - } - } - } -} diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 4c66ef099..898f1765a 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.0.20.1 + 1.1.14.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer @@ -62,6 +62,7 @@ + diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs index a5069b8f3..2fc63b541 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs @@ -331,6 +331,11 @@ namespace Barotrauma } } if (targetSlot < 0) { return false; } + //the item should always stay in the Any slot if it's containable in one + if (pickable.AllowedSlots.Contains(InvSlotType.Any)) + { + targetInventory.TryPutItem(item, Character, CharacterInventory.AnySlot); + } return targetInventory.TryPutItem(item, targetSlot, allowSwapping, allowCombine: false, Character); } else @@ -521,8 +526,5 @@ namespace Barotrauma protected virtual void OnStateChanged(AIState from, AIState to) { } protected virtual void OnTargetChanged(AITarget previousTarget, AITarget newTarget) { } - - public virtual void ClientRead(IReadMessage msg) { } - public virtual void ServerWrite(IWriteMessage msg) { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index caa1b2919..9495c3e08 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -344,12 +344,12 @@ namespace Barotrauma private Identifier GetTargetingTag(AITarget aiTarget) { if (aiTarget?.Entity == null) { return Identifier.Empty; } - string targetingTag = string.Empty; + Identifier targetingTag = Identifier.Empty; if (aiTarget.Entity is Character targetCharacter) { if (targetCharacter.IsDead) { - targetingTag = "dead"; + targetingTag = "dead".ToIdentifier(); } else if (AIParams.TryGetTarget(targetCharacter.CharacterHealth.GetActiveAfflictionTags(), out CharacterParams.TargetParams tp) && tp.Threshold >= Character.GetDamageDoneByAttacker(targetCharacter)) { @@ -357,11 +357,11 @@ namespace Barotrauma } else if (PetBehavior != null && aiTarget.Entity == PetBehavior.Owner) { - targetingTag = "owner"; + targetingTag = "owner".ToIdentifier(); } else if (PetBehavior != null && (!Character.IsOnFriendlyTeam(targetCharacter) || IsAttackingOwner(targetCharacter))) { - targetingTag = "hostile"; + targetingTag = "hostile".ToIdentifier(); } else if (AIParams.TryGetTarget(targetCharacter, out CharacterParams.TargetParams tP)) { @@ -373,25 +373,25 @@ namespace Barotrauma { // Pets see other pets as pets by default. // Monsters see them only as pet only when they have a matching ai target. Otherwise they use the other tags, specified below. - targetingTag = "pet"; + targetingTag = "pet".ToIdentifier(); } else if (targetCharacter.IsHusk && AIParams.HasTag("husk")) { - targetingTag = "husk"; + targetingTag = "husk".ToIdentifier(); } else if (!Character.IsSameSpeciesOrGroup(targetCharacter)) { if (enemy.CombatStrength > CombatStrength) { - targetingTag = "stronger"; + targetingTag = "stronger".ToIdentifier(); } else if (enemy.CombatStrength < CombatStrength) { - targetingTag = "weaker"; + targetingTag = "weaker".ToIdentifier(); } else { - targetingTag = "equal"; + targetingTag = "equal".ToIdentifier(); } } } @@ -406,27 +406,27 @@ namespace Barotrauma break; } } - if (targetingTag.IsNullOrEmpty()) + if (targetingTag.IsEmpty) { if (targetItem.GetComponent() != null) { - targetingTag = "sonar"; + targetingTag = "sonar".ToIdentifier(); } if (targetItem.GetComponent() != null) { - targetingTag = "door"; + targetingTag = "door".ToIdentifier(); } } } else if (aiTarget.Entity is Structure) { - targetingTag = "wall"; + targetingTag = "wall".ToIdentifier(); } else if (aiTarget.Entity is Hull) { - targetingTag = "room"; + targetingTag = "room".ToIdentifier(); } - return targetingTag.ToIdentifier(); + return targetingTag; } public override void SelectTarget(AITarget target) => SelectTarget(target, 100); @@ -767,7 +767,7 @@ namespace Barotrauma mainLimb.body.SmoothRotate(rotation, Character.AnimController.SwimFastParams.TorsoTorque); } } - if (disableTailCoroutine == null && SelectedAiTarget.Entity is Item i && i.HasTag("guardianshelter")) + if (disableTailCoroutine == null && SelectedAiTarget.Entity is Item i && i.HasTag(Tags.GuardianShelter)) { if (!CoroutineManager.IsCoroutineRunning(disableTailCoroutine)) { @@ -864,10 +864,11 @@ namespace Barotrauma } // Ensure that the creature keeps inside the level SteerInsideLevel(deltaTime); - float speed = Character.AnimController.GetCurrentSpeed(run && Character.CanRun); - steeringManager.Update(speed); - float targetMovement = useSteeringLengthAsMovementSpeed ? Steering.Length() : speed; - Character.AnimController.TargetMovement = Character.ApplyMovementLimits(Steering, targetMovement); + float defaultSpeed = Character.AnimController.GetCurrentSpeed(run && Character.CanRun); + //calculate a normalized Steering value at this point: we multiply it with the actual, desired speed in ApplyMovementLimits + steeringManager.Update(1.0f); + float speed = useSteeringLengthAsMovementSpeed ? Steering.Length() : defaultSpeed; + Character.AnimController.TargetMovement = Character.ApplyMovementLimits(Steering, speed); if (Character.CurrentHull != null && Character.AnimController.InWater) { // Limit the swimming speed inside the sub. @@ -1935,14 +1936,7 @@ namespace Barotrauma } else if (advance) { - if (pathSteering != null) - { - pathSteering.SteeringSeek(steerPos, weight: 10, minGapWidth: minGapSize); - } - else - { - SteeringManager.SteeringSeek(steerPos, 10); - } + SteeringManager.SteeringSeek(steerPos, 10); } else { @@ -2310,7 +2304,7 @@ namespace Barotrauma if (damageTarget != null) { Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true); - item.Use(deltaTime, Character); + item.Use(deltaTime, user: Character); } } } @@ -2545,6 +2539,7 @@ namespace Barotrauma item.body.LinearVelocity *= 0.9f; item.body.LinearVelocity -= velocity * 0.25f; bool wasBroken = item.Condition <= 0.0f; + item.LastEatenTime = (float)Timing.TotalTimeUnpaused; item.AddDamage(Character, item.WorldPosition, new Attack(0.0f, 0.0f, 0.0f, 0.0f, 0.02f * Character.Params.EatingSpeed), deltaTime); Character.ApplyStatusEffects(ActionType.OnEating, deltaTime); if (item.Condition <= 0.0f) @@ -3116,6 +3111,13 @@ namespace Barotrauma // ignore if owner is tagged to be explicitly ignored (Feign Death) continue; } + var characterTargetingTag = GetTargetingTag(owner.AiTarget); + if (!characterTargetingTag.IsEmpty) + { + // if the enemy is configured to ignore the target character, ignore the provocative item they're holding/wearing too + var characterTargetingParams = GetTargetParams(characterTargetingTag); + if (characterTargetingParams?.State == AIState.Idle) { continue; } + } } } if (targetCharacter != null) @@ -4055,18 +4057,6 @@ namespace Barotrauma } return null; } - - public override void ServerWrite(IWriteMessage msg) - { - msg.WriteByte((byte)State); - PetBehavior?.ServerWrite(msg); - } - - public override void ClientRead(IReadMessage msg) - { - State = (AIState)msg.ReadByte(); - PetBehavior?.ClientRead(msg); - } } //the "memory" of the Character diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 890772d58..43378e91a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -355,7 +355,7 @@ namespace Barotrauma Vector2 forward = VectorExtensions.Forward(rotation); float angle = MathHelper.ToDegrees(VectorExtensions.Angle(toTarget, forward)); if (angle > 70) { continue; } - if (!Character.CanSeeCharacter(c)) { continue; } + if (!Character.CanSeeTarget(c)) { continue; } if (dist < closestDistance || closestEnemy == null) { closestEnemy = c; @@ -465,7 +465,7 @@ namespace Barotrauma else { // Allows bots to heal targets autonomously while swimming outside of the sub. - if (AIObjectiveRescueAll.IsValidTarget(Character, Character)) + if (AIObjectiveRescueAll.IsValidTarget(Character, Character, out _)) { AddTargets(Character, Character); } @@ -480,24 +480,33 @@ namespace Barotrauma if (objectiveManager.CurrentObjective == null) { return; } objectiveManager.DoCurrentObjective(deltaTime); - bool run = (objectiveManager.CurrentObjective.ForceRun && !objectiveManager.CurrentObjective.ForceWalk) || (!objectiveManager.CurrentObjective.ForceWalk && objectiveManager.GetCurrentPriority() > AIObjectiveManager.RunPriority); - if (ObjectiveManager.CurrentObjective is AIObjectiveGoTo goTo && goTo.Target != null) + var currentObjective = objectiveManager.CurrentObjective; + bool run = !currentObjective.ForceWalk && (currentObjective.ForceRun || objectiveManager.GetCurrentPriority() > AIObjectiveManager.RunPriority); + if (currentObjective is AIObjectiveGoTo goTo) { - if (Character.CurrentHull == null) + if (run && goTo == objectiveManager.ForcedOrder && goTo.IsWaitOrder && !Character.IsOnPlayerTeam) { - run = Vector2.DistanceSquared(Character.WorldPosition, goTo.Target.WorldPosition) > 300 * 300; + // NPCs with a wait order don't run. + run = false; } - else + else if (goTo.Target != null) { - float yDiff = goTo.Target.WorldPosition.Y - Character.WorldPosition.Y; - if (Math.Abs(yDiff) > 100) + if (Character.CurrentHull == null) { - run = true; + run = Vector2.DistanceSquared(Character.WorldPosition, goTo.Target.WorldPosition) > 300 * 300; } else { - float xDiff = goTo.Target.WorldPosition.X - Character.WorldPosition.X; - run = Math.Abs(xDiff) > 500; + float yDiff = goTo.Target.WorldPosition.Y - Character.WorldPosition.Y; + if (Math.Abs(yDiff) > 100) + { + run = true; + } + else + { + float xDiff = goTo.Target.WorldPosition.X - Character.WorldPosition.X; + run = Math.Abs(xDiff) > 500; + } } } } @@ -577,7 +586,7 @@ namespace Barotrauma if (Character.LockHands) { return; } if (ObjectiveManager.CurrentObjective == null) { return; } if (Character.CurrentHull == null) { return; } - bool shouldActOnSuffocation = Character.IsLowInOxygen && !Character.AnimController.HeadInWater && HasDivingSuit(Character, requireOxygenTank: false) && !HasItem(Character, AIObjectiveFindDivingGear.OXYGEN_SOURCE, out _, conditionPercentage: 1); + bool shouldActOnSuffocation = Character.IsLowInOxygen && !Character.AnimController.HeadInWater && HasDivingSuit(Character, requireOxygenTank: false) && !HasItem(Character, Tags.OxygenSource, out _, conditionPercentage: 1); bool isCarrying = ObjectiveManager.HasActiveObjective() || ObjectiveManager.HasActiveObjective(); bool NeedsDivingGearOnPath(AIObjectiveGoTo gotoObjective) @@ -594,7 +603,7 @@ namespace Barotrauma if (findItemState != FindItemState.OtherItem) { var decontain = ObjectiveManager.GetActiveObjectives().LastOrDefault(); - if (decontain != null && decontain.TargetItem != null && decontain.TargetItem.HasTag(AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR) && + if (decontain != null && decontain.TargetItem != null && decontain.TargetItem.HasTag(Tags.HeavyDivingGear) && ObjectiveManager.GetActiveObjective() is AIObjectiveGoTo gotoObjective && NeedsDivingGearOnPath(gotoObjective)) { // Don't try to put the diving suit in a locker if the suit would be needed in any hull in the path to the locker. @@ -680,8 +689,8 @@ namespace Barotrauma } if (removeDivingSuit) { - var divingSuit = Character.Inventory.FindItemByTag(AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR); - if (divingSuit != null && !divingSuit.HasTag(AIObjectiveFindDivingGear.DIVING_GEAR_WEARABLE_INDOORS)) + var divingSuit = Character.Inventory.FindItemByTag(Tags.HeavyDivingGear); + if (divingSuit != null && !divingSuit.HasTag(Tags.DivingGearWearableIndoors)) { if (shouldActOnSuffocation || Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) { @@ -723,9 +732,9 @@ namespace Barotrauma } if (takeMaskOff) { - if (Character.HasEquippedItem(AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR)) + if (Character.HasEquippedItem(Tags.LightDivingGear)) { - var mask = Character.Inventory.FindItemByTag(AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR); + var mask = Character.Inventory.FindItemByTag(Tags.LightDivingGear); if (mask != null) { if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List() { InvSlotType.Any })) @@ -928,7 +937,7 @@ namespace Barotrauma if (isPreferencesDefined) { // Use any valid locker as a fall back container. - return container.Item.HasTag("locker") ? 0.5f : 0; + return container.Item.HasTag(Tags.FallbackLocker) ? 0.5f : 0; } return 1; } @@ -1069,7 +1078,7 @@ namespace Barotrauma foreach (Character target in Character.CharacterList) { if (target.CurrentHull != hull) { continue; } - if (AIObjectiveRescueAll.IsValidTarget(target, Character)) + if (AIObjectiveRescueAll.IsValidTarget(target, Character, out _)) { if (AddTargets(Character, target) && newOrder == null && (!Character.IsMedic || Character == target) && !ObjectiveManager.HasActiveObjective()) { @@ -1287,6 +1296,7 @@ namespace Barotrauma minorDamageThreshold = 10; majorDamageThreshold = 40; } + bool eitherIsMentallyUnstable = IsMentallyUnstable || attacker.AIController is { IsMentallyUnstable: true }; if (IsFriendly(attacker)) { if (attacker.AnimController.Anim == Barotrauma.AnimController.Animation.CPR && attacker.SelectedCharacter == Character) @@ -1296,7 +1306,7 @@ namespace Barotrauma return; } float cumulativeDamage = realDamage + Character.GetDamageDoneByAttacker(attacker); - bool isAccidental = attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && attacker.CombatAction == null; + bool isAccidental = attacker.IsBot && !eitherIsMentallyUnstable && attacker.CombatAction == null; if (isAccidental) { if (attacker.TeamID != Character.TeamID || (!Character.IsSecurity && cumulativeDamage > minorDamageThreshold)) @@ -1306,7 +1316,7 @@ namespace Barotrauma } else { - isAttackerInfected = attacker.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.AlienInfectedType) > 0; + isAttackerInfected = attacker.CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.AlienInfectedType) > 0; // Inform other NPCs if (isAttackerInfected || cumulativeDamage > minorDamageThreshold || totalDamage > minorDamageThreshold) { @@ -1378,6 +1388,11 @@ namespace Barotrauma if (otherCharacter.IsPlayer) { continue; } if (otherCharacter.AIController is not HumanAIController otherHumanAI) { continue; } if (!otherHumanAI.IsFriendly(Character)) { continue; } + if (attacker.AIController is EnemyAIController enemyAI && otherHumanAI.IsFriendly(attacker)) + { + // Don't react to friendly enemy AI attacking other characters. E.g. husks attacking someone when whe are a cultist. + continue; + } bool isWitnessing = otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) || otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull); if (!isWitnessing) { @@ -1411,6 +1426,10 @@ namespace Barotrauma humanAI.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.GetTarget() is Controller ? AIObjectiveCombat.CombatMode.None : AIObjectiveCombat.CombatMode.Retreat; } + if (c.IsKiller) + { + return AIObjectiveCombat.CombatMode.Offensive; + } return humanAI.ObjectiveManager.IsCurrentOrder() || humanAI.ObjectiveManager.Objectives.Any(o => o is AIObjectiveFightIntruders) ? @@ -1442,6 +1461,10 @@ namespace Barotrauma isAttackerFightingEnemy = true; return AIObjectiveCombat.CombatMode.None; } + if (c.IsKiller) + { + return AIObjectiveCombat.CombatMode.Offensive; + } if (isWitnessing && c.CombatAction != null && !c.IsSecurity) { return c.CombatAction.WitnessReaction; @@ -1452,7 +1475,7 @@ namespace Barotrauma isAttackerFightingEnemy = true; return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : (instigator.CombatAction != null ? instigator.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat); } - if (attacker.TeamID == CharacterTeamType.FriendlyNPC && !(attacker.AIController.IsMentallyUnstable || attacker.AIController.IsMentallyUnstable)) + if (attacker.TeamID == CharacterTeamType.FriendlyNPC && !eitherIsMentallyUnstable) { if (c.IsSecurity) { @@ -1653,7 +1676,7 @@ namespace Barotrauma needsSuit = (hull == null || hull.LethalPressure > 0) && !Character.IsImmuneToPressure; return needsAir || needsSuit; } - if (hull.WaterPercentage > 60 || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1) + if (hull.WaterPercentage > 60 || (hull.IsWetRoom && hull.WaterPercentage > 10) || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1) { return needsAir; } @@ -1666,14 +1689,14 @@ namespace Barotrauma /// Check whether the character has a diving suit in usable condition plus some oxygen. /// public static bool HasDivingSuit(Character character, float conditionPercentage = 0, bool requireOxygenTank = true) - => HasItem(character, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out _, requireOxygenTank ? AIObjectiveFindDivingGear.OXYGEN_SOURCE : Identifier.Empty, conditionPercentage, requireEquipped: true, + => HasItem(character, Tags.HeavyDivingGear, out _, requireOxygenTank ? Tags.OxygenSource : Identifier.Empty, conditionPercentage, requireEquipped: true, predicate: (Item item) => character.HasEquippedItem(item, InvSlotType.OuterClothes | InvSlotType.InnerClothes)); /// /// Check whether the character has a diving mask in usable condition plus some oxygen. /// public static bool HasDivingMask(Character character, float conditionPercentage = 0, bool requireOxygenTank = true) - => HasItem(character, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out _, requireOxygenTank ? AIObjectiveFindDivingGear.OXYGEN_SOURCE : Identifier.Empty, conditionPercentage, requireEquipped: true); + => HasItem(character, Tags.LightDivingGear, out _, requireOxygenTank ? Tags.OxygenSource : Identifier.Empty, conditionPercentage, requireEquipped: true); private static List matchingItems = new List(); @@ -1739,7 +1762,7 @@ namespace Barotrauma { continue; } - if (!otherCharacter.CanSeeCharacter(character)) { continue; } + if (!otherCharacter.CanSeeTarget(character)) { continue; } if (!otherHumanAI.structureDamageAccumulator.ContainsKey(character)) { otherHumanAI.structureDamageAccumulator.Add(character, 0.0f); } float prevAccumulatedDamage = otherHumanAI.structureDamageAccumulator[character]; @@ -1816,7 +1839,7 @@ namespace Barotrauma bool someoneSpoke = false; bool stolenItemsInside = item.OwnInventory?.FindAllItems(it => it.SpawnedInCurrentOutpost && !it.AllowStealing, recursive: true).Any() ?? false; - if ((item.SpawnedInCurrentOutpost && !item.AllowStealing || stolenItemsInside) && thief.TeamID != CharacterTeamType.FriendlyNPC && !item.HasTag("handlocker")) + if ((item.SpawnedInCurrentOutpost && !item.AllowStealing || stolenItemsInside) && thief.TeamID != CharacterTeamType.FriendlyNPC && !item.HasTag(Tags.HandLockerItem)) { foreach (Character otherCharacter in Character.CharacterList) { @@ -1827,14 +1850,14 @@ namespace Barotrauma continue; } //if (!otherCharacter.IsFacing(thief.WorldPosition)) { continue; } - if (!otherCharacter.CanSeeCharacter(thief)) { continue; } + if (!otherCharacter.CanSeeTarget(thief)) { continue; } // Don't react if the player is taking an extinguisher and there's any fires on the sub, or diving gear when the sub is flooding // -> allow them to use the emergency items if (thief.Submarine != null) { var connectedHulls = thief.Submarine.GetHulls(alsoFromConnectedSubs: true); - if (item.HasTag("fireextinguisher") && connectedHulls.Any(h => h.FireSources.Any())) { continue; } - if (item.HasTag("diving") && connectedHulls.Any(h => h.ConnectedGaps.Any(g => AIObjectiveFixLeaks.IsValidTarget(g, thief)))) { continue; } + if (item.HasTag(Tags.FireExtinguisher) && connectedHulls.Any(h => h.FireSources.Any())) { continue; } + if (item.HasTag(Tags.DivingGear) && connectedHulls.Any(h => h.ConnectedGaps.Any(g => AIObjectiveFixLeaks.IsValidTarget(g, thief)))) { continue; } } if (!someoneSpoke) { @@ -1966,7 +1989,7 @@ namespace Barotrauma foreach (var c in Character.CharacterList) { if (c.CurrentHull != hull) { continue; } - if (AIObjectiveRescueAll.IsValidTarget(c, character)) + if (AIObjectiveRescueAll.IsValidTarget(c, character, out _)) { AddTargets(character, c); } @@ -2171,7 +2194,10 @@ namespace Barotrauma { bool sameTeam = me.TeamID == other.TeamID; bool teamGood = sameTeam || !onlySameTeam && me.IsOnFriendlyTeam(other); - if (!teamGood) { return false; } + if (!teamGood) + { + return other.IsHusk && me.IsDisguisedAsHusk; + } if (other.IsPet) { // Hostile NPCs are hostile to all pets, unless they are in the same team. diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index 5f26687da..db44b0290 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -241,7 +241,6 @@ namespace Barotrauma float priority = MathHelper.Lerp(3, 1, character.Params.PathFinderPriority); findPathTimer = priority * Rand.Range(1.0f, 1.2f); IsPathDirty = false; - return DiffToCurrentNode(); void SkipCurrentPathNodes() { @@ -284,15 +283,6 @@ namespace Barotrauma } Vector2 diff = DiffToCurrentNode(); - var collider = character.AnimController.Collider; - // Only humanoids can climb ladders - bool canClimb = character.AnimController is HumanoidAnimController; - //if not in water and the waypoint is between the top and bottom of the collider, no need to move vertically - if (canClimb && !character.AnimController.InWater && !character.IsClimbing && diff.Y < collider.Height / 2 + collider.Radius) - { - // TODO: might cause some edge cases -> do we need this? - diff.Y = 0.0f; - } if (diff == Vector2.Zero) { return Vector2.Zero; } return Vector2.Normalize(diff) * weight; } @@ -401,16 +391,15 @@ namespace Barotrauma else { bool nextLadderSameAsCurrent = currentLadder == nextLadder; + float colliderHeight = collider.Height / 2 + collider.Radius; + float heightDiff = currentPath.CurrentNode.SimPosition.Y - collider.SimPosition.Y; + float distanceMargin = ConvertUnits.ToDisplayUnits(colliderSize.X); if (currentLadder != null && nextLadder != null) { //climbing ladders -> don't move horizontally diff.X = 0.0f; } - //at the same height as the waypoint - float heightDiff = Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y); - float colliderHeight = collider.Height / 2 + collider.Radius; - float distanceMargin = ConvertUnits.ToDisplayUnits(colliderSize.X); - if (heightDiff < colliderHeight * 1.25f) + if (Math.Abs(heightDiff) < colliderHeight * 1.25f) { if (nextLadder != null && !nextLadderSameAsCurrent) { @@ -463,10 +452,10 @@ namespace Barotrauma } } } - return ConvertUnits.ToSimUnits(diff); } else if (character.AnimController.InWater) { + // Swimming var door = currentPath.CurrentNode.ConnectedDoor; if (door == null || door.CanBeTraversed) { @@ -502,6 +491,13 @@ namespace Barotrauma bool isTargetTooLow = currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y; var door = currentPath.CurrentNode.ConnectedDoor; float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 5, 0, 1)); + float colliderHeight = collider.Height / 2 + collider.Radius; + float heightDiff = currentPath.CurrentNode.SimPosition.Y - collider.SimPosition.Y; + if (heightDiff < colliderHeight) + { + //the waypoint is between the top and bottom of the collider, no need to move vertically. + diff.Y = 0.0f; + } if (currentPath.CurrentNode.Stairs != null) { bool isNextNodeInSameStairs = currentPath.NextNode?.Stairs == currentPath.CurrentNode.Stairs; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs index 2fb0e0027..46e1e6aff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs @@ -328,7 +328,7 @@ namespace Barotrauma if (checkedSpeakers.Any(s => !potentialSpeaker.CanHearCharacter(s))) { return false; } //check if the character is close enough to see the rest of the speakers (this should be replaced with a more performant method) - if (checkedSpeakers.Any(s => !potentialSpeaker.CanSeeCharacter(s))) { return false; } + if (checkedSpeakers.Any(s => !potentialSpeaker.CanSeeTarget(s))) { return false; } //check if the character has an appropriate personality if (selectedConversation.allowedSpeakerTags.Count > 0) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index dd3b79d0c..ee3bd03c5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -40,6 +40,7 @@ namespace Barotrauma public virtual bool AllowOutsideSubmarine => false; public virtual bool AllowInFriendlySubs => false; public virtual bool AllowInAnySub => false; + public virtual bool AllowWhileHandcuffed => true; protected readonly List subObjectives = new List(); private float _cumulatedDevotion; @@ -246,32 +247,43 @@ namespace Barotrauma { get { - if (IgnoreAtOutpost && Level.IsLoadedFriendlyOutpost && character.TeamID != CharacterTeamType.FriendlyNPC) - { - if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID)) - { - return false; - } - } + if (!AllowWhileHandcuffed && character.LockHands) { return false; } if (!AllowOutsideSubmarine && character.Submarine == null) { return false; } + // Evaluate ignored at outpost first, because it has higher priority than AllowInAnySub or AllowInFriendlySubs. + if (IsIgnoredAtOutpost()) { return false; } if (AllowInAnySub) { return true; } if ((AllowInFriendlySubs && character.Submarine.TeamID == CharacterTeamType.FriendlyNPC) || character.IsEscorted) { return true; } - return character.Submarine.TeamID == character.TeamID || - character.Submarine.TeamID == character.OriginalTeamID || - character.Submarine.DockedTo.Any(sub => sub.TeamID == character.TeamID || sub.TeamID == character.OriginalTeamID); + return character.Submarine.TeamID == character.TeamID || character.Submarine.TeamID == character.OriginalTeamID; } } + /// + /// Returns true only when at a friendly outpost and when the order is set to be ignored there. + /// Note that even if this returns false, the objective can be disallowed, because AllowInFriendlySubs is false. + /// + public bool IsIgnoredAtOutpost() + { + if (!IgnoreAtOutpost) { return false; } + if (!Level.IsLoadedFriendlyOutpost) { return false; } + if (!character.IsOnPlayerTeam) { return false; } + if (character.Submarine?.Info == null) { return false; } + return character.Submarine.Info.IsOutpost && character.Submarine.TeamID == CharacterTeamType.FriendlyNPC; + } + + protected void HandleNonAllowed() + { + Priority = 0; + Abandon = !IsIgnoredAtOutpost(); + } + protected virtual float GetPriority() { - bool isOrder = objectiveManager.IsOrder(this); if (!IsAllowed) { - Priority = 0; - Abandon = true; + HandleNonAllowed(); return Priority; } - if (isOrder) + if (objectiveManager.IsOrder(this)) { Priority = objectiveManager.GetOrderPriority(this); } @@ -446,7 +458,7 @@ namespace Barotrauma } } - protected virtual bool Check() + private bool Check() { if (AbortCondition != null && AbortCondition(this)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs index e3e8af4cd..e43bb2622 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs @@ -19,6 +19,7 @@ namespace Barotrauma protected override bool Filter(PowerContainer battery) { if (battery == null) { return false; } + if (battery.OutputDisabled) { return false; } var item = battery.Item; if (item.IgnoreByAI(character)) { return false; } if (!item.IsInteractable(character)) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs index d9c0c85ec..45d99f33a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs @@ -12,6 +12,7 @@ namespace Barotrauma public override Identifier Identifier { get; set; } = "cleanup item".ToIdentifier(); public override bool KeepDivingGearOn => true; public override bool AllowAutomaticItemUnequipping => false; + public override bool AllowWhileHandcuffed => false; public readonly Item item; public bool IsPriority { get; set; } @@ -30,8 +31,7 @@ namespace Barotrauma { if (!IsAllowed) { - Priority = 0; - Abandon = true; + HandleNonAllowed(); return Priority; } else @@ -119,18 +119,21 @@ namespace Barotrauma if (item.IgnoreByAI(character)) { Abandon = true; - return false; } - if (item.ParentInventory != null) + else if (item.ParentInventory != null) { - if (item.Container != null && !AIObjectiveCleanupItems.IsValidContainer(item.Container, character, allowUnloading: objectiveManager.HasOrder())) + if (!objectiveManager.HasOrder()) + { + // Don't allow taking items from containers in the idle state. + Abandon = true; + } + else if (item.Container != null && !AIObjectiveCleanupItems.IsValidContainer(item.Container, character)) { // Target was picked up or moved by someone. Abandon = true; - return false; } } - return IsCompleted; + return !Abandon && IsCompleted; } public override void Reset() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs index 7d82e8377..72277a406 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs @@ -14,8 +14,6 @@ namespace Barotrauma public readonly List prioritizedItems = new List(); - public static readonly Identifier AllowCleanupTag = "allowcleanup".ToIdentifier(); - protected override int MaxTargets => 100; public AIObjectiveCleanupItems(Character character, AIObjectiveManager objectiveManager, Item prioritizedItem = null, float priorityModifier = 1) @@ -83,9 +81,8 @@ namespace Barotrauma return true; } - public static bool IsValidContainer(Item container, Character character, bool allowUnloading = true) => - allowUnloading && - container.HasTag(AllowCleanupTag) && + public static bool IsValidContainer(Item container, Character character) => + container.HasTag(Tags.AllowCleanup) && container.HasAccess(character) && container.ParentInventory == null && container.OwnInventory != null && container.OwnInventory.AllItems.Any() && container.GetComponent() != null && @@ -103,15 +100,18 @@ namespace Barotrauma // In a character inventory return false; } - if (!IsValidContainer(item.Container, character, allowUnloading)) { return false; } + if (!allowUnloading) { return false; } + if (!IsValidContainer(item.Container, character)) { return false; } } if (!item.HasAccess(character)) { return false; } if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; } if (item.HasBallastFloraInHull) { return false; } + //something (e.g. a pet) was eating the item within the last second - don't clean up + if (item.LastEatenTime > Timing.TotalTimeUnpaused - 1.0) { return false; } var wire = item.GetComponent(); if (wire != null) { - if (wire.Connections.Any()) { return false; } + if (wire.Connections.Any(c => c != null)) { return false; } } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index a1b25992a..612eaa7fe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -46,7 +46,6 @@ namespace Barotrauma _weapon = value; _weaponComponent = null; hasAimed = false; - RemoveSubObjective(ref seekAmmunitionObjective); } } private ItemComponent _weaponComponent; @@ -55,14 +54,7 @@ namespace Barotrauma get { if (Weapon == null) { return null; } - if (_weaponComponent == null) - { - _weaponComponent = - Weapon.GetComponent() ?? - Weapon.GetComponent() ?? - Weapon.GetComponent() as ItemComponent; - } - return _weaponComponent; + return _weaponComponent ?? GetWeaponComponent(Weapon); } } @@ -280,13 +272,13 @@ namespace Barotrauma } break; case CombatMode.Arrest: - if (HumanAIController.HasItem(Enemy, "handlocker".ToIdentifier(), out _, requireEquipped: true)) + if (HumanAIController.HasItem(Enemy, Tags.HandLockerItem, out _, requireEquipped: true)) { IsCompleted = true; } else if (Enemy.IsKnockedDown && !objectiveManager.IsCurrentObjective() && - !HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out _, requireEquipped: false)) + !HumanAIController.HasItem(character, Tags.HandLockerItem, out _, requireEquipped: false)) { IsCompleted = true; } @@ -348,8 +340,10 @@ namespace Barotrauma if (character.LockHands || Enemy == null) { Weapon = null; + RemoveSubObjective(ref seekAmmunitionObjective); return false; } + bool isAllowedToSeekWeapons = character.CurrentHull != null && !IsEnemyCloserThan(300) && character.IsOnPlayerTeam && IsOffensiveOrArrest; if (checkWeaponsTimer < 0) { checkWeaponsTimer = checkWeaponsInterval; @@ -375,7 +369,7 @@ namespace Barotrauma // All good, the weapon is loaded break; } - if (Reload(seekAmmo: false)) + if (Reload(seekAmmo: isAllowedToSeekWeapons)) { // All good, we can use the weapon. break; @@ -407,7 +401,6 @@ namespace Barotrauma } } } - bool isAllowedToSeekWeapons = character.CurrentHull != null && !IsEnemyCloserThan(300) && character.IsOnPlayerTeam && IsOffensiveOrArrest; if (!isAllowedToSeekWeapons) { if (WeaponComponent == null) @@ -416,7 +409,7 @@ namespace Barotrauma Mode = CombatMode.Retreat; } } - else if (seekAmmunitionObjective == null && (WeaponComponent == null || WeaponComponent.CombatPriority < goodWeaponPriority)) + else if (seekAmmunitionObjective == null && (WeaponComponent == null || (WeaponComponent.CombatPriority < goodWeaponPriority))) { // Poor weapon equipped -> try to find better. RemoveSubObjective(ref seekAmmunitionObjective); @@ -431,27 +424,10 @@ namespace Barotrauma { if (Weapon != null && (i == Weapon || i.Prefab.Identifier == Weapon.Prefab.Identifier)) { return 0; } if (i.IsOwnedBy(character)) { return 0; } - var mw = i.GetComponent(); - var rw = i.GetComponent(); float priority = 0; - if (mw != null) + if (GetWeaponComponent(i) is ItemComponent ic) { - priority = mw.CombatPriority / 100; - } - else if (rw != null) - { - priority = rw.CombatPriority / 100; - } - if (i.HasTag("stunner")) - { - if (Mode == CombatMode.Arrest) - { - priority *= 2; - } - else - { - priority /= 2; - } + priority = GetWeaponPriority(ic, prioritizeMelee: false, isCloseToEnemy: false, out _) / 100; } return priority; } @@ -477,6 +453,7 @@ namespace Barotrauma if (!CheckWeapon(seekAmmo: false)) { Weapon = null; + RemoveSubObjective(ref seekAmmunitionObjective); } } return Weapon != null; @@ -521,111 +498,164 @@ namespace Barotrauma private Item FindWeapon(out ItemComponent weaponComponent) => GetWeapon(FindWeaponsFromInventory(), out weaponComponent); + private static ItemComponent GetWeaponComponent(Item item) => + item.GetComponent() ?? + item.GetComponent() ?? + item.GetComponent() ?? + item.GetComponent() as ItemComponent; + + private float GetWeaponPriority(ItemComponent weapon, bool prioritizeMelee, bool isCloseToEnemy, out float lethalDmg) + { + lethalDmg = -1; + float priority = weapon.CombatPriority; + if (weapon is RepairTool repairTool) + { + switch (repairTool.UsableIn) + { + case RepairTool.UseEnvironment.Air: + if (character.InWater) { return 0; } + break; + case RepairTool.UseEnvironment.Water: + if (!character.InWater) { return 0; } + break; + case RepairTool.UseEnvironment.None: + return 0; + case RepairTool.UseEnvironment.Both: + default: + break; + } + } + if (prioritizeMelee && weapon is MeleeWeapon) + { + priority *= 5; + } + if (weapon.IsEmpty(character)) + { + if (weapon is RangedWeapon && isCloseToEnemy) + { + // Ignore weapons that don't have any ammunition (-> Don't seek ammo). + return 0; + } + else + { + // Reduce the priority for weapons that don't have proper ammunition loaded. + if (character.HasEquippedItem(Weapon, predicate: CharacterInventory.IsHandSlotType)) + { + // Yet prefer the equipped weapon. + priority *= 0.75f; + } + else + { + priority *= 0.5f; + } + } + } + if (Enemy.Params.Health.StunImmunity) + { + if (weapon.Item.HasTag(Tags.StunnerItem)) + { + priority /= 2; + } + } + else if (Enemy.IsKnockedDown) + { + // Enemy is stunned, reduce the priority of stunner weapons. + Attack attack = GetAttackDefinition(weapon); + if (attack != null) + { + lethalDmg = attack.GetTotalDamage(); + float max = lethalDmg + 1; + if (weapon.Item.HasTag(Tags.StunnerItem)) + { + priority = max; + } + else + { + float stunDmg = ApproximateStunDamage(weapon, attack); + float diff = stunDmg - lethalDmg; + priority = Math.Clamp(priority - Math.Max(diff * 2, 0), min: 1, max); + } + } + } + else if (Mode == CombatMode.Arrest) + { + // Enemy is not stunned, increase the priority of stunner weapons and decrease the priority of lethal weapons. + if (weapon.Item.HasTag(Tags.StunnerItem)) + { + priority *= 5; + } + else + { + Attack attack = GetAttackDefinition(weapon); + if (attack != null) + { + lethalDmg = attack.GetTotalDamage(); + float stunDmg = ApproximateStunDamage(weapon, attack); + float diff = stunDmg - lethalDmg; + if (diff < 0) + { + priority /= 2; + } + } + } + } + else if (weapon is MeleeWeapon && weapon.Item.HasTag(Tags.StunnerItem) && (Enemy.Params.Health.StunImmunity || !CanMeleeStunnerStun(weapon))) + { + // Cannot do stun damage -> use the melee damage to determine the priority. + Attack attack = GetAttackDefinition(weapon); + priority = attack?.GetTotalDamage() ?? priority / 2; + } + return priority; + } + + private float ApproximateStunDamage(ItemComponent weapon, Attack attack) + { + // Try to reduce the priority using the actual damage values and status effects. + // This is an approximation, because we can't check the status effect conditions here. + // The result might be incorrect if there is a high stun effect that's only applied in certain conditions. + var statusEffects = attack.StatusEffects.Where(se => !se.HasConditions && se.type == ActionType.OnUse && se.HasRequiredItems(character)); + if (weapon.statusEffectLists != null && weapon.statusEffectLists.TryGetValue(ActionType.OnUse, out List hitEffects)) + { + statusEffects = statusEffects.Concat(hitEffects); + } + float afflictionsStun = attack.Afflictions.Keys.Sum(a => a.Identifier == AfflictionPrefab.StunType ? a.Strength : 0); + float effectsStun = statusEffects.None() ? 0 : statusEffects.Max(se => + { + float stunAmount = 0; + var stunAffliction = se.Afflictions.Find(a => a.Identifier == AfflictionPrefab.StunType); + if (stunAffliction != null) + { + stunAmount = stunAffliction.Strength; + } + return stunAmount; + }); + return attack.Stun + afflictionsStun + effectsStun; + } + + private bool CanMeleeStunnerStun(ItemComponent weapon) + { + // If there's an item container that takes a battery, + // assume that it's required for the stun effect + // as we can't check the status effect conditions here. + var mobileBatteryTag = Tags.MobileBattery; + var containers = weapon.Item.Components.Where(ic => + ic is ItemContainer container && + container.ContainableItemIdentifiers.Contains(mobileBatteryTag)); + // If there's no such container, assume that the melee weapon can stun without a battery. + return containers.None() || containers.Any(container => + (container as ItemContainer)?.Inventory.AllItems.Any(i => i != null && i.HasTag(mobileBatteryTag) && i.Condition > 0.0f) ?? false); + } + private Item GetWeapon(IEnumerable weaponList, out ItemComponent weaponComponent) { weaponComponent = null; float bestPriority = 0; float lethalDmg = -1; - bool isAllowedToSeekWeapons = !IsEnemyCloserThan(300); + bool isCloseToEnemy = IsEnemyCloserThan(300); bool prioritizeMelee = IsEnemyCloserThan(50) || EnemyAIController.IsLatchedTo(Enemy, character); foreach (var weapon in weaponList) { - float priority = weapon.CombatPriority; - if (weapon is RepairTool repairTool) - { - switch (repairTool.UsableIn) - { - case RepairTool.UseEnvironment.Air: - if (character.InWater) { continue; } - break; - case RepairTool.UseEnvironment.Water: - if (!character.InWater) { continue; } - break; - case RepairTool.UseEnvironment.None: - continue; - case RepairTool.UseEnvironment.Both: - default: - break; - } - } - if (prioritizeMelee) - { - if (weapon is MeleeWeapon) - { - priority *= 5; - } - else - { - priority /= 2; - } - } - if (weapon.IsEmpty(character)) - { - if (weapon is RangedWeapon && !isAllowedToSeekWeapons) - { - // Close to the enemy. Ignore weapons that don't have any ammunition (-> Don't seek ammo). - continue; - } - else - { - // Halve the priority for weapons that don't have proper ammunition loaded. - priority /= 2; - } - } - if (Enemy.Params.Health.StunImmunity) - { - if (weapon.Item.HasTag("stunner")) - { - priority /= 2; - } - } - else if (Enemy.IsKnockedDown) - { - // Enemy is stunned, reduce the priority of stunner weapons. - Attack attack = GetAttackDefinition(weapon); - if (attack != null) - { - lethalDmg = attack.GetTotalDamage(); - float max = lethalDmg + 1; - if (weapon.Item.HasTag("stunner")) - { - priority = max; - } - else - { - float stunDmg = ApproximateStunDamage(weapon, attack); - float diff = stunDmg - lethalDmg; - priority = Math.Clamp(priority - Math.Max(diff * 2, 0), min: 1, max); - } - } - } - else if (Mode == CombatMode.Arrest) - { - // Enemy is not stunned, increase the priority of stunner weapons and decrease the priority of lethal weapons. - if (weapon.Item.HasTag("stunner")) - { - priority *= 2; - } - else - { - Attack attack = GetAttackDefinition(weapon); - if (attack != null) - { - lethalDmg = attack.GetTotalDamage(); - float stunDmg = ApproximateStunDamage(weapon, attack); - float diff = stunDmg - lethalDmg; - if (diff < 0) - { - priority /= 2; - } - } - } - } - else if (weapon is MeleeWeapon && weapon.Item.HasTag("stunner") && !CanMeleeStunnerStun(weapon)) - { - Attack attack = GetAttackDefinition(weapon); - priority = attack?.GetTotalDamage() ?? priority / 2; - } + float priority = GetWeaponPriority(weapon, prioritizeMelee, isCloseToEnemy, out lethalDmg); if (priority > bestPriority) { weaponComponent = weapon; @@ -636,7 +666,7 @@ namespace Barotrauma if (bestPriority < 1) { return null; } if (Mode == CombatMode.Arrest) { - if (weaponComponent.Item.HasTag("stunner")) + if (weaponComponent.Item.HasTag(Tags.StunnerItem)) { isLethalWeapon = false; } @@ -654,44 +684,6 @@ namespace Barotrauma } } return weaponComponent.Item; - - float ApproximateStunDamage(ItemComponent weapon, Attack attack) - { - // Try to reduce the priority using the actual damage values and status effects. - // This is an approximation, because we can't check the status effect conditions here. - // The result might be incorrect if there is a high stun effect that's only applied in certain conditions. - var statusEffects = attack.StatusEffects.Where(se => !se.HasConditions && se.type == ActionType.OnUse && se.HasRequiredItems(character)); - if (weapon.statusEffectLists != null && weapon.statusEffectLists.TryGetValue(ActionType.OnUse, out List hitEffects)) - { - statusEffects = statusEffects.Concat(hitEffects); - } - float afflictionsStun = attack.Afflictions.Keys.Sum(a => a.Identifier == AfflictionPrefab.StunType ? a.Strength : 0); - float effectsStun = statusEffects.None() ? 0 : statusEffects.Max(se => - { - float stunAmount = 0; - var stunAffliction = se.Afflictions.Find(a => a.Identifier == AfflictionPrefab.StunType); - if (stunAffliction != null) - { - stunAmount = stunAffliction.Strength; - } - return stunAmount; - }); - return attack.Stun + afflictionsStun + effectsStun; - } - - bool CanMeleeStunnerStun(ItemComponent weapon) - { - // If there's an item container that takes a battery, - // assume that it's required for the stun effect - // as we can't check the status effect conditions here. - var mobileBatteryTag = "mobilebattery".ToIdentifier(); - var containers = weapon.Item.Components.Where(ic => - ic is ItemContainer container && - container.ContainableItemIdentifiers.Contains(mobileBatteryTag)); - // If there's no such container, assume that the melee weapon can stun without a battery. - return containers.None() || containers.Any(container => - (container as ItemContainer)?.Inventory.AllItems.Any(i => i != null && i.HasTag(mobileBatteryTag) && i.Condition > 0.0f) ?? false); - } } public static float GetLethalDamage(ItemComponent weapon) @@ -771,13 +763,13 @@ namespace Barotrauma { return false; } - if (!character.HasEquippedItem(Weapon, predicate: IsHandSlotType)) + if (!character.HasEquippedItem(Weapon, predicate: CharacterInventory.IsHandSlotType)) { //clear aim and shoot inputs so the bot doesn't immediately fire the weapon if it was previously e.g. using a scooter character.ClearInput(InputType.Aim); character.ClearInput(InputType.Shoot); Weapon.TryInteract(character, forceSelectKey: true); - var slots = Weapon.AllowedSlots.Where(s => IsHandSlotType(s)); + var slots = Weapon.AllowedSlots.Where(s => CharacterInventory.IsHandSlotType(s)); if (character.Inventory.TryPutItem(Weapon, character, slots)) { SetAimTimer(Rand.Range(0.2f, 0.4f) / AimSpeed); @@ -791,8 +783,6 @@ namespace Barotrauma } } return true; - - static bool IsHandSlotType(InvSlotType s) => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand); } private float findHullTimer; @@ -926,7 +916,7 @@ namespace Barotrauma if (followTargetObjective == null) { return; } if (Mode == CombatMode.Arrest && Enemy.IsKnockedDown) { - if (HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out _)) + if (HumanAIController.HasItem(character, Tags.HandLockerItem, out _)) { if (!arrestingRegistered) { @@ -986,7 +976,7 @@ namespace Barotrauma foreach (var item in Enemy.Inventory.AllItemsMod) { if (character.TeamID == CharacterTeamType.FriendlyNPC && item.StolenDuringRound || - item.HasTag("weapon") || + item.HasTag(Tags.Weapon) || item.GetComponent() != null || item.GetComponent() != null) { @@ -997,9 +987,9 @@ namespace Barotrauma } //prefer using handcuffs already on the enemy's inventory - if (!HumanAIController.HasItem(Enemy, "handlocker".ToIdentifier(), out IEnumerable matchingItems)) + if (!HumanAIController.HasItem(Enemy, Tags.HandLockerItem, out IEnumerable matchingItems)) { - HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out matchingItems); + HumanAIController.HasItem(character, Tags.HandLockerItem, out matchingItems); } if (matchingItems.Any() && @@ -1079,7 +1069,7 @@ namespace Barotrauma if (ammunitionIdentifiers != null) { // Try reload ammunition from inventory - static bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag("mobileradio"); + static bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag(Tags.MobileRadio); Item ammunition = character.Inventory.FindItem(i => i.HasIdentifierOrTags(ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i), recursive: true); if (ammunition != null) { @@ -1205,7 +1195,7 @@ namespace Barotrauma myBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody); } // Check that we don't hit friendlies. No need to check the walls, because there's a separate check for that at 1096 (which intentionally has a small delay) - var pickedBodies = Submarine.PickBodies(Weapon.SimPosition, Character.GetRelativeSimPosition(from: Weapon, to: Enemy), myBodies, Physics.CollisionCharacter); + var pickedBodies = Submarine.PickBodies(Weapon.SimPosition, Submarine.GetRelativeSimPosition(from: Weapon, to: Enemy), myBodies, Physics.CollisionCharacter); foreach (var body in pickedBodies) { Character target = null; @@ -1248,7 +1238,7 @@ namespace Barotrauma } } character.SetInput(InputType.Shoot, false, true); - Weapon.Use(deltaTime, character); + Weapon.Use(deltaTime, user: character); reloadTimer = Math.Max(reloadTime, reloadTime * Rand.Range(1f, 1.25f) / AimSpeed); } @@ -1265,7 +1255,7 @@ namespace Barotrauma { Unequip(); } - SteeringManager.Reset(); + SteeringManager?.Reset(); } protected override void OnAbandon() @@ -1275,7 +1265,7 @@ namespace Barotrauma { Unequip(); } - SteeringManager.Reset(); + SteeringManager?.Reset(); } public override void Reset() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index 244b66a89..53db54d77 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -11,6 +11,7 @@ namespace Barotrauma class AIObjectiveContainItem: AIObjective { public override Identifier Identifier { get; set; } = "contain item".ToIdentifier(); + public override bool AllowWhileHandcuffed => false; public Func GetItemPriority; @@ -109,7 +110,7 @@ namespace Barotrauma private bool CheckItem(Item item) { - return item.HasIdentifierOrTags(itemIdentifiers) && item.ConditionPercentage >= ConditionLevel && item.HasAccess(character); + return item.HasIdentifierOrTags(itemIdentifiers) && item.ConditionPercentage >= ConditionLevel && item.HasAccess(character) && container.ShouldBeContained(item, out _); } protected override void Act(float deltaTime) @@ -226,7 +227,10 @@ namespace Barotrauma AllowToFindDivingGear = AllowToFindDivingGear, AllowDangerousPressure = AllowDangerousPressure, TargetCondition = ConditionLevel, - ItemFilter = (Item potentialItem) => RemoveEmpty ? container.CanBeContained(potentialItem) : container.Inventory.CanBePut(potentialItem), + ItemFilter = (Item potentialItem) => + { + return (RemoveEmpty ? container.CanBeContained(potentialItem) : container.Inventory.CanBePut(potentialItem)) && container.ShouldBeContained(potentialItem, out _); + }, ItemCount = ItemCount, TakeWholeStack = MoveWholeStack }, onAbandon: () => diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs index a41303c20..d8cfdf3fb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs @@ -9,11 +9,12 @@ namespace Barotrauma class AIObjectiveDecontainItem : AIObjective { public override Identifier Identifier { get; set; } = "decontain item".ToIdentifier(); + public override bool AllowWhileHandcuffed => false; public Func GetItemPriority; //can either be a tag or an identifier - private readonly string[] itemIdentifiers; + private readonly Identifier[] itemIdentifiers; private readonly ItemContainer sourceContainer; private readonly ItemContainer targetContainer; private readonly Item targetItem; @@ -52,16 +53,16 @@ namespace Barotrauma this.targetContainer = targetContainer; } - public AIObjectiveDecontainItem(Character character, string itemIdentifier, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1) - : this(character, new string[] { itemIdentifier }, objectiveManager, sourceContainer, targetContainer, priorityModifier) { } + public AIObjectiveDecontainItem(Character character, Identifier itemIdentifier, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1) + : this(character, new Identifier[] { itemIdentifier }, objectiveManager, sourceContainer, targetContainer, priorityModifier) { } - public AIObjectiveDecontainItem(Character character, string[] itemIdentifiers, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1) + public AIObjectiveDecontainItem(Character character, Identifier[] itemIdentifiers, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { this.itemIdentifiers = itemIdentifiers; for (int i = 0; i < itemIdentifiers.Length; i++) { - itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant(); + itemIdentifiers[i] = itemIdentifiers[i]; } this.sourceContainer = sourceContainer; this.targetContainer = targetContainer; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveEscapeHandcuffs.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveEscapeHandcuffs.cs index 5a57adc31..ed5408fb7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveEscapeHandcuffs.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveEscapeHandcuffs.cs @@ -88,7 +88,7 @@ namespace Barotrauma escapeProgress += Rand.Range(2, 5); if (escapeProgress > 15) { - Item handcuffs = character.Inventory.FindItemByTag("handlocker".ToIdentifier()); + Item handcuffs = character.Inventory.FindItemByTag(Tags.HandLockerItem); if (handcuffs != null) { handcuffs.Drop(character); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs index cec282b90..7cd20e569 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -15,6 +15,8 @@ namespace Barotrauma public override bool AllowInAnySub => true; + public override bool AllowWhileHandcuffed => false; + private readonly Hull targetHull; private AIObjectiveGetItem getExtinguisherObjective; @@ -30,8 +32,7 @@ namespace Barotrauma { if (!IsAllowed) { - Priority = 0; - Abandon = true; + HandleNonAllowed(); return Priority; } bool isOrder = objectiveManager.HasOrder(); @@ -176,19 +177,19 @@ namespace Barotrauma getExtinguisherObjective = null; gotoObjective = null; sinTime = 0; - SteeringManager.Reset(); + SteeringManager?.Reset(); } protected override void OnCompleted() { base.OnCompleted(); - SteeringManager.Reset(); + SteeringManager?.Reset(); } protected override void OnAbandon() { base.OnAbandon(); - SteeringManager.Reset(); + SteeringManager?.Reset(); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs index 3cb129c96..19fb5d725 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs @@ -12,6 +12,7 @@ namespace Barotrauma public override bool ForceRun => true; public override bool KeepDivingGearOn => true; public override bool AbandonWhenCannotCompleteSubjectives => false; + public override bool AllowWhileHandcuffed => false; private readonly Identifier gearTag; @@ -22,34 +23,20 @@ namespace Barotrauma public const float MIN_OXYGEN = 10; - public static readonly Identifier HEAVY_DIVING_GEAR = "deepdiving".ToIdentifier(); - public static readonly Identifier LIGHT_DIVING_GEAR = "lightdiving".ToIdentifier(); - /// - /// Diving gear that's suitable for wearing indoors (-> the bots don't try to unequip it when they don't need diving gear) - /// - public static readonly Identifier DIVING_GEAR_WEARABLE_INDOORS = "divinggear_wearableindoors".ToIdentifier(); - public static readonly Identifier OXYGEN_SOURCE = "oxygensource".ToIdentifier(); - protected override bool CheckObjectiveSpecific() => targetItem != null && character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head); public AIObjectiveFindDivingGear(Character character, bool needsDivingSuit, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { - gearTag = needsDivingSuit ? HEAVY_DIVING_GEAR : LIGHT_DIVING_GEAR; + gearTag = needsDivingSuit ? Tags.HeavyDivingGear : Tags.LightDivingGear; } protected override void Act(float deltaTime) { - if (character.LockHands) - { - Abandon = true; - return; - } - TrySetTargetItem(character.Inventory.FindItemByTag(gearTag, true)); - if (targetItem == null && gearTag == LIGHT_DIVING_GEAR) + if (targetItem == null && gearTag == Tags.LightDivingGear) { - TrySetTargetItem(character.Inventory.FindItemByTag(HEAVY_DIVING_GEAR, true)); + TrySetTargetItem(character.Inventory.FindItemByTag(Tags.HeavyDivingGear, true)); } if (targetItem == null || !character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head) && @@ -74,7 +61,7 @@ namespace Barotrauma onCompleted: () => { RemoveSubObjective(ref getDivingGear); - if (gearTag == HEAVY_DIVING_GEAR && HumanAIController.HasItem(character, LIGHT_DIVING_GEAR, out IEnumerable masks, requireEquipped: true)) + if (gearTag == Tags.HeavyDivingGear && HumanAIController.HasItem(character, Tags.LightDivingGear, out IEnumerable masks, requireEquipped: true)) { foreach (Item mask in masks) { @@ -95,10 +82,10 @@ namespace Barotrauma { if (character.IsOnPlayerTeam) { - if (HumanAIController.HasItem(character, OXYGEN_SOURCE, out _, conditionPercentage: min)) + if (HumanAIController.HasItem(character, Tags.OxygenSource, out _, conditionPercentage: min)) { character.Speak(TextManager.Get("dialogswappingoxygentank").Value, null, 0, "swappingoxygentank".ToIdentifier(), 30.0f); - if (character.Inventory.FindAllItems(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > min, recursive: true).Count == 1) + if (character.Inventory.FindAllItems(i => i.HasTag(Tags.OxygenSource) && i.Condition > min, recursive: true).Count == 1) { character.Speak(TextManager.Get("dialoglastoxygentank").Value, null, 0.0f, "dialoglastoxygentank".ToIdentifier(), 30.0f); } @@ -109,7 +96,7 @@ namespace Barotrauma } } var container = targetItem.GetComponent(); - var objective = new AIObjectiveContainItem(character, OXYGEN_SOURCE, container, objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC) + var objective = new AIObjectiveContainItem(character, Tags.OxygenSource, container, objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC) { AllowToFindDivingGear = false, AllowDangerousPressure = true, @@ -119,7 +106,7 @@ namespace Barotrauma }; if (container.HasSubContainers) { - objective.TargetSlot = container.FindSuitableSubContainerIndex(OXYGEN_SOURCE); + objective.TargetSlot = container.FindSuitableSubContainerIndex(Tags.OxygenSource); } // Only remove the oxygen source being replaced objective.RemoveExistingPredicate = i => objective.IsInTargetSlot(i); @@ -132,7 +119,7 @@ namespace Barotrauma // Try to seek any oxygen sources, even if they have minimal amount of oxygen. TryAddSubObjective(ref getOxygen, () => { - return new AIObjectiveContainItem(character, OXYGEN_SOURCE, targetItem.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC) + return new AIObjectiveContainItem(character, Tags.OxygenSource, targetItem.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC) { AllowToFindDivingGear = false, AllowDangerousPressure = true, @@ -142,7 +129,7 @@ namespace Barotrauma onAbandon: () => { Abandon = true; - if (remainingTanks > 0 && !HumanAIController.HasItem(character, OXYGEN_SOURCE, out _, conditionPercentage: 0.01f)) + if (remainingTanks > 0 && !HumanAIController.HasItem(character, Tags.OxygenSource, out _, conditionPercentage: 0.01f)) { character.Speak(TextManager.Get("dialogcantfindtoxygen").Value, null, 0, "cantfindoxygen".ToIdentifier(), 30.0f); } @@ -158,7 +145,7 @@ namespace Barotrauma int ReportOxygenTankCount() { if (character.Submarine != Submarine.MainSub) { return 1; } - int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > 1); + int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(Tags.OxygenSource) && i.Condition > 1); if (remainingOxygenTanks == 0) { character.Speak(TextManager.Get("DialogOutOfOxygenTanks").Value, null, 0.0f, "outofoxygentanks".ToIdentifier(), 30.0f); @@ -177,7 +164,7 @@ namespace Barotrauma { return item != null && - item.HasTag(OXYGEN_SOURCE) && + item.HasTag(Tags.OxygenSource) && item.Condition > 0 && (oxygenSourceSlotIndex == null || item.ParentInventory.IsInSlot(item, oxygenSourceSlotIndex.Value)); } @@ -188,7 +175,7 @@ namespace Barotrauma targetItem = item; if (targetItem != null) { - oxygenSourceSlotIndex = targetItem.GetComponent()?.FindSuitableSubContainerIndex(OXYGEN_SOURCE); + oxygenSourceSlotIndex = targetItem.GetComponent()?.FindSuitableSubContainerIndex(Tags.OxygenSource); } else { @@ -212,7 +199,7 @@ namespace Barotrauma // When we are venturing outside of our sub, let's just suppose that we have enough oxygen with us and optimize it so that we don't keep switching off half used tanks. float min = 0.01f; float minOxygen = character.IsInFriendlySub ? MIN_OXYGEN : min; - if (minOxygen > min && character.Inventory.AllItems.Any(i => i.HasTag("oxygensource") && i.ConditionPercentage >= minOxygen)) + if (minOxygen > min && character.Inventory.AllItems.Any(i => i.HasTag(Tags.OxygenSource) && i.ConditionPercentage >= minOxygen)) { // There's a valid oxygen tank in the inventory -> no need to swap the tank too early. minOxygen = min; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index a35ec388d..a9abe7d30 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -40,24 +40,21 @@ namespace Barotrauma protected override float GetPriority() { - if (!IsAllowed) - { - Priority = 0; - return Priority; - } if (character.CurrentHull == null) { Priority = ( objectiveManager.HasOrder(o => o.Priority > 0) || - objectiveManager.HasOrder(o => o.Priority > 0) || objectiveManager.HasActiveObjective() || - objectiveManager.Objectives.Any(o => o is AIObjectiveCombat && o.Priority > 0)) + objectiveManager.Objectives.Any(o => (o is AIObjectiveCombat || o is AIObjectiveReturn) && o.Priority > 0)) && ((!character.IsLowInOxygen && character.IsImmuneToPressure)|| HumanAIController.HasDivingSuit(character)) ? 0 : AIObjectiveManager.EmergencyObjectivePriority - 10; } else { - if ((character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false)) || - NeedMoreDivingGear(character.CurrentHull, AIObjectiveFindDivingGear.GetMinOxygen(character))) + bool isSuffocatingInDivingSuit = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false); + static bool IsSuffocatingWithoutDivingGear(Character c) => c.IsLowInOxygen && c.AnimController.HeadInWater && !HumanAIController.HasDivingGear(c, requireOxygenTank: true); + if (isSuffocatingInDivingSuit || + NeedMoreDivingGear(character.CurrentHull, AIObjectiveFindDivingGear.GetMinOxygen(character)) || + (!objectiveManager.HasActiveObjective() && IsSuffocatingWithoutDivingGear(character))) { Priority = AIObjectiveManager.MaxObjectivePriority; } @@ -215,7 +212,7 @@ namespace Barotrauma AllowGoingOutside = character.IsProtectedFromPressure || character.CurrentHull == null || - character.CurrentHull.IsTaggedAirlock() || + character.CurrentHull.IsAirlock || character.CurrentHull.LeadsOutside(character) }, onCompleted: () => @@ -258,6 +255,13 @@ namespace Barotrauma } if (subObjectives.Any(so => so.CanBeCompleted)) { return; } UpdateSimpleEscape(deltaTime); + if (cannotFindSafeHull && !character.IsInFriendlySub && objectiveManager.Objectives.None(o => o is AIObjectiveReturn)) + { + if (OrderPrefab.Prefabs.TryGet("return".ToIdentifier(), out OrderPrefab orderPrefab)) + { + objectiveManager.AddObjective(new AIObjectiveReturn(character, character, objectiveManager)); + } + } } } @@ -433,8 +437,7 @@ namespace Barotrauma } else { - // TODO: could also target gaps that get us inside? - if (potentialHull.IsTaggedAirlock()) + if (potentialHull.IsAirlock) { hullSafety = 100; hullIsAirlock = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index f08a264c6..b4c28bfb7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -12,7 +12,9 @@ namespace Barotrauma public override Identifier Identifier { get; set; } = "fix leak".ToIdentifier(); public override bool ForceRun => true; public override bool KeepDivingGearOn => true; + public override bool AllowInFriendlySubs => true; public override bool AllowInAnySub => true; + public override bool AllowWhileHandcuffed => false; public Gap Leak { get; private set; } @@ -35,8 +37,7 @@ namespace Barotrauma { if (!IsAllowed) { - Priority = 0; - Abandon = true; + HandleNonAllowed(); return Priority; } float coopMultiplier = 1; @@ -94,6 +95,7 @@ namespace Barotrauma protected override void Act(float deltaTime) { var weldingTool = character.Inventory.FindItemByTag("weldingequipment".ToIdentifier(), true); + var repairTool = weldingTool?.GetComponent(); if (weldingTool == null) { TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingequipment".ToIdentifier(), objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC), @@ -110,17 +112,25 @@ namespace Barotrauma } else { - if (weldingTool.OwnInventory == null) + if (repairTool == null) { #if DEBUG - DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"" + weldingTool + "\" has no proper inventory"); + DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"{weldingTool}\" has no RepairTool component but is tagged as a welding tool"); #endif Abandon = true; return; } - if (weldingTool.OwnInventory != null && weldingTool.OwnInventory.AllItems.None(i => i.HasTag("weldingfuel") && i.Condition > 0.0f)) + if (weldingTool.OwnInventory == null && repairTool.requiredItems.Any(r => r.Key == RelatedItem.RelationType.Contained)) { - TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel".ToIdentifier(), weldingTool.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC) +#if DEBUG + DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"{weldingTool}\" has no proper inventory"); +#endif + Abandon = true; + return; + } + if (weldingTool.OwnInventory != null && weldingTool.OwnInventory.AllItems.None(i => i.HasTag(Tags.WeldingFuel) && i.Condition > 0.0f)) + { + TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, Tags.WeldingFuel, weldingTool.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC) { RemoveExisting = true }, @@ -138,7 +148,7 @@ namespace Barotrauma void ReportWeldingFuelTankCount() { if (character.Submarine != Submarine.MainSub) { return; } - int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("weldingfuel") && i.Condition > 1); + int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(Tags.WeldingFuel) && i.Condition > 1); if (remainingOxygenTanks == 0) { character.Speak(TextManager.Get("DialogOutOfWeldingFuel").Value, null, 0.0f, "outofweldingfuel".ToIdentifier(), 30.0f); @@ -152,15 +162,6 @@ namespace Barotrauma } } if (subObjectives.Any()) { return; } - var repairTool = weldingTool.GetComponent(); - if (repairTool == null) - { -#if DEBUG - DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"" + weldingTool + "\" has no RepairTool component but is tagged as a welding tool"); -#endif - Abandon = true; - return; - } Vector2 toLeak = Leak.WorldPosition - character.AnimController.AimSourceWorldPos; // TODO: use the collider size/reach? if (!character.AnimController.InWater && Math.Abs(toLeak.X) < 100 && toLeak.Y < 0.0f && toLeak.Y > -150) @@ -200,7 +201,8 @@ namespace Barotrauma Leak.linkedTo.Any(e => e is Hull h && (character.CurrentHull == h || h.linkedTo.Contains(character.CurrentHull))), endNodeFilter = IsSuitableEndNode, // The Go To objective can be abandoned if the leak is fixed (in which case we don't want to use the dialogue) - SpeakCannotReachCondition = () => !CheckObjectiveSpecific() + // Only report about contextual targets. + SpeakCannotReachCondition = () => isPriority && !CheckObjectiveSpecific() }, onAbandon: () => { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs index d9c8ff24a..dff7dc2ab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs @@ -9,7 +9,8 @@ namespace Barotrauma public override Identifier Identifier { get; set; } = "fix leaks".ToIdentifier(); public override bool ForceRun => true; public override bool KeepDivingGearOn => true; - public override bool AllowInAnySub => true; + public override bool AllowInFriendlySubs => true; + private Hull PrioritizedHull { get; set; } public AIObjectiveFixLeaks(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, Hull prioritizedHull = null) : base(character, objectiveManager, priorityModifier) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index aaee84fa1..1ed1fe400 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -15,6 +15,7 @@ namespace Barotrauma public override bool AbandonWhenCannotCompleteSubjectives => false; public override bool AllowMultipleInstances => true; + public override bool AllowWhileHandcuffed => false; public HashSet ignoredItems = new HashSet(); @@ -158,11 +159,6 @@ namespace Barotrauma protected override void Act(float deltaTime) { - if (character.LockHands) - { - Abandon = true; - return; - } if (IdentifiersOrTags != null && !isDoneSeeking) { if (checkInventory) @@ -271,15 +267,49 @@ namespace Barotrauma Inventory itemInventory = targetItem.ParentInventory; var slots = itemInventory?.FindIndices(targetItem); + var droppedStack = TargetItem.DroppedStack.ToList(); if (HumanAIController.TakeItem(targetItem, character.Inventory, Equip, Wear, storeUnequipped: true, targetTags: IdentifiersOrTags)) { - if (TakeWholeStack && slots != null) + if (TakeWholeStack) { - foreach (int slot in slots) + //taking the whole stack in this context means "as many items that can fit in one of the bot's slots", + //and the stack means either a stack of items in an inventory slot or a "dropped stack" + //so we need a bit of extra logic here + int maxStackSize = 0; + int takenItemCount = 1; + for (int i = 0; i < character.Inventory.Capacity; i++) { - foreach (Item item in itemInventory.GetItemsAt(slot).ToList()) + maxStackSize = Math.Max(maxStackSize, character.Inventory.HowManyCanBePut(targetItem.Prefab, i, condition: null)); + } + if (slots != null) + { + foreach (int slot in slots) { - HumanAIController.TakeItem(item, character.Inventory, equip: false, storeUnequipped: true); + foreach (Item item in itemInventory.GetItemsAt(slot).ToList()) + { + if (HumanAIController.TakeItem(item, character.Inventory, equip: false, storeUnequipped: true)) + { + takenItemCount++; + if (takenItemCount >= maxStackSize) { break; } + } + else + { + break; + } + } + } + } + foreach (var item in droppedStack) + { + if (item == TargetItem) { continue; } + if (HumanAIController.TakeItem(item, character.Inventory, equip: false, storeUnequipped: true)) + { + takenItemCount++; + if (takenItemCount >= maxStackSize) { break; } + } + else + { + break; } } } @@ -411,7 +441,7 @@ namespace Barotrauma if (!CheckItem(item)) { continue; } if (item.Container != null) { - if (item.Container.HasTag("donttakeitems")) { continue; } + if (item.Container.HasTag(Tags.DontTakeItems)) { continue; } if (ignoredItems.Contains(item.Container)) { continue; } if (ignoredContainerIdentifiers != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs index 5355767d1..5c909a4a0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs @@ -12,6 +12,7 @@ namespace Barotrauma public override string DebugTag => $"{Identifier}"; public override bool KeepDivingGearOn => true; public override bool AllowMultipleInstances => true; + public override bool AllowWhileHandcuffed => false; public bool AllowStealing { get; set; } public bool TakeWholeStack { get; set; } @@ -40,55 +41,48 @@ namespace Barotrauma protected override void Act(float deltaTime) { - if (character.LockHands) + if (subObjectivesCreated) { return; } + foreach (Identifier tag in gearTags) { - Abandon = true; - return; - } - if (!subObjectivesCreated) - { - foreach (Identifier tag in gearTags) - { - if (subObjectives.Any(so => so is AIObjectiveGetItem getItem && getItem.IdentifiersOrTags.Contains(tag))) { continue; } - int count = gearTags.Count(t => t == tag); - AIObjectiveGetItem? getItem = null; - TryAddSubObjective(ref getItem, () => - new AIObjectiveGetItem(character, tag, objectiveManager, Equip, CheckInventory && count <= 1) - { - AllowVariants = AllowVariants, - Wear = Wear, - TakeWholeStack = TakeWholeStack, - AllowStealing = AllowStealing, - ignoredIdentifiersOrTags = ignoredTags, - CheckPathForEachItem = CheckPathForEachItem, - RequireNonEmpty = RequireNonEmpty, - ItemCount = count, - SpeakIfFails = RequireAllItems - }, - onCompleted: () => - { - var item = getItem?.TargetItem; - if (item?.IsOwnedBy(character) != null) - { - achievedItems.Add(item); - } - }, - onAbandon: () => - { - var item = getItem?.TargetItem; - if (item != null) - { - achievedItems.Remove(item); - } - RemoveSubObjective(ref getItem); - if (RequireAllItems) - { - Abandon = true; - } - }); - } - subObjectivesCreated = true; + if (subObjectives.Any(so => so is AIObjectiveGetItem getItem && getItem.IdentifiersOrTags.Contains(tag))) { continue; } + int count = gearTags.Count(t => t == tag); + AIObjectiveGetItem? getItem = null; + TryAddSubObjective(ref getItem, () => + new AIObjectiveGetItem(character, tag, objectiveManager, Equip, CheckInventory && count <= 1) + { + AllowVariants = AllowVariants, + Wear = Wear, + TakeWholeStack = TakeWholeStack, + AllowStealing = AllowStealing, + ignoredIdentifiersOrTags = ignoredTags, + CheckPathForEachItem = CheckPathForEachItem, + RequireNonEmpty = RequireNonEmpty, + ItemCount = count, + SpeakIfFails = RequireAllItems + }, + onCompleted: () => + { + var item = getItem?.TargetItem; + if (item?.IsOwnedBy(character) != null) + { + achievedItems.Add(item); + } + }, + onAbandon: () => + { + var item = getItem?.TargetItem; + if (item != null) + { + achievedItems.Remove(item); + } + RemoveSubObjective(ref getItem); + if (RequireAllItems) + { + Abandon = true; + } + }); } + subObjectivesCreated = true; } public override void Reset() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index dfc1966bf..c5b039824 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -223,23 +223,30 @@ namespace Barotrauma Hull targetHull = GetTargetHull(); if (!IsFollowOrder) { + // Abandon if going through unsafe paths or targeting unsafe hulls. bool isUnreachable = HumanAIController.UnreachableHulls.Contains(targetHull); if (!objectiveManager.CurrentObjective.IgnoreUnsafeHulls) { - if (HumanAIController.UnsafeHulls.Contains(targetHull)) + // Wait orders check this so that the bot temporarily leaves the unsafe hull. + // Non-orders (that are not set to ignore the unsafe hulls) abandon. In practice this means e.g. repair and clean up item subobjectives (of the looping parent objective). + // Other orders are only abandoned if the hull is unreachable, because the path is invalid or not found at all. + if (IsWaitOrder || !objectiveManager.HasOrders()) { - isUnreachable = true; - HumanAIController.AskToRecalculateHullSafety(targetHull); - } - else if (PathSteering?.CurrentPath != null) - { - foreach (WayPoint wp in PathSteering.CurrentPath.Nodes) + if (HumanAIController.UnsafeHulls.Contains(targetHull)) { - if (wp.CurrentHull == null) { continue; } - if (HumanAIController.UnsafeHulls.Contains(wp.CurrentHull)) + isUnreachable = true; + HumanAIController.AskToRecalculateHullSafety(targetHull); + } + else if (PathSteering?.CurrentPath != null) + { + foreach (WayPoint wp in PathSteering.CurrentPath.Nodes) { - isUnreachable = true; - HumanAIController.AskToRecalculateHullSafety(wp.CurrentHull); + if (wp.CurrentHull == null) { continue; } + if (HumanAIController.UnsafeHulls.Contains(wp.CurrentHull)) + { + isUnreachable = true; + HumanAIController.AskToRecalculateHullSafety(wp.CurrentHull); + } } } } @@ -803,7 +810,7 @@ namespace Barotrauma private void StopMovement() { - SteeringManager.Reset(); + SteeringManager?.Reset(); if (Target != null) { character.AnimController.TargetDir = Target.WorldPosition.X > character.WorldPosition.X ? Direction.Right : Direction.Left; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index bb0c590c2..bd01ecebf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -382,7 +382,9 @@ namespace Barotrauma { foreach (Item item in Item.ItemList) { - if (item.CurrentHull != currentHull || !item.HasTag("chair")) { continue; } + if (item.CurrentHull != currentHull || !item.HasTag(Tags.ChairItem)) { continue; } + //not possible in vanilla game, but a mod might have holdable/attachable chairs + if (item.ParentInventory != null || item.body is { Enabled: true }) { continue; } var controller = item.GetComponent(); if (controller == null || controller.User != null) { continue; } item.TryInteract(character, forceSelectKey: true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs index 118849867..74ac238a2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs @@ -17,6 +17,8 @@ namespace Barotrauma set => throw new Exception("Trying to set the value for AIObjectiveLoadItem.IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); } + public override bool AllowWhileHandcuffed => false; + private AIObjectiveLoadItems.ItemCondition TargetItemCondition { get; } private Item Container { get; } private ItemContainer ItemContainer { get; } @@ -161,8 +163,7 @@ namespace Barotrauma { if (!IsAllowed) { - Priority = 0; - Abandon = true; + HandleNonAllowed(); return Priority; } else if (!AIObjectiveLoadItems.IsValidTarget(Container, character, targetCondition: TargetItemCondition)) @@ -299,7 +300,7 @@ namespace Barotrauma if (rootInventoryOwner is Character owner && owner != character) { return false; } if (rootInventoryOwner is Item parentItem) { - if (parentItem.HasTag("donttakeitems")) { return false; } + if (parentItem.HasTag(Tags.DontTakeItems)) { return false; } } if (!item.HasAccess(character)) { return false; } if (!character.HasItem(item) && !CanEquip(item, allowWearing: false)) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs index 8edac36a0..5419eedf7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs @@ -44,6 +44,8 @@ namespace Barotrauma public override bool CanBeCompleted => true; public override bool AbandonWhenCannotCompleteSubjectives => false; public override bool AllowSubObjectiveSorting => true; + public override bool AllowWhileHandcuffed => false; + public virtual bool InverseTargetEvaluation => false; protected virtual bool ResetWhenClearingIgnoreList => true; protected virtual bool ForceOrderPriority => true; @@ -117,51 +119,44 @@ namespace Barotrauma { if (!IsAllowed) { - Priority = 0; + HandleNonAllowed(); return Priority; } - if (character.LockHands) + // Allow the target value to be more than 100. + float targetValue = TargetEvaluation(); + if (InverseTargetEvaluation) + { + targetValue = 100 - targetValue; + } + var currentSubObjective = CurrentSubObjective; + if (currentSubObjective != null && currentSubObjective.Priority > targetValue) + { + // If the priority is higher than the target value, let's just use it. + // The priority calculation is more precise, but it takes into account things like distances, + // so it's better not to use it if it's lower than the rougher targetValue. + targetValue = currentSubObjective.Priority; + } + // If the target value is less than 1% of the max value, let's just treat it as zero. + if (targetValue < 1) { Priority = 0; } else { - // Allow the target value to be more than 100. - float targetValue = TargetEvaluation(); - if (InverseTargetEvaluation) + if (objectiveManager.IsOrder(this)) { - targetValue = 100 - targetValue; - } - var currentSubObjective = CurrentSubObjective; - if (currentSubObjective != null && currentSubObjective.Priority > targetValue) - { - // If the priority is higher than the target value, let's just use it. - // The priority calculation is more precise, but it takes into account things like distances, - // so it's better not to use it if it's lower than the rougher targetValue. - targetValue = currentSubObjective.Priority; - } - // If the target value is less than 1% of the max value, let's just treat it as zero. - if (targetValue < 1) - { - Priority = 0; + Priority = ForceOrderPriority ? objectiveManager.GetOrderPriority(this) : targetValue; } else { - if (objectiveManager.IsOrder(this)) + float max = AIObjectiveManager.LowestOrderPriority - 1; + if (this is AIObjectiveRescueAll rescueObjective && rescueObjective.Targets.Contains(character)) { - Priority = ForceOrderPriority ? objectiveManager.GetOrderPriority(this) : targetValue; - } - else - { - float max = AIObjectiveManager.LowestOrderPriority - 1; - if (this is AIObjectiveRescueAll rescueObjective && rescueObjective.Targets.Contains(character)) - { - // Allow higher prio - max = AIObjectiveManager.EmergencyObjectivePriority; - } - float value = MathHelper.Clamp((CumulatedDevotion + (targetValue * PriorityModifier)) / 100, 0, 1); - Priority = MathHelper.Lerp(0, max, value); + // Allow higher prio + max = AIObjectiveManager.EmergencyObjectivePriority; } + float value = MathHelper.Clamp((CumulatedDevotion + (targetValue * PriorityModifier)) / 100, 0, 1); + Priority = MathHelper.Lerp(0, max, value); } } return Priority; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index af05c7095..40be336b8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -530,7 +530,7 @@ namespace Barotrauma case "cleanupitems": if (order.TargetEntity is Item targetItem) { - if (targetItem.HasTag("allowcleanup") && targetItem.ParentInventory == null && targetItem.OwnInventory != null) + if (targetItem.HasTag(Tags.AllowCleanup) && targetItem.ParentInventory == null && targetItem.OwnInventory != null) { // Target all items inside the container newObjective = new AIObjectiveCleanupItems(character, this, targetItem.OwnInventory.AllItems, priorityModifier); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs index 163b7bee2..2e35e6ff0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -14,6 +14,7 @@ namespace Barotrauma public override bool AllowAutomaticItemUnequipping => true; public override bool AllowMultipleInstances => true; public override bool AllowInAnySub => true; + public override bool AllowWhileHandcuffed => false; public override bool PrioritizeIfSubObjectivesActive => component != null && (component is Reactor || component is Turret); private readonly ItemComponent component, controller; @@ -47,10 +48,9 @@ namespace Barotrauma protected override float GetPriority() { bool isOrder = objectiveManager.IsOrder(this); - if (!IsAllowed || character.LockHands) + if (!IsAllowed) { - Priority = 0; - Abandon = !isOrder; + HandleNonAllowed(); return Priority; } if (!isOrder && component.Item.ConditionPercentage <= 0) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs index b46f1f2e4..b5515d5de 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs @@ -13,6 +13,7 @@ namespace Barotrauma public override bool KeepDivingGearOn => true; public override bool KeepDivingGearOnAlsoWhenInactive => true; public override bool PrioritizeIfSubObjectivesActive => true; + public override bool AllowWhileHandcuffed => false; private AIObjectiveGetItem getSingleItemObjective; private AIObjectiveGetItems getAllItemsObjective; @@ -60,8 +61,7 @@ namespace Barotrauma { if (!IsAllowed) { - Priority = 0; - Abandon = true; + HandleNonAllowed(); return Priority; } Priority = objectiveManager.GetOrderPriority(this); @@ -75,11 +75,6 @@ namespace Barotrauma protected override void Act(float deltaTime) { - if (character.LockHands) - { - Abandon = true; - return; - } if (!subObjectivesCreated) { if (FindAllItems && targetItem == null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs index 72d84b107..80392c641 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs @@ -12,6 +12,7 @@ namespace Barotrauma public override Identifier Identifier { get; set; } = "pump water".ToIdentifier(); public override bool KeepDivingGearOn => true; public override bool AllowAutomaticItemUnequipping => true; + public override bool AllowWhileHandcuffed => false; private List pumpList; @@ -54,7 +55,7 @@ namespace Barotrauma var pump = item.GetComponent(); if (pump == null || pump.Item.Submarine == null || pump.Item.CurrentHull == null) { continue; } if (pump.Item.Submarine.TeamID != character.TeamID) { continue; } - if (pump.Item.HasTag("ballast")) { continue; } + if (pump.Item.HasTag(Tags.Ballast)) { continue; } pumpList.Add(pump); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index 9c06fb8b2..81197c7e0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -10,8 +10,9 @@ namespace Barotrauma { public override Identifier Identifier { get; set; } = "repair item".ToIdentifier(); - public override bool AllowInAnySub => true; + public override bool AllowInFriendlySubs => true; public override bool KeepDivingGearOn => Item?.CurrentHull == null; + public override bool AllowWhileHandcuffed => false; public Item Item { get; private set; } @@ -36,10 +37,13 @@ namespace Barotrauma protected override float GetPriority() { - if (!IsAllowed || Item.IgnoreByAI(character)) + if (!IsAllowed) { HandleNonAllowed(); } + if (Item.IgnoreByAI(character)) { - Priority = 0; Abandon = true; + } + if (Abandon) + { if (IsRepairing()) { Item.Repairables.ForEach(r => r.StopRepairing(character)); @@ -136,32 +140,35 @@ namespace Barotrauma } if (repairTool != null) { - if (repairTool.Item.OwnInventory == null) + if (repairTool.requiredItems.TryGetValue(RelatedItem.RelationType.Contained, out var requiredItems)) { -#if DEBUG - DebugConsole.ThrowError($"{character.Name}: AIObjectiveRepairItem failed - the item \"" + repairTool + "\" has no proper inventory"); -#endif - Abandon = true; - return; - } - RelatedItem item = null; - Item fuel = null; - foreach (RelatedItem requiredItem in repairTool.requiredItems[RelatedItem.RelationType.Contained]) - { - item = requiredItem; - fuel = repairTool.Item.OwnInventory.AllItems.FirstOrDefault(it => it.Condition > 0.0f && requiredItem.MatchesItem(it)); - if (fuel != null) { break; } - } - if (fuel == null) - { - RemoveSubObjective(ref goToObjective); - TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, item.Identifiers, repairTool.Item.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC) + if (repairTool.Item.OwnInventory == null) { - RemoveExisting = true - }, - onCompleted: () => RemoveSubObjective(ref refuelObjective), - onAbandon: () => Abandon = true); - return; +#if DEBUG + DebugConsole.ThrowError($"{character.Name}: AIObjectiveRepairItem failed - the item \"{repairTool}\" has no proper inventory."); +#endif + Abandon = true; + return; + } + RelatedItem item = null; + Item fuel = null; + foreach (RelatedItem requiredItem in requiredItems) + { + item = requiredItem; + fuel = repairTool.Item.OwnInventory.AllItems.FirstOrDefault(it => it.Condition > 0.0f && requiredItem.MatchesItem(it)); + if (fuel != null) { break; } + } + if (fuel == null) + { + RemoveSubObjective(ref goToObjective); + TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, item.Identifiers, repairTool.Item.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC) + { + RemoveExisting = true + }, + onCompleted: () => RemoveSubObjective(ref refuelObjective), + onAbandon: () => Abandon = true); + return; + } } } if (character.CanInteractWith(Item, out _, checkLinked: false)) @@ -170,10 +177,8 @@ namespace Barotrauma if (waitTimer < WaitTimeBeforeRepair) { return; } HumanAIController.FaceTarget(Item); - if (repairTool != null) - { - OperateRepairTool(deltaTime); - } + + bool repairThroughRepairInterface = false; foreach (Repairable repairable in Item.Repairables) { if (repairable.CurrentFixer != null && repairable.CurrentFixer != character) @@ -185,10 +190,11 @@ namespace Barotrauma { if (character.SelectedItem != Item) { - if (Item.TryInteract(character, ignoreRequiredItems: true, forceUseKey: true) || - Item.TryInteract(character, ignoreRequiredItems: true, forceSelectKey: true)) + if (Item.TryInteract(character, forceUseKey: true) || + Item.TryInteract(character, forceSelectKey: true)) { character.SelectedItem = Item; + repairThroughRepairInterface = true; } else { @@ -209,8 +215,17 @@ namespace Barotrauma { repairable.StartRepairing(character, Repairable.FixActions.Repair); } + else + { + repairThroughRepairInterface = true; + } break; } + + if (!repairThroughRepairInterface && repairTool != null && !Abandon) + { + OperateRepairTool(deltaTime); + } } else { @@ -222,7 +237,8 @@ namespace Barotrauma { var objective = new AIObjectiveGoTo(Item, character, objectiveManager) { - TargetName = Item.Name + TargetName = Item.Name, + SpeakCannotReachCondition = () => isPriority }; if (repairTool != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs index dbb0a68a9..3e10ab620 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs @@ -19,7 +19,7 @@ namespace Barotrauma public Item PrioritizedItem { get; private set; } public override bool AllowMultipleInstances => true; - public override bool AllowInAnySub => true; + public override bool AllowInFriendlySubs => true; public readonly static float RequiredSuccessFactor = 0.4f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index cba0a97f4..14963a008 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -16,12 +16,13 @@ namespace Barotrauma public override bool AllowOutsideSubmarine => true; public override bool AllowInAnySub => true; + public override bool AllowWhileHandcuffed => false; const float TreatmentDelay = 0.5f; const float CloseEnoughToTreat = 100.0f; - private readonly Character targetCharacter; + public readonly Character Target; private AIObjectiveGoTo goToObjective; private AIObjectiveContainItem replaceOxygenObjective; @@ -44,7 +45,7 @@ namespace Barotrauma Abandon = true; return; } - this.targetCharacter = targetCharacter; + Target = targetCharacter; } protected override void OnAbandon() @@ -61,55 +62,55 @@ namespace Barotrauma protected override void Act(float deltaTime) { - if (character.LockHands || targetCharacter == null || targetCharacter.Removed || targetCharacter.IsDead) + if (Target == null || Target.Removed || Target.IsDead) { Abandon = true; return; } - var otherRescuer = targetCharacter.SelectedBy; + var otherRescuer = Target.SelectedBy; if (otherRescuer != null && otherRescuer != character) { // Someone else is rescuing/holding the target. Abandon = otherRescuer.IsPlayer || character.GetSkillLevel("medical") < otherRescuer.GetSkillLevel("medical"); return; } - if (targetCharacter != character) + if (Target != character) { - if (targetCharacter.IsIncapacitated) + if (Target.IsIncapacitated) { // Check if the character needs more oxygen - if (!ignoreOxygen && character.SelectedCharacter == targetCharacter || character.CanInteractWith(targetCharacter)) + if (!ignoreOxygen && character.SelectedCharacter == Target || character.CanInteractWith(Target)) { // Replace empty oxygen and welding fuel. - if (HumanAIController.HasItem(targetCharacter, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out IEnumerable suits, requireEquipped: true)) + if (HumanAIController.HasItem(Target, Tags.HeavyDivingGear, out IEnumerable suits, requireEquipped: true)) { Item suit = suits.FirstOrDefault(); if (suit != null) { AIController.UnequipEmptyItems(character, suit); - AIController.UnequipContainedItems(character, suit, it => it.HasTag("weldingfuel")); + AIController.UnequipContainedItems(character, suit, it => it.HasTag(Tags.WeldingFuel)); } } - else if (HumanAIController.HasItem(targetCharacter, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out IEnumerable masks, requireEquipped: true)) + else if (HumanAIController.HasItem(Target, Tags.LightDivingGear, out IEnumerable masks, requireEquipped: true)) { Item mask = masks.FirstOrDefault(); if (mask != null) { AIController.UnequipEmptyItems(character, mask); - AIController.UnequipContainedItems(character, mask, it => it.HasTag("weldingfuel")); + AIController.UnequipContainedItems(character, mask, it => it.HasTag(Tags.WeldingFuel)); } } - bool ShouldRemoveDivingSuit() => targetCharacter.OxygenAvailable < CharacterHealth.InsufficientOxygenThreshold && targetCharacter.CurrentHull?.LethalPressure <= 0; + bool ShouldRemoveDivingSuit() => Target.OxygenAvailable < CharacterHealth.InsufficientOxygenThreshold && Target.CurrentHull?.LethalPressure <= 0; if (ShouldRemoveDivingSuit()) { suits.ForEach(suit => suit.Drop(character)); } - else if (suits.Any() && suits.None(s => s.OwnInventory?.AllItems != null && s.OwnInventory.AllItems.Any(it => it.HasTag(AIObjectiveFindDivingGear.OXYGEN_SOURCE) && it.ConditionPercentage > 0))) + else if (suits.Any() && suits.None(s => s.OwnInventory?.AllItems != null && s.OwnInventory.AllItems.Any(it => it.HasTag(Tags.OxygenSource) && it.ConditionPercentage > 0))) { // The target has a suit equipped with an empty oxygen tank. // Can't remove the suit, because the target needs it. // If we happen to have an extra oxygen tank in the inventory, let's swap it. - Item spareOxygenTank = FindOxygenTank(targetCharacter) ?? FindOxygenTank(character); + Item spareOxygenTank = FindOxygenTank(Target) ?? FindOxygenTank(character); if (spareOxygenTank != null) { Item suit = suits.FirstOrDefault(); @@ -133,36 +134,36 @@ namespace Barotrauma Item FindOxygenTank(Character c) => c.Inventory.FindItem(i => - i.HasTag(AIObjectiveFindDivingGear.OXYGEN_SOURCE) && + i.HasTag(Tags.OxygenSource) && i.ConditionPercentage > 1 && - i.FindParentInventory(inv => inv.Owner is Item otherItem && otherItem.HasTag("diving")) == null, + i.FindParentInventory(inv => inv.Owner is Item otherItem && otherItem.HasTag(Tags.DivingGear)) == null, recursive: true); } } - if (character.Submarine != null && targetCharacter.CurrentHull != null) + if (character.Submarine != null && Target.CurrentHull != null) { - if (HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD) + if (HumanAIController.GetHullSafety(Target.CurrentHull, Target) < HumanAIController.HULL_SAFETY_THRESHOLD) { // Incapacitated target is not in a safe place -> Move to a safe place first - if (character.SelectedCharacter != targetCharacter) + if (character.SelectedCharacter != Target) { - if (HumanAIController.VisibleHulls.Contains(targetCharacter.CurrentHull) && targetCharacter.CurrentHull.DisplayName != null) + if (HumanAIController.VisibleHulls.Contains(Target.CurrentHull) && Target.CurrentHull.DisplayName != null) { character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget", - ("[targetname]", targetCharacter.Name, FormatCapitals.No), - ("[roomname]", targetCharacter.CurrentHull.DisplayName, FormatCapitals.Yes)).Value, - null, 1.0f, $"foundunconscioustarget{targetCharacter.Name}".ToIdentifier(), 60.0f); + ("[targetname]", Target.Name, FormatCapitals.No), + ("[roomname]", Target.CurrentHull.DisplayName, FormatCapitals.Yes)).Value, + null, 1.0f, $"foundunconscioustarget{Target.Name}".ToIdentifier(), 60.0f); } // Go to the target and select it - if (!character.CanInteractWith(targetCharacter)) + if (!character.CanInteractWith(Target)) { RemoveSubObjective(ref replaceOxygenObjective); RemoveSubObjective(ref goToObjective); - TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager) + TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(Target, character, objectiveManager) { CloseEnough = CloseEnoughToTreat, DialogueIdentifier = "dialogcannotreachpatient".ToIdentifier(), - TargetName = targetCharacter.DisplayName + TargetName = Target.DisplayName }, onCompleted: () => RemoveSubObjective(ref goToObjective), onAbandon: () => @@ -173,7 +174,7 @@ namespace Barotrauma } else { - character.SelectCharacter(targetCharacter); + character.SelectCharacter(Target); } } else @@ -213,16 +214,16 @@ namespace Barotrauma if (subObjectives.Any()) { return; } - if (targetCharacter != character && !character.CanInteractWith(targetCharacter)) + if (Target != character && !character.CanInteractWith(Target)) { RemoveSubObjective(ref replaceOxygenObjective); RemoveSubObjective(ref goToObjective); // Go to the target and select it - TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager) + TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(Target, character, objectiveManager) { CloseEnough = CloseEnoughToTreat, DialogueIdentifier = "dialogcannotreachpatient".ToIdentifier(), - TargetName = targetCharacter.DisplayName + TargetName = Target.DisplayName }, onCompleted: () => RemoveSubObjective(ref goToObjective), onAbandon: () => @@ -234,14 +235,14 @@ namespace Barotrauma else { // We can start applying treatment - if (character != targetCharacter && character.SelectedCharacter != targetCharacter) + if (character != Target && character.SelectedCharacter != Target) { - if (targetCharacter.CurrentHull?.DisplayName != null) + if (Target.CurrentHull?.DisplayName != null) { character.Speak(TextManager.GetWithVariables("DialogFoundWoundedTarget", - ("[targetname]", targetCharacter.Name, FormatCapitals.No), - ("[roomname]", targetCharacter.CurrentHull.DisplayName, FormatCapitals.Yes)).Value, - null, 1.0f, $"foundwoundedtarget{targetCharacter.Name}".ToIdentifier(), 60.0f); + ("[targetname]", Target.Name, FormatCapitals.No), + ("[roomname]", Target.CurrentHull.DisplayName, FormatCapitals.Yes)).Value, + null, 1.0f, $"foundwoundedtarget{Target.Name}".ToIdentifier(), 60.0f); } } GiveTreatment(deltaTime); @@ -253,7 +254,7 @@ namespace Barotrauma private readonly Dictionary currentTreatmentSuitabilities = new Dictionary(); private void GiveTreatment(float deltaTime) { - if (targetCharacter == null) + if (Target == null) { string errorMsg = $"{character.Name}: Attempted to update a Rescue objective with no target!"; DebugConsole.ThrowError(errorMsg); @@ -263,10 +264,10 @@ namespace Barotrauma SteeringManager.Reset(); - if (!targetCharacter.IsPlayer) + if (!Target.IsPlayer) { // If the target is a bot, don't let it move - targetCharacter.AIController?.SteeringManager?.Reset(); + Target.AIController?.SteeringManager?.Reset(); } if (treatmentTimer > 0.0f) { @@ -275,28 +276,28 @@ namespace Barotrauma } treatmentTimer = TreatmentDelay; - float cprSuitability = targetCharacter.Oxygen < 0.0f ? -targetCharacter.Oxygen * 100.0f : 0.0f; + float cprSuitability = Target.Oxygen < 0.0f ? -Target.Oxygen * 100.0f : 0.0f; //find which treatments are the most suitable to treat the character's current condition - targetCharacter.CharacterHealth.GetSuitableTreatments(currentTreatmentSuitabilities, user: character, normalize: false, predictFutureDuration: 10.0f); + Target.CharacterHealth.GetSuitableTreatments(currentTreatmentSuitabilities, user: character, normalize: false, predictFutureDuration: 10.0f); //check if we already have a suitable treatment for any of the afflictions - foreach (Affliction affliction in GetSortedAfflictions(targetCharacter)) + foreach (Affliction affliction in GetSortedAfflictions(Target)) { if (affliction == null) { throw new Exception("Affliction was null"); } if (affliction.Prefab == null) { throw new Exception("Affliction prefab was null"); } float bestSuitability = 0.0f; Item bestItem = null; - foreach (KeyValuePair treatmentSuitability in affliction.Prefab.TreatmentSuitability) + foreach (KeyValuePair treatmentSuitability in affliction.Prefab.TreatmentSuitabilities) { if (currentTreatmentSuitabilities.ContainsKey(treatmentSuitability.Key) && currentTreatmentSuitabilities[treatmentSuitability.Key] > bestSuitability) { Item matchingItem = character.Inventory.FindItemByIdentifier(treatmentSuitability.Key, true); //allow taking items from the target's inventory too if the target is unconscious - if (matchingItem == null && targetCharacter.IsIncapacitated) + if (matchingItem == null && Target.IsIncapacitated) { - matchingItem ??= targetCharacter.Inventory?.FindItemByIdentifier(treatmentSuitability.Key, true); + matchingItem ??= Target.Inventory?.FindItemByIdentifier(treatmentSuitability.Key, true); } if (matchingItem != null) { @@ -307,7 +308,7 @@ namespace Barotrauma } if (bestItem != null) { - if (targetCharacter != character) { character.SelectCharacter(targetCharacter); } + if (Target != character) { character.SelectCharacter(Target); } ApplyTreatment(affliction, bestItem); //wait a bit longer after applying a treatment to wait for potential side-effects to manifest treatmentTimer = TreatmentDelay * 4; @@ -370,12 +371,12 @@ namespace Barotrauma ("[treatment1]", itemListStr), ("[treatment2]", itemNameList.Last())); } - if (targetCharacter != character && character.IsOnPlayerTeam) + if (Target != character && character.IsOnPlayerTeam) { character.Speak(TextManager.GetWithVariables("DialogListRequiredTreatments", - ("[targetname]", targetCharacter.Name, FormatCapitals.No), + ("[targetname]", Target.Name, FormatCapitals.No), ("[treatmentlist]", itemListStr, FormatCapitals.Yes)).Value, - null, 2.0f, $"listrequiredtreatments{targetCharacter.Name}".ToIdentifier(), 60.0f); + null, 2.0f, $"listrequiredtreatments{Target.Name}".ToIdentifier(), 60.0f); } RemoveSubObjective(ref getItemObjective); TryAddSubObjective(ref getItemObjective, @@ -397,18 +398,18 @@ namespace Barotrauma } } } - else if (!targetCharacter.IsUnconscious) + else if (!Target.IsUnconscious) { Abandon = true; //no suitable treatments found, not inside our own sub (= can't search for more treatments), the target isn't unconscious (= can't give CPR) SpeakCannotTreat(); return; } - if (character != targetCharacter) + if (character != Target) { if (cprSuitability > 0.0f) { - character.SelectCharacter(targetCharacter); + character.SelectCharacter(Target); character.AnimController.Anim = AnimController.Animation.CPR; performedCpr = true; } @@ -421,40 +422,40 @@ namespace Barotrauma private void SpeakCannotTreat() { - LocalizedString msg = character == targetCharacter ? + LocalizedString msg = character == Target ? TextManager.Get("dialogcannottreatself") : - TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, FormatCapitals.No); + TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", Target.DisplayName, FormatCapitals.No); character.Speak(msg.Value, identifier: "cannottreatpatient".ToIdentifier(), minDurationBetweenSimilar: 20.0f); } private void ApplyTreatment(Affliction affliction, Item item) { - item.ApplyTreatment(character, targetCharacter, targetCharacter.CharacterHealth.GetAfflictionLimb(affliction)); + item.ApplyTreatment(character, Target, Target.CharacterHealth.GetAfflictionLimb(affliction)); } protected override bool CheckObjectiveSpecific() { - bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter); - if (isCompleted && targetCharacter != character && character.IsOnPlayerTeam) + bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(Target) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, Target); + if (isCompleted && Target != character && character.IsOnPlayerTeam) { string textTag = performedCpr ? "DialogTargetResuscitated" : "DialogTargetHealed"; - string message = TextManager.GetWithVariable(textTag, "[targetname]", targetCharacter.Name)?.Value; - character.Speak(message, delay: 1.0f, identifier: $"targethealed{targetCharacter.Name}".ToIdentifier(), minDurationBetweenSimilar: 60.0f); + string message = TextManager.GetWithVariable(textTag, "[targetname]", Target.Name)?.Value; + character.Speak(message, delay: 1.0f, identifier: $"targethealed{Target.Name}".ToIdentifier(), minDurationBetweenSimilar: 60.0f); } return isCompleted; } protected override float GetPriority() { - if (!IsAllowed || targetCharacter == null) + if (Target == null) { Abandon = true; } + if (!IsAllowed) { HandleNonAllowed(); } + if (Abandon) { - Priority = 0; - Abandon = true; return Priority; } if (character.CurrentHull != null) { - if (Character.CharacterList.Any(c => c.CurrentHull == targetCharacter.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c))) + if (Character.CharacterList.Any(c => c.CurrentHull == Target.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c))) { // Don't go into rooms that have enemies Priority = 0; @@ -462,18 +463,18 @@ namespace Barotrauma return Priority; } } - float horizontalDistance = Math.Abs(character.WorldPosition.X - targetCharacter.WorldPosition.X); - float verticalDistance = Math.Abs(character.WorldPosition.Y - targetCharacter.WorldPosition.Y); + float horizontalDistance = Math.Abs(character.WorldPosition.X - Target.WorldPosition.X); + float verticalDistance = Math.Abs(character.WorldPosition.Y - Target.WorldPosition.Y); if (character.Submarine?.Info is { IsRuin: false }) { verticalDistance *= 2; } float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, horizontalDistance + verticalDistance)); - if (character.CurrentHull != null && targetCharacter.CurrentHull == character.CurrentHull) + if (character.CurrentHull != null && Target.CurrentHull == character.CurrentHull) { distanceFactor = 1; } - float vitalityFactor = 1 - AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) / 100; + float vitalityFactor = 1 - AIObjectiveRescueAll.GetVitalityFactor(Target) / 100; float devotion = CumulatedDevotion / 100; Priority = MathHelper.Lerp(0, AIObjectiveManager.EmergencyObjectivePriority, MathHelper.Clamp(devotion + (vitalityFactor * distanceFactor * PriorityModifier), 0, 1)); return Priority; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs index 97ce2855f..4bfadb3f5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs @@ -34,24 +34,29 @@ namespace Barotrauma protected override bool Filter(Character target) { - if (!IsValidTarget(target, character, requireTreatableAfflictions: false)) { return false; } - if (GetTreatableAfflictions(target).Any()) + if (!IsValidTarget(target, character, out bool ignoredasMinorWounds)) { - return true; - } - else - { - //the target might be at a low enough health to be considered a valid target, - //but if all afflictions are below treatment thresholds, the bot won't (and shouldn't) treat them - // -> make the bot speak to make it clear the bot intentionally ignores very minor injuries - if (!charactersWithMinorInjuries.Contains(character)) + if (ignoredasMinorWounds) { - character.Speak(TextManager.GetWithVariable("dialogignoreminorinjuries", "[targetname]", target.Name).Value, - null, 1.0f, $"notreatableafflictions{target.Name}".ToIdentifier(), 10.0f); - charactersWithMinorInjuries.Add(character); + //the target might be at a low enough health to be considered a valid target, + //but if all afflictions are below treatment thresholds, the bot won't (and shouldn't) treat them + // -> make the bot speak to make it clear the bot intentionally ignores very minor injuries + if (character.IsOnPlayerTeam && target != character && !charactersWithMinorInjuries.Contains(target)) + { + // But only speak about targets when we are not already actively treating, in which case we should be speaking about the current target. + if (objectiveManager.GetFirstActiveObjective() == null) + { + charactersWithMinorInjuries.Add(target); + character.Speak(TextManager.GetWithVariable("dialogignoreminorinjuries", "[targetname]", target.Name).Value, + delay: 1.0f, + identifier: $"notreatableafflictions{target.Name}".ToIdentifier(), + minDurationBetweenSimilar: 10.0f); + } + } } return false; - } + } + return true; } protected override IEnumerable GetList() => Character.CharacterList; @@ -103,12 +108,13 @@ namespace Barotrauma return Math.Clamp(vitality, 0, 100); } - public static IEnumerable GetTreatableAfflictions(Character character, bool ignoreTreatmentThreshold = false) + public static IEnumerable GetTreatableAfflictions(Character character, bool ignoreTreatmentThreshold) { var allAfflictions = character.CharacterHealth.GetAllAfflictions(); foreach (Affliction affliction in allAfflictions) { if (affliction.Prefab.IsBuff) { continue; } + if (!affliction.Prefab.HasTreatments) { continue; } if (!ignoreTreatmentThreshold) { //other afflictions of the same type increase the "treatability" @@ -116,7 +122,6 @@ namespace Barotrauma float totalAfflictionStrength = character.CharacterHealth.GetTotalAdjustedAfflictionStrength(affliction); if (totalAfflictionStrength < affliction.Prefab.TreatmentThreshold) { continue; } } - if (affliction.Prefab.TreatmentSuitability.None(kvp => kvp.Value > 0)) { continue; } if (allAfflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Identifier))) { continue; } yield return affliction; } @@ -128,39 +133,50 @@ namespace Barotrauma protected override void OnObjectiveCompleted(AIObjective objective, Character target) => HumanAIController.RemoveTargets(character, target); - public static bool IsValidTarget(Character target, Character character, bool requireTreatableAfflictions = true) + public static bool IsValidTarget(Character target, Character character, out bool ignoredAsMinorWounds) { + ignoredAsMinorWounds = false; if (target == null || target.IsDead || target.Removed) { return false; } if (target.IsInstigator) { return false; } if (target.IsPet) { return false; } if (!HumanAIController.IsFriendly(character, target, onlySameTeam: true)) { return false; } + bool isBelowTreatmentThreshold; + float vitalityFactor; if (character.AIController is HumanAIController humanAI) { - if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target)) - { - return false; - } - if (!humanAI.ObjectiveManager.HasOrder()) - { - if (!character.IsMedic && target != character) - { - // Don't allow to treat others autonomously, unless we are a medic - return false; - } - // Ignore unsafe hulls, unless ordered - if (humanAI.UnsafeHulls.Contains(target.CurrentHull)) - { - return false; - } - } - if (requireTreatableAfflictions && GetTreatableAfflictions(target).None()) - { - return false; - } + if (!IsValidTargetForAI(target, humanAI)) { return false; } + vitalityFactor = GetVitalityFactor(target); + isBelowTreatmentThreshold = vitalityFactor < GetVitalityThreshold(humanAI.ObjectiveManager, character, target); } else { - if (GetVitalityFactor(target) >= vitalityThreshold) { return false; } + vitalityFactor = GetVitalityFactor(target); + isBelowTreatmentThreshold = vitalityFactor < vitalityThreshold; + } + bool hasTreatableAfflictions = GetTreatableAfflictions(target, ignoreTreatmentThreshold: false).Any(); + bool isValidTarget = isBelowTreatmentThreshold && hasTreatableAfflictions; + if (!isValidTarget) + { + ignoredAsMinorWounds = hasTreatableAfflictions || vitalityFactor < 100; + } + return isValidTarget; + } + + private static bool IsValidTargetForAI(Character target, HumanAIController humanAI) + { + Character character = humanAI.Character; + if (!humanAI.ObjectiveManager.HasOrder()) + { + if (!character.IsMedic && target != character) + { + // Don't allow to treat others autonomously, unless we are a medic + return false; + } + // Ignore unsafe hulls, unless ordered + if (humanAI.UnsafeHulls.Contains(target.CurrentHull)) + { + return false; + } } if (character.Submarine != null) { @@ -189,11 +205,5 @@ namespace Barotrauma } return character.GetDamageDoneByAttacker(target) <= 0; } - - public override void Reset() - { - base.Reset(); - charactersWithMinorInjuries.Clear(); - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs index 14fc0d495..ee7109ff5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs @@ -20,7 +20,7 @@ namespace Barotrauma ReturnTarget = GetReturnTarget(Submarine.MainSubs) ?? GetReturnTarget(Submarine.Loaded); if (ReturnTarget == null) { - DebugConsole.ThrowError("Error with a Return objective: no suitable return target found"); + DebugConsole.AddSafeError("Error with a Return objective: no suitable return target found"); Abandon = true; } @@ -47,8 +47,7 @@ namespace Barotrauma } else { - // TODO: Consider if this needs to be addressed - Priority = 0; + Priority = AIObjectiveManager.LowestOrderPriority - 1; } return Priority; } @@ -91,7 +90,7 @@ namespace Barotrauma targetHull = d.Item.CurrentHull; break; } - if (targetHull != null && !targetHull.IsTaggedAirlock()) + if (targetHull != null && !targetHull.IsAirlock) { // Target the closest airlock float closestDist = 0; @@ -99,7 +98,7 @@ namespace Barotrauma foreach (Hull hull in Hull.HullList) { if (hull.Submarine != targetHull.Submarine) { continue; } - if (!hull.IsTaggedAirlock()) { continue; } + if (!hull.IsAirlock) { continue; } float dist = Vector2.DistanceSquared(targetHull.Position, hull.Position); if (airlock == null || closestDist <= 0 || dist < closestDist) { @@ -146,7 +145,7 @@ namespace Barotrauma bool targetIsAirlock = false; foreach (var hull in ReturnTarget.GetHulls(false)) { - bool hullIsAirlock = hull.IsTaggedAirlock(); + bool hullIsAirlock = hull.IsAirlock; if(hullIsAirlock || (!targetIsAirlock && hull.LeadsOutside(character))) { float distanceSquared = Vector2.DistanceSquared(character.WorldPosition, hull.WorldPosition); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index 0b48320e7..21c5aeb35 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -85,6 +85,12 @@ namespace Barotrauma public readonly bool TargetAllCharacters; public bool IsReport => TargetAllCharacters && !MustSetTarget; + public bool IsVisibleAsReportButton => + IsReport && !Hidden && SymbolSprite != null && + (!TraitorModeOnly || GameMain.GameSession is { TraitorsEnabled: true }); + + public bool TraitorModeOnly; + public bool IsDismissal => Identifier == DismissalIdentifier; public readonly float FadeOutTime; @@ -172,6 +178,7 @@ namespace Barotrauma ControllerTags = orderElement.GetAttributeIdentifierArray("controllertags", Array.Empty()).ToImmutableArray(); TargetAllCharacters = orderElement.GetAttributeBool("targetallcharacters", false); AppropriateJobs = orderElement.GetAttributeIdentifierArray("appropriatejobs", Array.Empty()).ToImmutableArray(); + TraitorModeOnly = orderElement.GetAttributeBool("TraitorModeOnly", false); PreferredJobs = orderElement.GetAttributeIdentifierArray("preferredjobs", Array.Empty()).ToImmutableArray(); Options = orderElement.GetAttributeIdentifierArray("options", Array.Empty()).ToImmutableArray(); HiddenOptions = orderElement.GetAttributeIdentifierArray("hiddenoptions", Array.Empty()).ToImmutableArray(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs index 4f64dac4d..d5f90c438 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs @@ -34,7 +34,23 @@ namespace Barotrauma set { happiness = MathHelper.Clamp(value, 0.0f, MaxHappiness); } } + /// + /// At which point is the pet considered "unhappy" (playing unhappy sounds and showing the icon) + /// + public float UnhappyThreshold { get; set; } + + /// + /// At which point is the pet considered "happy" (playing happy sounds and showing the icon) + /// + public float HappyThreshold { get; set; } + public float MaxHappiness { get; set; } + + + /// + /// At which point is the pet considered "hungry" (playing unhappy sounds and showing the icon) + /// + public float HungryThreshold { get; set; } public float MaxHunger { get; set; } public float HappinessDecreaseRate { get; set; } @@ -43,7 +59,7 @@ namespace Barotrauma public float PlayForce { get; set; } public float PlayTimer { get; set; } - private float? unstunY { get; set; } + private float? UnstunY { get; set; } public EnemyAIController AIController { get; private set; } = null; @@ -136,7 +152,7 @@ namespace Barotrauma if (aggregate >= r && Items[i].Prefab != null) { GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetProducedItem:" + pet.AIController.Character.SpeciesName + ":" + Items[i].Prefab.Identifier); - Entity.Spawner.AddItemToSpawnQueue(Items[i].Prefab, pet.AIController.Character.WorldPosition); + Entity.Spawner?.AddItemToSpawnQueue(Items[i].Prefab, pet.AIController.Character.WorldPosition); break; } } @@ -164,14 +180,18 @@ namespace Barotrauma AIController = aiController; AIController.Character.CanBeDragged = true; - MaxHappiness = element.GetAttributeFloat("maxhappiness", 100.0f); - MaxHunger = element.GetAttributeFloat("maxhunger", 100.0f); + MaxHappiness = element.GetAttributeFloat(nameof(MaxHappiness), 100.0f); + UnhappyThreshold = element.GetAttributeFloat(nameof(UnhappyThreshold), MaxHappiness * 0.25f); + HappyThreshold = element.GetAttributeFloat(nameof(HappyThreshold), MaxHappiness * 0.8f); + + MaxHunger = element.GetAttributeFloat(nameof(MaxHunger), 100.0f); + HungryThreshold = element.GetAttributeFloat(nameof(HungryThreshold), MaxHunger * 0.5f); Happiness = MaxHappiness * 0.5f; Hunger = MaxHunger * 0.5f; - HappinessDecreaseRate = element.GetAttributeFloat("happinessdecreaserate", 0.1f); - HungerIncreaseRate = element.GetAttributeFloat("hungerincreaserate", 0.25f); + HappinessDecreaseRate = element.GetAttributeFloat(nameof(HappinessDecreaseRate), 0.1f); + HungerIncreaseRate = element.GetAttributeFloat(nameof(HungerIncreaseRate), 0.25f); PlayForce = element.GetAttributeFloat("playforce", 15.0f); @@ -208,9 +228,9 @@ namespace Barotrauma public StatusIndicatorType GetCurrentStatusIndicatorType() { - if (Hunger > MaxHunger * 0.5f) { return StatusIndicatorType.Hungry; } - if (Happiness > MaxHappiness * 0.8f) { return StatusIndicatorType.Happy; } - if (Happiness < MaxHappiness * 0.25f) { return StatusIndicatorType.Sad; } + if (Hunger > HungryThreshold) { return StatusIndicatorType.Hungry; } + if (Happiness > HappyThreshold) { return StatusIndicatorType.Happy; } + if (Happiness < UnhappyThreshold) { return StatusIndicatorType.Sad; } return StatusIndicatorType.None; } @@ -264,12 +284,12 @@ namespace Barotrauma public void Play(Character player) { if (PlayTimer > 0.0f) { return; } - if (Owner == null) { Owner = player; } + Owner ??= player; PlayTimer = 5.0f; AIController.Character.IsRagdolled = true; Happiness += 10.0f; AIController.Character.AnimController.MainLimb.body.LinearVelocity += new Vector2(0, PlayForce); - unstunY = AIController.Character.SimPosition.Y; + UnstunY = AIController.Character.SimPosition.Y; #if CLIENT AIController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.9f); #endif @@ -297,16 +317,16 @@ namespace Barotrauma var character = AIController.Character; if (character?.Removed ?? true || character.IsDead) { return; } - if (unstunY.HasValue) + if (UnstunY.HasValue) { if (PlayTimer > 4.0f) { float extent = character.AnimController.MainLimb.body.GetMaxExtent(); - if (character.SimPosition.Y < (unstunY.Value + extent * 3.0f) && + if (character.SimPosition.Y < (UnstunY.Value + extent * 3.0f) && character.AnimController.MainLimb.body.LinearVelocity.Y < 0.0f) { character.IsRagdolled = false; - unstunY = null; + UnstunY = null; } else { @@ -316,7 +336,7 @@ namespace Barotrauma else { character.IsRagdolled = false; - unstunY = null; + UnstunY = null; } } @@ -362,15 +382,11 @@ namespace Barotrauma { character.CharacterHealth.ApplyAffliction(character.AnimController.MainLimb, new Affliction(AfflictionPrefab.InternalDamage, 8.0f * deltaTime)); } - else if (Hunger < MaxHunger * 0.1f) - { - character.CharacterHealth.ReduceAllAfflictionsOnAllLimbs(8.0f * deltaTime); - } if (character.SelectedBy != null) { character.IsRagdolled = true; - unstunY = character.SimPosition.Y; + UnstunY = character.SimPosition.Y; } for (int i = 0; i < itemsToProduce.Count; i++) @@ -435,37 +451,34 @@ namespace Barotrauma spawnPoint ??= WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine?.Info.Type == SubmarineType.Player).GetRandomUnsynced(); spawnPos = spawnPoint?.WorldPosition ?? Submarine.MainSub.WorldPosition; } - var pet = Character.Create(speciesName, spawnPos, seed, spawnInitialItems: false); - var petBehavior = (pet?.AIController as EnemyAIController)?.PetBehavior; - if (petBehavior != null) + + var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName.ToIdentifier()); + if (characterPrefab == null) { - petBehavior.Owner = owner; - var petBehaviorElement = subElement.Element("petbehavior"); - if (petBehaviorElement != null) + DebugConsole.ThrowError($"Failed to load the pet \"{speciesName}\". Character prefab not found."); + continue; + } + var pet = Character.Create(characterPrefab, spawnPos, seed, spawnInitialItems: false); + if (pet != null) + { + var petBehavior = (pet.AIController as EnemyAIController)?.PetBehavior; + if (petBehavior != null) { - petBehavior.Hunger = petBehaviorElement.GetAttributeFloat("hunger", 50.0f); - petBehavior.Happiness = petBehaviorElement.GetAttributeFloat("happiness", 50.0f); + petBehavior.Owner = owner; + var petBehaviorElement = subElement.Element("petbehavior"); + if (petBehaviorElement != null) + { + petBehavior.Hunger = petBehaviorElement.GetAttributeFloat("hunger", 50.0f); + petBehavior.Happiness = petBehaviorElement.GetAttributeFloat("happiness", 50.0f); + } } } - var inventoryElement = subElement.Element("inventory"); if (inventoryElement != null) { pet.SpawnInventoryItems(pet.Inventory, inventoryElement.FromPackage(null)); - } + } } } - - public void ServerWrite(IWriteMessage msg) - { - msg.WriteRangedSingle(Happiness, 0.0f, MaxHappiness, 8); - msg.WriteRangedSingle(Hunger, 0.0f, MaxHunger, 8); - } - - public void ClientRead(IReadMessage msg) - { - Happiness = msg.ReadRangedSingle(0.0f, MaxHappiness, 8); - Hunger = msg.ReadRangedSingle(0.0f, MaxHunger, 8); - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs index ba29bdbb4..69165bcaa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs @@ -24,7 +24,7 @@ namespace Barotrauma { if (TargetItemComponent is Turret turret) { - if (!turret.CheckTurretAngle(entity.WorldPosition)) + if (!turret.IsWithinAimingRadius(entity.WorldPosition)) { importance *= 0.1f; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs index 2ac885b14..99d9e2fd1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs @@ -350,20 +350,20 @@ namespace Barotrauma ShipIssueWorkers.Clear(); - if (CommandedSubmarine.GetItems(false).Find(i => i.HasTag("reactor") && !i.NonInteractable)?.GetComponent() is Reactor reactor) + if (CommandedSubmarine.GetItems(false).Find(i => i.HasTag(Tags.Reactor) && !i.NonInteractable)?.GetComponent() is Reactor reactor) { var order = new Order(OrderPrefab.Prefabs["operatereactor"], "powerup".ToIdentifier(), reactor.Item, reactor); ShipIssueWorkers.Add(new ShipIssueWorkerPowerUpReactor(this, order)); } - if (CommandedSubmarine.GetItems(false).Find(i => i.HasTag("navterminal") && !i.NonInteractable) is Item nav && nav.GetComponent() is Steering steeringComponent) + if (CommandedSubmarine.GetItems(false).Find(i => i.HasTag(Tags.NavTerminal) && !i.NonInteractable) is Item nav && nav.GetComponent() is Steering steeringComponent) { steering = steeringComponent; var order = new Order(OrderPrefab.Prefabs["steer"], "navigatetactical".ToIdentifier(), nav, steeringComponent); ShipIssueWorkers.Add(new ShipIssueWorkerSteer(this, order)); } - foreach (Item item in CommandedSubmarine.GetItems(true).FindAll(i => i.HasTag("turret") && !i.HasTag("hardpoint"))) + foreach (Item item in CommandedSubmarine.GetItems(true).FindAll(i => i.HasTag(Tags.Turret) && !i.HasTag(Tags.Hardpoint))) { var order = new Order(OrderPrefab.Prefabs["operateweapons"], item, item.GetComponent()); ShipIssueWorkers.Add(new ShipIssueWorkerOperateWeapons(this, order)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs index 34092a282..a2c281159 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs @@ -80,7 +80,7 @@ namespace Barotrauma { foreach (var turret in turrets) { - turret.UpdateAutoOperate(deltaTime, friendlyTag); + turret.UpdateAutoOperate(deltaTime, ignorePower: true, friendlyTag); } } } @@ -107,7 +107,7 @@ namespace Barotrauma private static IEnumerable GetThalamusEntities(Submarine wreck, Identifier tag) where T : MapEntity => GetThalamusEntities(wreck, tag).Where(e => e is T).Select(e => e as T); - private static IEnumerable GetThalamusEntities(Submarine wreck, Identifier tag) => MapEntity.mapEntityList.Where(e => e.Submarine == wreck && e.Prefab != null && IsThalamus(e.Prefab, tag)); + private static IEnumerable GetThalamusEntities(Submarine wreck, Identifier tag) => MapEntity.MapEntityList.Where(e => e.Submarine == wreck && e.Prefab != null && IsThalamus(e.Prefab, tag)); private static bool IsThalamus(MapEntityPrefab entityPrefab, Identifier tag) => entityPrefab.HasSubCategory("thalamus") || entityPrefab.Tags.Contains(tag); @@ -273,6 +273,10 @@ namespace Barotrauma } } destroyedOrgans.ForEach(o => spawnOrgans.Remove(o)); + if (!IsClient) + { + if (!initialCellsSpawned) { SpawnInitialCells(); } + } bool isSomeoneNearby = false; float minDist = Sonar.DefaultSonarRange * 2.0f; #if SERVER @@ -322,7 +326,6 @@ namespace Barotrauma OperateTurrets(deltaTime, Config.Entity); if (!IsClient) { - if (!initialCellsSpawned) { SpawnInitialCells(); } UpdateReinforcements(deltaTime); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs index 9bb81acc8..69a1f4e9c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs @@ -27,7 +27,7 @@ namespace Barotrauma public string Brain { get; private set; } [Serialize("", IsPropertySaveable.No)] - public string Spawner { get; private set; } + public Identifier Spawner { get; private set; } [Serialize("", IsPropertySaveable.No)] public string BrainRoomBackground { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index 1606de078..47e3ec73d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -286,17 +286,32 @@ namespace Barotrauma public void UpdateUseItem(bool allowMovement, Vector2 handWorldPos) { - useItemTimer = 0.5f; + useItemTimer = 0.05f; StartUsingItem(); if (!allowMovement) { TargetMovement = Vector2.Zero; TargetDir = handWorldPos.X > character.WorldPosition.X ? Direction.Right : Direction.Left; - float sqrDist = Vector2.DistanceSquared(character.WorldPosition, handWorldPos); - if (sqrDist > MathUtils.Pow(ConvertUnits.ToDisplayUnits(upperArmLength + forearmLength), 2)) + if (InWater) { - TargetMovement = Vector2.Normalize(handWorldPos - character.WorldPosition) * GetCurrentSpeed(false) * Math.Max(character.SpeedMultiplier, 1); + float sqrDist = Vector2.DistanceSquared(character.WorldPosition, handWorldPos); + if (sqrDist > MathUtils.Pow(ConvertUnits.ToDisplayUnits(upperArmLength + forearmLength), 2)) + { + TargetMovement = GetTargetMovement(Vector2.Normalize(handWorldPos - character.WorldPosition)); + } + } + else + { + float distX = Math.Abs(handWorldPos.X - character.WorldPosition.X); + if (distX > ConvertUnits.ToDisplayUnits(upperArmLength + forearmLength)) + { + TargetMovement = GetTargetMovement(Vector2.UnitX * Math.Sign(handWorldPos.X - character.WorldPosition.X)); + } + } + Vector2 GetTargetMovement(Vector2 dir) + { + return dir * GetCurrentSpeed(false) * Math.Max(character.SpeedMultiplier, 1); } } @@ -308,6 +323,15 @@ namespace Barotrauma handSimPos -= character.Submarine.SimPosition; } + Vector2 refPos = rightShoulder?.WorldAnchorA ?? leftShoulder?.WorldAnchorA ?? MainLimb.SimPosition; + Vector2 diff = handSimPos - refPos; + float dist = diff.Length(); + float maxDist = ArmLength * 0.9f; + if (dist > maxDist) + { + handSimPos = refPos + diff / dist * maxDist; + } + var leftHand = GetLimb(LimbType.LeftHand); if (leftHand != null) { @@ -323,6 +347,16 @@ namespace Barotrauma rightHand.PullJointEnabled = true; rightHand.PullJointWorldAnchorB = handSimPos; } + + //make the character crouch if using an item some distance below them (= on the floor) + if (!inWater && + character.WorldPosition.Y - handWorldPos.Y > ConvertUnits.ToDisplayUnits(CurrentGroundedParams.TorsoPosition) / 4 && + this is HumanoidAnimController humanoidAnimController) + { + humanoidAnimController.Crouching = true; + humanoidAnimController.ForceSelectAnimationType = AnimationType.Crouch; + character.SetInput(InputType.Crouch, hit: false, held: true); + } } public void Grab(Vector2 rightHandPos, Vector2 leftHandPos) @@ -352,13 +386,8 @@ namespace Barotrauma //calculate the handle positions Matrix itemTransfrom = Matrix.CreateRotationZ(item.body.Rotation); - float horizontalOffset = ConvertUnits.ToSimUnits((item.Sprite.size.X / 2 - item.Sprite.Origin.X) * item.Scale); - - //handlePos[0] = ConvertUnits.ToSimUnits(new Vector2(-45,25) * 0.5f); - //handlePos[1] = ConvertUnits.ToSimUnits(new Vector2(-65,30) * 0.5f); - - transformedHandlePos[0] = Vector2.Transform(new Vector2(handlePos[0].X + horizontalOffset, handlePos[0].Y), itemTransfrom); - transformedHandlePos[1] = Vector2.Transform(new Vector2(handlePos[1].X + horizontalOffset, handlePos[1].Y), itemTransfrom); + transformedHandlePos[0] = Vector2.Transform(handlePos[0], itemTransfrom); + transformedHandlePos[1] = Vector2.Transform(handlePos[1], itemTransfrom); Limb torso = GetLimb(LimbType.Torso) ?? MainLimb; Limb leftHand = GetLimb(LimbType.LeftHand); @@ -385,7 +414,9 @@ namespace Barotrauma if (aim && !isClimbing && !usingController && character.Stun <= 0.0f && itemPos != Vector2.Zero && !character.IsIncapacitated) { Vector2 mousePos = ConvertUnits.ToSimUnits(character.SmoothedCursorPosition); - Vector2 diff = holdable.Aimable ? (mousePos - AimSourceSimPos) * Dir : Vector2.UnitX; + Vector2 diff = holdable.Aimable ? + (mousePos - AimSourceSimPos) * Dir : + MathUtils.RotatePoint(Vector2.UnitX, torsoRotation); holdAngle = MathUtils.VectorToAngle(new Vector2(diff.X, diff.Y * Dir)) - torsoRotation * Dir; holdAngle += GetAimWobble(rightHand, leftHand, item); itemAngle = torsoRotation + holdAngle * Dir; @@ -480,6 +511,16 @@ namespace Barotrauma return; } + float targetAngle = MathUtils.WrapAngleTwoPi(itemAngle + itemAngleRelativeToHoldAngle * Dir); + float currentRotation = MathUtils.WrapAngleTwoPi(item.body.Rotation); + float itemRotation = MathHelper.SmoothStep(currentRotation, targetAngle, deltaTime * 25); + if (previousDirection != dir || Math.Abs(targetAngle - currentRotation) > MathHelper.Pi) + { + itemRotation = targetAngle; + } + item.SetTransform(currItemPos, itemRotation, setPrevTransform: false); + previousDirection = dir; + if (holdable.Pusher != null) { if (character.Stun > 0.0f || character.IsIncapacitated) @@ -497,24 +538,11 @@ namespace Barotrauma else { holdable.Pusher.TargetPosition = currItemPos; - holdable.Pusher.TargetRotation = holdAngle * Dir; - + holdable.Pusher.TargetRotation = itemRotation; holdable.Pusher.MoveToTargetPosition(true); - - currItemPos = holdable.Pusher.SimPosition; - itemAngle = holdable.Pusher.Rotation; } } } - float targetAngle = MathUtils.WrapAngleTwoPi(itemAngle + itemAngleRelativeToHoldAngle * Dir); - float currentRotation = MathUtils.WrapAngleTwoPi(item.body.Rotation); - float itemRotation = MathHelper.SmoothStep(currentRotation, targetAngle, deltaTime * 25); - if (previousDirection != dir || Math.Abs(targetAngle - currentRotation) > MathHelper.Pi) - { - itemRotation = targetAngle; - } - item.SetTransform(currItemPos, itemRotation, setPrevTransform: false); - previousDirection = dir; if (!isClimbing && !character.IsIncapacitated && itemPos != Vector2.Zero && (aim || !holdable.UseHandRotationForHoldAngle)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 6682b2d50..91924e625 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -144,7 +144,7 @@ namespace Barotrauma set { HumanSwimFastParams = value as HumanSwimFastParams; } } - public bool Crouching; + public bool Crouching { get; set; } private float upperLegLength = 0.0f, lowerLegLength = 0.0f; @@ -197,7 +197,7 @@ namespace Barotrauma public HumanoidAnimController(Character character, string seed, HumanRagdollParams ragdollParams = null) : base(character, seed, ragdollParams) { // TODO: load from the character info file? - movementLerp = RagdollParams.MainElement.GetAttributeFloat("movementlerp", 0.4f); + movementLerp = RagdollParams?.MainElement?.GetAttributeFloat("movementlerp", 0.4f) ?? 0f; } public override void Recreate(RagdollParams ragdollParams = null) @@ -243,19 +243,14 @@ namespace Barotrauma if (MainLimb == null) { return; } levitatingCollider = !IsHanging; - ColliderIndex = Crouching && !swimming ? 1 : 0; if ((character.SelectedItem?.GetComponent()?.ControlCharacterPose ?? false) || (character.SelectedSecondaryItem?.GetComponent()?.ControlCharacterPose ?? false) || character.SelectedSecondaryItem?.GetComponent() != null || (ForceSelectAnimationType != AnimationType.Crouch && ForceSelectAnimationType != AnimationType.NotDefined)) { Crouching = false; - ColliderIndex = 0; - } - else if (!Crouching && ColliderIndex == 1) - { - Crouching = true; } + ColliderIndex = Crouching && !swimming ? 1 : 0; //stun (= disable the animations) if the ragdoll receives a large enough impact if (strongestImpact > 0.0f) @@ -417,6 +412,22 @@ namespace Barotrauma swimming = inWater; swimmingStateLockTimer = 0.5f; } + if (character.SelectedItem?.Prefab is { GrabWhenSelected: true } && + character.SelectedItem.ParentInventory == null && + character.SelectedItem.body is not { Enabled: true } && + character.SelectedItem.GetComponent()?.CurrentFixer != character) + { + bool moving = character.IsKeyDown(InputType.Left) || character.IsKeyDown(InputType.Right); + moving |= (character.InWater || character.IsClimbing) && (character.IsKeyDown(InputType.Up) || character.IsKeyDown(InputType.Down)); + if (!moving) + { + Vector2 handPos = character.SelectedItem.WorldPosition - Vector2.UnitY * ConvertUnits.ToDisplayUnits(ArmLength / 2); + handPos.Y = Math.Max(handPos.Y, character.SelectedItem.WorldRect.Y - character.SelectedItem.WorldRect.Height); + UpdateUseItem( + allowMovement: false, + handPos); + } + } if (swimming) { UpdateSwimming(); @@ -616,7 +627,7 @@ namespace Barotrauma if (TorsoAngle.HasValue && !torso.Disabled) { float torsoAngle = TorsoAngle.Value; - float herpesStrength = character.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.SpaceHerpesType); + float herpesStrength = character.CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.SpaceHerpesType); if (Crouching && !movingHorizontally && !Aiming) { torsoAngle -= HumanCrouchParams.ExtraTorsoAngleWhenStationary; } torsoAngle -= herpesStrength / 150.0f; torso.body.SmoothRotate(torsoAngle * Dir, currentGroundedParams.TorsoTorque); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index cc28d59c1..c9ac1f88b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -744,6 +744,13 @@ namespace Barotrauma { if (character.DisableImpactDamageTimer > 0.0f) { return; } + if (f2.Body?.UserData is Item) + { + //no impact damage from items + //items that can impact characters (melee weapons, projectiles) should handle the damage themselves + return; + } + Vector2 normal = localNormal; float impact = Vector2.Dot(velocity, -normal); if (f1.Body == Collider.FarseerBody || !Collider.Enabled) @@ -753,16 +760,18 @@ namespace Barotrauma if (isNotRemote) { - if (impact > ImpactTolerance) + float impactTolerance = ImpactTolerance; + if (character.Stun > 0.0f) { impactTolerance *= 0.5f; } + if (impact > impactTolerance) { impactPos = ConvertUnits.ToDisplayUnits(impactPos); - if (character.Submarine != null) impactPos += character.Submarine.Position; + if (character.Submarine != null) { impactPos += character.Submarine.Position; } - float impactDamage = Math.Min((impact - ImpactTolerance) * ImpactDamageMultiplayer, character.MaxVitality * MaxImpactDamage); + float impactDamage = GetImpactDamage(impact, impactTolerance); character.LastDamageSource = null; character.AddDamage(impactPos, AfflictionPrefab.ImpactDamage.Instantiate(impactDamage).ToEnumerable(), 0.0f, true); - strongestImpact = Math.Max(strongestImpact, impact - ImpactTolerance); + strongestImpact = Math.Max(strongestImpact, impact - impactTolerance); character.ApplyStatusEffects(ActionType.OnImpact, 1.0f); //briefly disable impact damage //otherwise the character will take damage multiple times when for example falling, @@ -776,6 +785,12 @@ namespace Barotrauma ImpactProjSpecific(impact, f1.Body); } + public float GetImpactDamage(float impact, float? impactTolerance = null) + { + float tolerance = impactTolerance ?? ImpactTolerance; + return Math.Min((impact - tolerance) * ImpactDamageMultiplayer, character.MaxVitality * MaxImpactDamage); + } + private readonly List connectedLimbs = new List(); private readonly List checkedJoints = new List(); public bool SeverLimbJoint(LimbJoint limbJoint) @@ -1023,7 +1038,12 @@ namespace Barotrauma CurrentHull = newHull; character.Submarine = currentHull?.Submarine; - character.AttachedProjectiles.ForEach(p => p?.Item?.UpdateTransform()); + foreach (var attachedProjectile in character.AttachedProjectiles) + { + attachedProjectile.Item.CurrentHull = currentHull; + attachedProjectile.Item.Submarine = character.Submarine; + attachedProjectile.Item.UpdateTransform(); + } } private void PreventOutsideCollision() @@ -1323,6 +1343,11 @@ namespace Barotrauma if (Collider.LinearVelocity == Vector2.Zero) { character.IsRagdolled = true; + if (character.IsBot) + { + // Seems to work without this on player controlled characters -> not sure if we should call it always or just for the bots. + character.SetInput(InputType.Ragdoll, hit: false, held: true); + } } } } @@ -1781,12 +1806,6 @@ namespace Barotrauma Character.Latchers.ForEachMod(l => l?.DeattachFromBody(reset: true)); Character.Latchers.Clear(); - if (detachProjectiles) - { - character.AttachedProjectiles.ForEachMod(p => p?.Unstick()); - character.AttachedProjectiles.Clear(); - } - Vector2 limbMoveAmount = forceMainLimbToCollider ? simPosition - MainLimb.SimPosition : simPosition - Collider.SimPosition; if (lerp) { @@ -1823,7 +1842,7 @@ namespace Barotrauma protected void TrySetLimbPosition(Limb limb, Vector2 original, Vector2 simPosition, float rotation, bool lerp = false, bool ignorePlatforms = true) { Vector2 movePos = simPosition; - + Vector2 prevPosition = limb.body.SimPosition; if (Vector2.DistanceSquared(original, simPosition) > 0.0001f) { Category collisionCategory = Physics.CollisionWall | Physics.CollisionLevel; @@ -1851,6 +1870,16 @@ namespace Barotrauma limb.PullJointWorldAnchorB = limb.PullJointWorldAnchorA; limb.PullJointEnabled = false; } + foreach (var attachedProjectile in character.AttachedProjectiles) + { + if (attachedProjectile.IsAttachedTo(limb.body)) + { + attachedProjectile.Item.SetTransform( + attachedProjectile.Item.SimPosition + (movePos - prevPosition), + attachedProjectile.Item.body.Rotation, + findNewHull: false); + } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index 5991498e5..f3397c089 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -210,8 +210,8 @@ namespace Barotrauma [Serialize(5f, IsPropertySaveable.Yes, description: "How fast the held weapon is swayed back and forth while aiming. Only affects monsters using ranged weapons (items)."), Editable] public float SwayFrequency { get; set; } - [Serialize(0.0f, IsPropertySaveable.No, description: "Legacy support. Use Afflictions.")] - public float Stun { get; private set; } + [Serialize(0.0f, IsPropertySaveable.No, description: "Legacy functionality. Behaves otherwise the same as stuns defined as afflictions, but explosions only apply the stun once instead of dividing it between the limbs.")] + public float Stun { get; set; } [Serialize(false, IsPropertySaveable.Yes, description: "Can damage only Humans."), Editable] public bool OnlyHumans { get; set; } @@ -434,13 +434,7 @@ namespace Barotrauma } break; case "conditional": - foreach (XAttribute attribute in subElement.Attributes()) - { - if (PropertyConditional.IsValid(attribute)) - { - Conditionals.Add(new PropertyConditional(attribute)); - } - } + Conditionals.AddRange(PropertyConditional.FromXElement(subElement)); break; } } @@ -544,8 +538,7 @@ namespace Barotrauma } if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs)) { - // TODO: do we need the conversion to list here? It generates garbage. - var targets = targetCharacter.AnimController.Limbs.Cast().ToList(); + var targets = targetCharacter.AnimController.Limbs; if (additionalEffectType != ActionType.OnEating) { effect.Apply(conditionalEffectType, deltaTime, targetCharacter, targets); @@ -612,7 +605,10 @@ namespace Barotrauma float penetration = Penetration; - float? penetrationValue = SourceItem?.GetComponent()?.Penetration; + RangedWeapon weapon = + SourceItem?.GetComponent() ?? + SourceItem?.GetComponent()?.Launcher?.GetComponent(); + float? penetrationValue = weapon?.Penetration; if (penetrationValue.HasValue) { penetration += penetrationValue.Value; @@ -646,8 +642,7 @@ namespace Barotrauma } if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs)) { - // TODO: do we need the conversion to list here? It generates garbage. - var targets = targetLimb.character.AnimController.Limbs.Cast().ToList(); + var targets = targetLimb.character.AnimController.Limbs; effect.Apply(conditionalEffectType, deltaTime, targetLimb.character, targets); effect.Apply(ActionType.OnUse, deltaTime, targetLimb.character, targets); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 6042aecee..41847f24f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -65,6 +65,13 @@ namespace Barotrauma } UpdateLimbLightSource(limb); } + foreach (var item in HeldItems) + { + if (item.body != null) + { + item.body.Enabled = enabled; + } + } AnimController.Collider.Enabled = value; } } @@ -335,7 +342,7 @@ namespace Barotrauma public bool IsInstigator => CombatAction != null && CombatAction.IsInstigator; public CombatAction CombatAction; - public AnimController AnimController; + public readonly AnimController AnimController; private Vector2 cursorPosition; @@ -386,6 +393,8 @@ namespace Barotrauma public bool IsMachine => Params.IsMachine; public bool IsHusk => Params.Husk; + public bool IsDisguisedAsHusk => CharacterHealth.GetAfflictionStrengthByType("disguiseashusk".ToIdentifier()) > 0; + public bool IsHuskInfected => CharacterHealth.GetActiveAfflictionTags().Contains("huskinfected".ToIdentifier()); public bool IsMale => info?.IsMale ?? false; @@ -781,7 +790,7 @@ namespace Barotrauma get { if (IsUnconscious) { return true; } - return CharacterHealth.GetAllAfflictions().Any(a => a.Prefab.AfflictionType == AfflictionPrefab.ParalysisType && a.Strength >= a.Prefab.MaxStrength); + return CharacterHealth.IsParalyzed; } } @@ -792,7 +801,7 @@ namespace Barotrauma public bool IsArrested { - get { return IsHuman && HasEquippedItem("handlocker"); } + get { return IsHuman && HasEquippedItem(Tags.HandLockerItem); } } public bool IsPet @@ -852,6 +861,7 @@ namespace Barotrauma public bool IsLatched => AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached; public float EmpVulnerability => Params.Health.EmpVulnerability; public float PoisonVulnerability => Params.Health.PoisonVulnerability; + public bool IsFlipped => AnimController.IsFlipped; public float Bloodloss { @@ -865,7 +875,7 @@ namespace Barotrauma public float Bleeding { - get { return CharacterHealth.GetAfflictionStrength(AfflictionPrefab.BleedingType, true); } + get { return CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.BleedingType, true); } } private bool speechImpedimentSet; @@ -936,6 +946,8 @@ namespace Barotrauma { GameMain.GameSession?.CrewManager?.AutoHideCrewList(); } + + _selectedItem?.GetComponent()?.OnViewUpdateProjSpecific(); } #endif if (prevSelectedItem != null && (_selectedItem == null || _selectedItem != prevSelectedItem) && itemSelectedTime > 0) @@ -1073,8 +1085,17 @@ namespace Barotrauma public bool IsLowInOxygen => CharacterHealth.OxygenAmount < 100; + /// + /// Godmoded characters cannot receive any afflictions whatsoever + /// public bool GodMode = false; + public bool Unkillable + { + get { return CharacterHealth.Unkillable; } + set { CharacterHealth.Unkillable = value; } + } + public CampaignMode.InteractionType CampaignInteractionType; public Identifier MerchantIdentifier; @@ -1308,9 +1329,9 @@ namespace Barotrauma break; } } - if (Params.VariantFile != null) + if (Params.VariantFile != null && Params.MainElement is ContentXElement paramsMainElement) { - var overrideElement = Params.VariantFile.Root.FromPackage(Params.MainElement.ContentPackage); + var overrideElement = Params.VariantFile.Root.FromPackage(paramsMainElement.ContentPackage); // Only override if the override file contains matching elements if (overrideElement.GetChildElement("inventory") != null) { @@ -1484,7 +1505,7 @@ namespace Barotrauma var head = AnimController.GetLimb(LimbType.Head); if (head == null) { return; } // Note that if there are any other wearables on the head, they are removed here. - head.OtherWearables.ForEach(w => w.Sprite.Remove()); + head.OtherWearables.ForEach(w => w.Sprite?.Remove()); head.OtherWearables.Clear(); //if the element has not been set at this point, the character has no hair and the index should be zero (= no hair) @@ -1664,17 +1685,31 @@ namespace Barotrauma info.Job?.GiveJobItems(this, spawnPoint); } - public void GiveIdCardTags(WayPoint spawnPoint, bool createNetworkEvent = false) + + public void GiveIdCardTags(WayPoint spawnPoint, bool requireSpawnPointTagsNotGiven = true, bool createNetworkEvent = false) { - if (info?.Job == null || spawnPoint == null) { return; } + GiveIdCardTags(spawnPoint.ToEnumerable(), requireSpawnPointTagsNotGiven, createNetworkEvent); + } + + public void GiveIdCardTags(IEnumerable spawnPoints, bool requireSpawnPointTagsNotGiven = true, bool createNetworkEvent = false) + { + if (info?.Job == null || spawnPoints == null) { return; } foreach (Item item in Inventory.AllItems) { - if (item?.GetComponent() == null) { continue; } - foreach (string s in spawnPoint.IdCardTags) + if (item?.GetComponent() is not IdCard idCard) { continue; } + if (requireSpawnPointTagsNotGiven) { - item.AddTag(s); + if (idCard.SpawnPointTagsGiven) { continue; } } + foreach (var spawnPoint in spawnPoints) + { + foreach (string s in spawnPoint.IdCardTags) + { + item.AddTag(s); + } + } + idCard.SpawnPointTagsGiven = true; if (createNetworkEvent && GameMain.NetworkMember is { IsServer: true }) { GameMain.NetworkMember.CreateEntityEvent(item, new Item.ChangePropertyEventData(item.SerializableProperties[nameof(item.Tags).ToIdentifier()], item)); @@ -1983,14 +2018,21 @@ namespace Barotrauma if (AnimController is HumanoidAnimController humanAnimController) { - humanAnimController.Crouching = humanAnimController.ForceSelectAnimationType == AnimationType.Crouch || IsKeyDown(InputType.Crouch); + humanAnimController.Crouching = + humanAnimController.ForceSelectAnimationType == AnimationType.Crouch || + IsKeyDown(InputType.Crouch); + if (Screen.Selected is not { IsEditor: true }) + { + humanAnimController.ForceSelectAnimationType = AnimationType.NotDefined; + } } if (!aiControlled && !AnimController.IsUsingItem && AnimController.Anim != AnimController.Animation.CPR && (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient || Controlled == this) && - (AnimController.OnGround || IsClimbing) && !AnimController.InWater) + ((!IsClimbing && AnimController.OnGround) || (IsClimbing && IsKeyDown(InputType.Aim))) && + !AnimController.InWater) { if (dontFollowCursor) { @@ -2187,14 +2229,14 @@ namespace Barotrauma { if (!item.RequireAimToUse || IsKeyDown(InputType.Aim)) { - item.Use(deltaTime, this); + item.Use(deltaTime, user: this); } } if (IsKeyDown(InputType.Shoot) && item.IsShootable) { if (!item.RequireAimToUse || IsKeyDown(InputType.Aim)) { - item.Use(deltaTime, this); + item.Use(deltaTime, user: this); } #if CLIENT else if (item.RequireAimToUse && !IsKeyDown(InputType.Aim)) @@ -2214,7 +2256,7 @@ namespace Barotrauma { if (!SelectedCharacter.CanBeSelected || (Vector2.DistanceSquared(SelectedCharacter.WorldPosition, WorldPosition) > MaxDragDistance * MaxDragDistance && - SelectedCharacter.GetDistanceToClosestLimb(SimPosition) > ConvertUnits.ToSimUnits(MaxDragDistance))) + SelectedCharacter.GetDistanceToClosestLimb(GetRelativeSimPosition(selectedCharacter, WorldPosition)) > ConvertUnits.ToSimUnits(MaxDragDistance))) { DeselectCharacter(); } @@ -2247,26 +2289,58 @@ namespace Barotrauma }; } - public bool CanSeeCharacter(Character target) + private Limb GetSeeingLimb() + { + return AnimController.GetLimb(LimbType.Head) ?? AnimController.GetLimb(LimbType.Torso) ?? AnimController.MainLimb; + } + + public bool CanSeeTarget(ISpatialEntity target, ISpatialEntity seeingEntity = null, bool checkFacing = false) + { + seeingEntity ??= AnimController.SimplePhysicsEnabled ? this : GetSeeingLimb(); + if (target is Character targetCharacter) + { + return IsCharacterVisible(targetCharacter, seeingEntity, checkFacing); + } + else + { + return CheckVisibility(target, seeingEntity, checkFacing); + } + } + + public static bool IsTargetVisible(ISpatialEntity target, ISpatialEntity seeingEntity, bool checkFacing = false) + { + if (seeingEntity is Character seeingCharacter) + { + return seeingCharacter.CanSeeTarget(target, checkFacing: checkFacing); + } + if (target is Character targetCharacter) + { + return IsCharacterVisible(targetCharacter, seeingEntity, checkFacing); + } + else + { + return CheckVisibility(target, seeingEntity, checkFacing); + } + } + + private static bool IsCharacterVisible(Character target, ISpatialEntity seeingEntity, bool checkFacing = false) { System.Diagnostics.Debug.Assert(target != null); - if (target == null) { return false; } - if (target.Removed) { return false; } - Limb seeingLimb = GetSeeingLimb(); - if (CanSeeTarget(target, seeingLimb)) { return true; } + if (target == null || target.Removed) { return false; } + if (CheckVisibility(target, seeingEntity, checkFacing)) { return true; } if (!target.AnimController.SimplePhysicsEnabled) { //find the limbs that are furthest from the target's position (from the viewer's point of view) Limb leftExtremity = null, rightExtremity = null; float leftMostDot = 0.0f, rightMostDot = 0.0f; - Vector2 dir = target.WorldPosition - WorldPosition; + Vector2 dir = target.WorldPosition - seeingEntity.WorldPosition; Vector2 leftDir = new Vector2(dir.Y, -dir.X); Vector2 rightDir = new Vector2(-dir.Y, dir.X); foreach (Limb limb in target.AnimController.Limbs) { if (limb.IsSevered || limb == target.AnimController.MainLimb) { continue; } if (limb.Hidden) { continue; } - Vector2 limbDir = limb.WorldPosition - WorldPosition; + Vector2 limbDir = limb.WorldPosition - seeingEntity.WorldPosition; float leftDot = Vector2.Dot(limbDir, leftDir); if (leftDot > leftMostDot) { @@ -2282,42 +2356,39 @@ namespace Barotrauma continue; } } - if (leftExtremity != null && CanSeeTarget(leftExtremity, seeingLimb)) { return true; } - if (rightExtremity != null && CanSeeTarget(rightExtremity, seeingLimb)) { return true; } + if (leftExtremity != null && CheckVisibility(leftExtremity, seeingEntity, checkFacing)) { return true; } + if (rightExtremity != null && CheckVisibility(rightExtremity, seeingEntity, checkFacing)) { return true; } } return false; } - private Limb GetSeeingLimb() - { - return AnimController.GetLimb(LimbType.Head) ?? AnimController.GetLimb(LimbType.Torso) ?? AnimController.MainLimb; - } - - public bool CanSeeTarget(ISpatialEntity target, ISpatialEntity seeingEntity = null) + private static bool CheckVisibility(ISpatialEntity target, ISpatialEntity seeingEntity, bool checkFacing = false) { System.Diagnostics.Debug.Assert(target != null); if (target == null) { return false; } - seeingEntity ??= AnimController.SimplePhysicsEnabled ? this : GetSeeingLimb() as ISpatialEntity; if (seeingEntity == null) { return false; } - ISpatialEntity sourceEntity = seeingEntity ; // TODO: Could we just use the method below? If not, let's refactor it so that we can. - Vector2 diff = ConvertUnits.ToSimUnits(target.WorldPosition - sourceEntity.WorldPosition); + Vector2 diff = ConvertUnits.ToSimUnits(target.WorldPosition - seeingEntity.WorldPosition); + if (checkFacing && seeingEntity is Character seeingCharacter) + { + if (Math.Sign(diff.X) != seeingCharacter.AnimController.Dir) { return false; } + } Body closestBody; //both inside the same sub (or both outside) //OR the we're inside, the other character outside - if (target.Submarine == Submarine || target.Submarine == null) + if (target.Submarine == seeingEntity.Submarine || target.Submarine == null) { - closestBody = Submarine.CheckVisibility(sourceEntity.SimPosition, sourceEntity.SimPosition + diff); + closestBody = Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff); } //we're outside, the other character inside - else if (Submarine == null) + else if (seeingEntity.Submarine == null) { closestBody = Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff); } //both inside different subs else { - closestBody = Submarine.CheckVisibility(sourceEntity.SimPosition, sourceEntity.SimPosition + diff); + closestBody = Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff); if (!IsBlocking(closestBody)) { closestBody = Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff); @@ -2370,9 +2441,6 @@ namespace Barotrauma return false; } - public bool HasEquippedItem(string tagOrIdentifier, bool allowBroken = true, InvSlotType? slotType = null) - => HasEquippedItem(tagOrIdentifier.ToIdentifier(), allowBroken, slotType); - public bool HasEquippedItem(Identifier tagOrIdentifier, bool allowBroken = true, InvSlotType? slotType = null) { if (Inventory == null) { return false; } @@ -2394,7 +2462,7 @@ namespace Barotrauma return false; } - public Item GetEquippedItem(string tagOrIdentifier = null, InvSlotType? slotType = null) + public Item GetEquippedItem(Identifier? tagOrIdentifier = null, InvSlotType? slotType = null) { if (Inventory == null) { return null; } for (int i = 0; i < Inventory.Capacity; i++) @@ -2409,7 +2477,10 @@ namespace Barotrauma } var item = Inventory.GetItemAt(i); if (item == null) { continue; } - if (tagOrIdentifier == null || item.Prefab.Identifier == tagOrIdentifier || item.HasTag(tagOrIdentifier)) { return item; } + if (tagOrIdentifier == null || tagOrIdentifier.Value.IsEmpty || item.Prefab.Identifier == tagOrIdentifier || item.HasTag(tagOrIdentifier.Value)) + { + return item; + } } return null; } @@ -2537,7 +2608,7 @@ namespace Barotrauma } } - return !checkVisibility || CanSeeCharacter(c); + return !checkVisibility || CanSeeTarget(c); } public bool CanInteractWith(Item item, bool checkLinked = true) @@ -2587,7 +2658,10 @@ namespace Barotrauma { foreach (MapEntity linked in item.linkedTo) { - if (linked is Item linkedItem) + if (linked is Item linkedItem && + //if the linked item is inside this container (a modder or sub builder doing smth really weird?) + //don't check it here because it'd lead to an infinite loop + linkedItem.ParentInventory?.Owner != item) { if (CanInteractWith(linkedItem, out float distToLinked, checkLinked: false)) { @@ -2675,10 +2749,18 @@ namespace Barotrauma if (SelectedSecondaryItem != null && !item.IsSecondaryItem) { + //don't allow selecting another Controller if it'd try to turn the character in the opposite direction + //(e.g. periscope that's facing the wrong way while sitting in a chair) if (item.GetComponent() is { } controller && controller.Direction != 0 && controller.Direction != AnimController.Direction) { return false; } - float threshold = ConvertUnits.ToSimUnits(cursorFollowMargin); - if (AnimController.Direction == Direction.Left && SimPosition.X + threshold < itemPosition.X) { return false; } - if (AnimController.Direction == Direction.Right && SimPosition.X - threshold > itemPosition.X) { return false; } + + //if a Controller that controls the character's pose is selected, + //don't allow selecting items that are behind the character's back + if (SelectedSecondaryItem.GetComponent() is { ControlCharacterPose: true } selectedController) + { + float threshold = ConvertUnits.ToSimUnits(cursorFollowMargin); + if (AnimController.Direction == Direction.Left && SimPosition.X + threshold < itemPosition.X) { return false; } + if (AnimController.Direction == Direction.Right && SimPosition.X - threshold > itemPosition.X) { return false; } + } } if (!item.Prefab.InteractThroughWalls && Screen.Selected != GameMain.SubEditorScreen && !insideTrigger) @@ -2703,7 +2785,7 @@ namespace Barotrauma this.onCustomInteract = onCustomInteract; CustomInteractHUDText = hudText; } - + public void SelectCharacter(Character character) { if (character == null || character == this) { return; } @@ -2754,7 +2836,7 @@ namespace Barotrauma if (!PlayerInput.PrimaryMouseButtonHeld() || Barotrauma.Inventory.DraggingItemToWorld) { FocusedCharacter = CanInteract || CanEat ? FindCharacterAtPosition(mouseSimPos) : null; - if (FocusedCharacter != null && !CanSeeCharacter(FocusedCharacter)) { FocusedCharacter = null; } + if (FocusedCharacter != null && !CanSeeTarget(FocusedCharacter)) { FocusedCharacter = null; } float aimAssist = GameSettings.CurrentConfig.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f); if (HeldItems.Any(it => it?.GetComponent()?.IsActive ?? false)) { @@ -2867,7 +2949,7 @@ namespace Barotrauma } #endif } - else + else if (!IsClimbing) { #if CLIENT if (Controlled == this) @@ -2915,9 +2997,9 @@ namespace Barotrauma CharacterHealth.OpenHealthWindow = null; #endif } - else if (IsKeyHit(InputType.Health) && (SelectedItem != null || SelectedSecondaryItem != null)) + else if (IsKeyHit(InputType.Health) && SelectedItem != null) { - SelectedItem = SelectedSecondaryItem = null; + SelectedItem = null; } else if (focusedItem != null) { @@ -3107,6 +3189,14 @@ namespace Barotrauma //implode if not protected from pressure, and either outside or in a high-pressure hull if (!IsProtectedFromPressure && (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f)) { + if (PressureTimer > CharacterHealth.PressureKillDelay * 0.1f) + { + //after a brief delay, start doing increasing amounts of organ damage + CharacterHealth.ApplyAffliction( + targetLimb: AnimController.MainLimb, + new Affliction(AfflictionPrefab.OrganDamage, PressureTimer / 10.0f * deltaTime)); + } + if (CharacterHealth.PressureKillDelay <= 0.0f) { PressureTimer = 100.0f; @@ -3176,47 +3266,48 @@ namespace Barotrauma UpdateAIChatMessages(deltaTime); - if (GameMain.NetworkMember?.ServerSettings?.AllowRagdollButton ?? true) + bool wasRagdolled = IsRagdolled; + if (IsForceRagdolled) { - bool wasRagdolled = IsRagdolled; - if (IsForceRagdolled) + IsRagdolled = IsForceRagdolled; + } + else if (this != Controlled) + { + wasRagdolled = IsRagdolled; + IsRagdolled = IsKeyDown(InputType.Ragdoll); + if (IsRagdolled && IsBot && GameMain.NetworkMember is not { IsClient: true }) { - IsRagdolled = IsForceRagdolled; - } - else if (this != Controlled) - { - wasRagdolled = IsRagdolled; - IsRagdolled = IsKeyDown(InputType.Ragdoll); - } - else - { - bool tooFastToUnragdoll = bodyMovingTooFast(AnimController.Collider) || bodyMovingTooFast(AnimController.MainLimb.body); - bool bodyMovingTooFast(PhysicsBody body) - { - return - body.LinearVelocity.LengthSquared() > 8.0f * 8.0f || - //falling down counts as going too fast - (!InWater && body.LinearVelocity.Y < -5.0f); - } - if (ragdollingLockTimer > 0.0f) - { - ragdollingLockTimer -= deltaTime; - } - else if (!tooFastToUnragdoll) - { - IsRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves - if (wasRagdolled != IsRagdolled) { ragdollingLockTimer = 0.2f; } - } - if (IsRagdolled) - { - SetInput(InputType.Ragdoll, false, true); - } - } - if (!wasRagdolled && IsRagdolled) - { - CheckTalents(AbilityEffectType.OnRagdoll); + ClearInput(InputType.Ragdoll); } } + else + { + bool tooFastToUnragdoll = bodyMovingTooFast(AnimController.Collider) || bodyMovingTooFast(AnimController.MainLimb.body); + bool bodyMovingTooFast(PhysicsBody body) + { + return + body.LinearVelocity.LengthSquared() > 8.0f * 8.0f || + //falling down counts as going too fast + (!InWater && body.LinearVelocity.Y < -5.0f); + } + if (ragdollingLockTimer > 0.0f) + { + ragdollingLockTimer -= deltaTime; + } + else if (!tooFastToUnragdoll) + { + IsRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves + if (wasRagdolled != IsRagdolled) { ragdollingLockTimer = 0.2f; } + } + if (IsRagdolled) + { + SetInput(InputType.Ragdoll, false, true); + } + } + if (!wasRagdolled && IsRagdolled) + { + CheckTalents(AbilityEffectType.OnRagdoll); + } lowPassMultiplier = MathHelper.Lerp(lowPassMultiplier, 1.0f, 0.1f); @@ -3419,7 +3510,7 @@ namespace Barotrauma } private float despawnTimer; - private void UpdateDespawn(float deltaTime, bool ignoreThresholds = false, bool createNetworkEvents = true) + private void UpdateDespawn(float deltaTime, bool createNetworkEvents = true) { if (!EnableDespawn) { return; } @@ -3430,7 +3521,7 @@ namespace Barotrauma int subCorpseCount = 0; - if (Submarine != null && !ignoreThresholds) + if (Submarine != null) { subCorpseCount = CharacterList.Count(c => c.IsDead && c.Submarine == Submarine); if (subCorpseCount < GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold) { return; } @@ -3464,6 +3555,11 @@ namespace Barotrauma despawnTimer += deltaTime * despawnPriority; if (despawnTimer < GameSettings.CurrentConfig.CorpseDespawnDelay) { return; } + Despawn(); + } + + private void Despawn(bool createNetworkEvents = true) + { Identifier despawnContainerId = IsHuman ? "despawncontainer".ToIdentifier() : @@ -3514,8 +3610,7 @@ namespace Barotrauma public void DespawnNow(bool createNetworkEvents = true) { - despawnTimer = GameSettings.CurrentConfig.CorpseDespawnDelay; - UpdateDespawn(1.0f, ignoreThresholds: true, createNetworkEvents: createNetworkEvents); + Despawn(createNetworkEvents); //update twice: first to spawn the duffel bag and move the items into it, then to remove the character for (int i = 0; i < 2; i++) { @@ -4259,7 +4354,7 @@ namespace Barotrauma if (Screen.Selected != GameMain.GameScreen) { return; } if (newStun > 0 && Params.Health.StunImmunity) { - if (EmpVulnerability <= 0 || CharacterHealth.GetAfflictionStrength(AfflictionPrefab.EMPType, allowLimbAfflictions: false) <= 0) + if (EmpVulnerability <= 0 || CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.EMPType, allowLimbAfflictions: false) <= 0) { return; } @@ -4323,8 +4418,7 @@ namespace Barotrauma if (limb.IsSevered) { continue; } if (limb.type == limbType) { - statusEffect.sourceBody = limb.body; - statusEffect.Apply(actionType, deltaTime, this, limb); + ApplyToLimb(actionType, deltaTime, statusEffect, this, limb); } } } @@ -4334,8 +4428,7 @@ namespace Barotrauma Limb limb = AnimController.GetLimb(limbType); if (limb != null) { - statusEffect.sourceBody = limb.body; - statusEffect.Apply(actionType, deltaTime, this, limb); + ApplyToLimb(actionType, deltaTime, statusEffect, this, limb); } } else if (statusEffect.HasTargetType(StatusEffect.TargetType.LastLimb)) @@ -4344,12 +4437,20 @@ namespace Barotrauma Limb limb = AnimController.Limbs.LastOrDefault(l => l.type == limbType && !l.IsSevered && !l.Hidden); if (limb != null) { - statusEffect.sourceBody = limb.body; - statusEffect.Apply(actionType, deltaTime, this, limb); + ApplyToLimb(actionType, deltaTime, statusEffect, this, limb); } } } } + else if (statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs)) + { + // Target all limbs + foreach (var limb in AnimController.Limbs) + { + if (limb.IsSevered) { continue; } + ApplyToLimb(actionType, deltaTime, statusEffect, character: this, limb); + } + } if (statusEffect.HasTargetType(StatusEffect.TargetType.This) || statusEffect.HasTargetType(StatusEffect.TargetType.Character)) { statusEffect.Apply(actionType, deltaTime, this, this); @@ -4373,6 +4474,12 @@ namespace Barotrauma { CharacterHealth.ApplyAfflictionStatusEffects(actionType); } + + static void ApplyToLimb(ActionType actionType, float deltaTime, StatusEffect statusEffect, Character character, Limb limb) + { + statusEffect.sourceBody = limb.body; + statusEffect.Apply(actionType, deltaTime, entity: character, target: limb); + } } private void Implode(bool isNetworkMessage = false) @@ -4433,7 +4540,7 @@ namespace Barotrauma public void Kill(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool isNetworkMessage = false, bool log = true) { - if (IsDead || CharacterHealth.Unkillable || GodMode) { return; } + if (IsDead || CharacterHealth.Unkillable || GodMode || Removed) { return; } HealthUpdateInterval = 0.0f; @@ -4533,20 +4640,21 @@ namespace Barotrauma SelectedCharacter = null; AnimController.ResetPullJoints(); - - foreach (var joint in AnimController.LimbJoints) + if (AnimController.LimbJoints != null) { - if (joint.revoluteJoint != null) + foreach (var joint in AnimController.LimbJoints) { - joint.revoluteJoint.MotorEnabled = false; + if (joint.revoluteJoint != null) + { + joint.revoluteJoint.MotorEnabled = false; + } } } - GameMain.GameSession?.KillCharacter(this); } partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool log); - public void Revive(bool removeAllAfflictions = true) + public void Revive(bool removeAfflictions = true) { if (Removed) { @@ -4557,20 +4665,21 @@ namespace Barotrauma aiTarget?.Remove(); aiTarget = new AITarget(this); - if (removeAllAfflictions) + if (removeAfflictions) { CharacterHealth.RemoveAllAfflictions(); + SetAllDamage(0.0f, 0.0f, 0.0f); + Bloodloss = 0.0f; + SetStun(0.0f, true); } - else - { - CharacterHealth.RemoveNegativeAfflictions(); - } - SetAllDamage(0.0f, 0.0f, 0.0f); Oxygen = 100.0f; - Bloodloss = 0.0f; - SetStun(0.0f, true); isDead = false; + if (info != null) + { + info.CauseOfDeath = null; + } + foreach (LimbJoint joint in AnimController.LimbJoints) { var revoluteJoint = joint.revoluteJoint; @@ -4594,10 +4703,7 @@ namespace Barotrauma limb.IsSevered = false; } - if (GameMain.GameSession != null) - { - GameMain.GameSession.ReviveCharacter(this); - } + GameMain.GameSession?.ReviveCharacter(this); } public override void Remove() @@ -4712,7 +4818,7 @@ namespace Barotrauma { SpawnInventoryItemsRecursive(inventory, itemData, new List()); } - + private void SpawnInventoryItemsRecursive(Inventory inventory, ContentXElement element, List extraDuffelBags) { foreach (var itemElement in element.Elements()) @@ -4737,21 +4843,6 @@ namespace Barotrauma continue; } - //make sure there's no other item in the slot - //this should not happen normally, but can occur if the character is accidentally given new job items while also loading previous items in the campaign - for (int i = 0; i < inventory.Capacity; i++) - { - if (slotIndices.Contains(i)) - { - var existingItem = inventory.GetItemAt(i); - if (existingItem != null && existingItem != newItem && (((MapEntity)existingItem).Prefab != ((MapEntity)newItem).Prefab || existingItem.Prefab.MaxStackSize == 1)) - { - DebugConsole.ThrowError($"Error while loading character inventory data. The slot {i} was already occupied by the item \"{existingItem.Name} ({existingItem.ID})\" when loading the item \"{newItem.Name} ({newItem.ID})\""); - existingItem.Drop(null, createNetworkEvent: false); - } - } - } - bool canBePutInOriginalInventory = true; if (slotIndices[0] >= inventory.Capacity) { @@ -4833,6 +4924,13 @@ namespace Barotrauma } } +#if SERVER + foreach (var circuitBox in newItem.GetComponents()) + { + circuitBox.MarkServerRequiredInitialization(); + } +#endif + int itemContainerIndex = 0; var itemContainers = newItem.GetComponents().ToList(); foreach (var childInvElement in itemElement.Elements()) @@ -4921,41 +5019,7 @@ namespace Barotrauma return visibleHulls; } - public Vector2 GetRelativeSimPosition(ISpatialEntity target, Vector2? worldPos = null) => GetRelativeSimPosition(this, target, worldPos); - - public static Vector2 GetRelativeSimPosition(ISpatialEntity from, ISpatialEntity to, Vector2? worldPos = null) - { - Vector2 targetPos = to.SimPosition; - if (worldPos.HasValue) - { - Vector2 wp = worldPos.Value; - if (to.Submarine != null) - { - wp -= to.Submarine.Position; - } - targetPos = ConvertUnits.ToSimUnits(wp); - } - if (from.Submarine == null && to.Submarine != null) - { - // outside and targeting inside - targetPos += to.Submarine.SimPosition; - } - else if (from.Submarine != null && to.Submarine == null) - { - // inside and targeting outside - targetPos -= from.Submarine.SimPosition; - } - else if (from.Submarine != to.Submarine) - { - if (from.Submarine != null && to.Submarine != null) - { - // both inside, but in different subs - Vector2 diff = from.Submarine.SimPosition - to.Submarine.SimPosition; - targetPos -= diff; - } - } - return targetPos; - } + public Vector2 GetRelativeSimPosition(ISpatialEntity target, Vector2? worldPos = null) => Submarine.GetRelativeSimPosition(this, target, worldPos); public bool IsCaptain => HasJob("captain"); public bool IsEngineer => HasJob("engineer"); @@ -4966,6 +5030,8 @@ namespace Barotrauma public bool IsWatchman => HasJob("watchman"); public bool IsVip => HasJob("prisoner"); public bool IsPrisoner => HasJob("prisoner"); + public bool IsKiller => HasJob("killer"); + public Color? UniqueNameColor { get; set; } = null; public bool HasJob(string identifier) => Info?.Job?.Prefab.Identifier == identifier; @@ -5052,6 +5118,7 @@ namespace Barotrauma public bool HasTalent(Identifier identifier) { + if (info == null) { return false; } return info.UnlockedTalents.Contains(identifier); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs index 9786888d3..954809cb1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs @@ -37,12 +37,18 @@ namespace Barotrauma public EventType EventType { get; } } - public struct InventoryStateEventData : IEventData + public readonly struct InventoryStateEventData : IEventData { public EventType EventType => EventType.InventoryState; + public readonly Range SlotRange; + + public InventoryStateEventData(Range slotRange) + { + SlotRange = slotRange; + } } - public struct ControlEventData : IEventData + public readonly struct ControlEventData : IEventData { public EventType EventType => EventType.Control; public readonly Client Owner; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index fde9954bd..61f4df9c7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -440,10 +440,9 @@ namespace Barotrauma { if (handleBuff) { - var head = Character.AnimController.GetLimb(LimbType.Head); - if (head != null) + if (AfflictionPrefab.Prefabs.TryGet("disguised", out AfflictionPrefab afflictionPrefab)) { - Character.CharacterHealth.ApplyAffliction(head, AfflictionPrefab.List.FirstOrDefault(a => a.Identifier == "disguised").Instantiate(100f)); + Character.CharacterHealth.ApplyAffliction(null, afflictionPrefab.Instantiate(100f)); } } @@ -464,11 +463,7 @@ namespace Barotrauma if (handleBuff) { - var head = Character.AnimController.GetLimb(LimbType.Head); - if (head != null) - { - Character.CharacterHealth.ReduceAfflictionOnLimb(head, "disguised".ToIdentifier(), 100f); - } + Character.CharacterHealth.ReduceAfflictionOnAllLimbs("disguised".ToIdentifier(), 100f); } } @@ -683,11 +678,15 @@ namespace Barotrauma FacialHairColors = CharacterConfigElement.GetAttributeTupleArray("facialhaircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray(); SkinColors = CharacterConfigElement.GetAttributeTupleArray("skincolors", new (Color, float)[] { (new Color(255, 215, 200, 255), 100f) }).ToImmutableArray(); - var headPreset = Prefab.Heads.GetRandom(randSync); + var headPreset = Prefab?.Heads.GetRandom(randSync); + if (headPreset == null) + { + DebugConsole.ThrowError("Failed to find a head preset!"); + } Head = new HeadInfo(this, headPreset); SetAttachments(randSync); SetColors(randSync); - + Job = job ?? ((jobPrefab == null) ? Job.Random(Rand.RandSync.Unsynced) : new Job(jobPrefab, randSync, variant)); if (!string.IsNullOrEmpty(name)) @@ -1089,6 +1088,7 @@ namespace Barotrauma private void LoadHeadSprite() { + if (Ragdoll?.MainElement == null) { return; } foreach (var limbElement in Ragdoll.MainElement.Elements()) { if (!limbElement.GetAttributeString("type", string.Empty).Equals("head", StringComparison.OrdinalIgnoreCase)) { continue; } @@ -1386,7 +1386,7 @@ namespace Barotrauma // Replace the name tag of any existing id cards or duffel bags foreach (var item in Item.ItemList) { - if (!item.HasTag("identitycard") && !item.HasTag("despawncontainer")) { continue; } + if (!item.HasTag("identitycard".ToIdentifier()) && !item.HasTag("despawncontainer".ToIdentifier())) { continue; } foreach (var tag in item.Tags.Split(',')) { var splitTag = tag.Split(":"); @@ -1446,7 +1446,7 @@ namespace Barotrauma if (MinReputationToHire.factionId != default) { charElement.Add( - new XAttribute("factionId", Name), + new XAttribute("factionId", MinReputationToHire.factionId), new XAttribute("minreputation", MinReputationToHire.reputation)); } @@ -1655,8 +1655,12 @@ namespace Barotrauma continue; } var targetType = (Order.OrderTargetType)orderElement.GetAttributeInt("targettype", 0); - int orderGiverInfoId = orderElement.GetAttributeInt("ordergiver", -1); - var orderGiver = orderGiverInfoId >= 0 ? Character.CharacterList.FirstOrDefault(c => c.Info?.GetIdentifier() == orderGiverInfoId) : null; + Character orderGiver = null; + if (orderElement.GetAttribute("ordergiver") is XAttribute orderGiverIdAttribute) + { + int orderGiverInfoId = orderGiverIdAttribute.GetAttributeInt(0); + orderGiver = Character.CharacterList.FirstOrDefault(c => c.Info?.GetIdentifier() == orderGiverInfoId); + } Entity targetEntity = null; switch (targetType) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs index d38730699..2dfb6ecf5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs @@ -46,6 +46,8 @@ namespace Barotrauma public static IEnumerable ConfigElements => Prefabs.Select(p => p.ConfigElement); public static readonly Identifier HumanSpeciesName = "human".ToIdentifier(); + public static readonly Identifier HumanGroup = "human".ToIdentifier(); + public static CharacterFile HumanConfigFile => HumanPrefab.ContentFile as CharacterFile; public static CharacterPrefab HumanPrefab => FindBySpeciesName(HumanSpeciesName); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 3b65cc320..5d46b8207 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -7,6 +7,7 @@ using System.Xml.Linq; using Barotrauma.Extensions; using System.Collections.Immutable; using Barotrauma.Items.Components; +using System.Linq; namespace Barotrauma { @@ -602,7 +603,6 @@ namespace Barotrauma public static readonly Identifier SpaceHerpesType = "spaceherpes".ToIdentifier(); public static readonly Identifier AlienInfectedType = "alieninfected".ToIdentifier(); public static readonly Identifier InvertControlsType = "invertcontrols".ToIdentifier(); - public static readonly Identifier HuskInfectionType = "huskinfection".ToIdentifier(); public static AfflictionPrefab InternalDamage => Prefabs["internaldamage"]; public static AfflictionPrefab BiteWounds => Prefabs["bitewounds"]; @@ -612,6 +612,7 @@ namespace Barotrauma public static AfflictionPrefab OxygenLow => Prefabs["oxygenlow"]; public static AfflictionPrefab Bloodloss => Prefabs["bloodloss"]; public static AfflictionPrefab Pressure => Prefabs["pressure"]; + public static AfflictionPrefab OrganDamage => Prefabs["organdamage"]; public static AfflictionPrefab Stun => Prefabs[StunType]; public static AfflictionPrefab RadiationSickness => Prefabs["radiationsickness"]; @@ -841,20 +842,16 @@ namespace Barotrauma /// public readonly Sprite AfflictionOverlay; - public IEnumerable> TreatmentSuitability + public ImmutableDictionary TreatmentSuitabilities { - get - { - foreach (var itemPrefab in ItemPrefab.Prefabs) - { - float suitability = itemPrefab.GetTreatmentSuitability(Identifier) + itemPrefab.GetTreatmentSuitability(AfflictionType); - if (!MathUtils.NearlyEqual(suitability, 0.0f)) - { - yield return new KeyValuePair(itemPrefab.Identifier, suitability); - } - } - } - } + get; + private set; + } = new Dictionary().ToImmutableDictionary(); + + /// + /// Can this affliction be treated with some item? + /// + public bool HasTreatments { get; private set; } public AfflictionPrefab(ContentXElement element, AfflictionsFile file, Type type) : base(file, element.GetAttributeIdentifier("identifier", "")) { @@ -974,6 +971,22 @@ namespace Barotrauma constructor = type.GetConstructor(new[] { typeof(AfflictionPrefab), typeof(float) }); } + private void RefreshTreatmentSuitabilities() + { + var newTreatmentSuitabilities = new Dictionary(); + + foreach (var itemPrefab in ItemPrefab.Prefabs) + { + float suitability = itemPrefab.GetTreatmentSuitability(Identifier) + itemPrefab.GetTreatmentSuitability(AfflictionType); + if (!MathUtils.NearlyEqual(suitability, 0.0f)) + { + newTreatmentSuitabilities.TryAdd(itemPrefab.Identifier, suitability); + } + } + HasTreatments = newTreatmentSuitabilities.Any(kvp => kvp.Value > 0); + TreatmentSuitabilities = newTreatmentSuitabilities.ToImmutableDictionary(); + } + public LocalizedString GetDescription(float strength, Description.TargetType targetType) { foreach (var description in Descriptions) @@ -993,9 +1006,16 @@ namespace Barotrauma return defaultDescription; } - public static void LoadAllEffects() + /// + /// Should be called before each round: loads all StatusEffects and refreshes treatment suitabilities. + /// + public static void LoadAllEffectsAndTreatmentSuitabilities() { - Prefabs.ForEach(p => p.LoadEffects()); + foreach (var prefab in Prefabs) + { + prefab.RefreshTreatmentSuitabilities(); + prefab.LoadEffects(); + } } public static void ClearAllEffects() @@ -1003,7 +1023,7 @@ namespace Barotrauma Prefabs.ForEach(p => p.ClearEffects()); } - public void LoadEffects() + private void LoadEffects() { ClearEffects(); foreach (var subElement in configElement.Elements()) @@ -1032,7 +1052,7 @@ namespace Barotrauma } } - public void ClearEffects() + private void ClearEffects() { effects.Clear(); periodicEffects.Clear(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 95dad7ad3..d7567b91c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -156,6 +156,12 @@ namespace Barotrauma } } + /// + /// How much vitality the character would have if it was alive? + /// E.g. a character killed by disconnection or with console commands may not have any vitality-reducing afflictions despite being dead + /// + public float VitalityDisregardingDeath => vitality; + public float HealthPercentage => MathUtils.Percentage(Vitality, MaxVitality); public float MaxVitality @@ -229,6 +235,8 @@ namespace Barotrauma } } + public bool IsParalyzed { get; private set; } + public float StunTimer { get; private set; } /// @@ -402,7 +410,17 @@ namespace Barotrauma return strength; } - public float GetAfflictionStrength(Identifier afflictionType, bool allowLimbAfflictions = true) + public float GetAfflictionStrengthByType(Identifier afflictionType, bool allowLimbAfflictions = true) + { + return GetAfflictionStrength(afflictionType, afflictionidentifier: Identifier.Empty, allowLimbAfflictions); + } + + public float GetAfflictionStrengthByIdentifier(Identifier afflictionIdentifier, bool allowLimbAfflictions = true) + { + return GetAfflictionStrength(afflictionType: Identifier.Empty, afflictionIdentifier, allowLimbAfflictions); + } + + public float GetAfflictionStrength(Identifier afflictionType, Identifier afflictionidentifier, bool allowLimbAfflictions = true) { float strength = 0.0f; foreach (KeyValuePair kvp in afflictions) @@ -410,7 +428,8 @@ namespace Barotrauma if (!allowLimbAfflictions && kvp.Value != null) { continue; } var affliction = kvp.Key; if (affliction.Strength < affliction.Prefab.ActivationThreshold) { continue; } - if (affliction.Prefab.AfflictionType == afflictionType) + if ((affliction.Prefab.AfflictionType == afflictionType || afflictionType.IsEmpty) && + (affliction.Prefab.Identifier == afflictionidentifier || afflictionidentifier.IsEmpty)) { strength += affliction.Strength; } @@ -714,7 +733,7 @@ namespace Barotrauma if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; } if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == AfflictionPrefab.StunType) { - if (Character.EmpVulnerability <= 0 || GetAfflictionStrength(AfflictionPrefab.EMPType, allowLimbAfflictions: false) <= 0) + if (Character.EmpVulnerability <= 0 || GetAfflictionStrengthByType(AfflictionPrefab.EMPType, allowLimbAfflictions: false) <= 0) { return; } @@ -953,6 +972,7 @@ namespace Barotrauma public void CalculateVitality() { Vitality = MaxVitality; + IsParalyzed = false; if (Unkillable || Character.GodMode) { return; } foreach (KeyValuePair kvp in afflictions) @@ -966,6 +986,12 @@ namespace Barotrauma } Vitality -= vitalityDecrease; affliction.CalculateDamagePerSecond(vitalityDecrease); + + if (affliction.Strength >= affliction.Prefab.MaxStrength && + affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType) + { + IsParalyzed = true; + } } #if CLIENT if (IsUnconscious) @@ -1136,7 +1162,7 @@ namespace Barotrauma } } - foreach (KeyValuePair treatment in affliction.Prefab.TreatmentSuitability) + foreach (KeyValuePair treatment in affliction.Prefab.TreatmentSuitabilities) { float suitability = treatment.Value * strength; if (treatment.Value > strength) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs index e1748b990..887743c78 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs @@ -134,7 +134,13 @@ namespace Barotrauma foreach (XElement itemElement in spawnItems.GetChildElements("Item")) { InitializeJobItem(character, itemElement, spawnPoint); - } + } + + if (GameMain.GameSession is { TraitorsEnabled: true } && character.IsSecurity) + { + var traitorGuidelineItem = ItemPrefab.Prefabs.Find(ip => ip.Tags.Contains(Tags.TraitorGuidelinesForSecurity)); + Entity.Spawner.AddItemToSpawnQueue(traitorGuidelineItem, character.Inventory); + } } private void InitializeJobItem(Character character, XElement itemElement, WayPoint spawnPoint = null, Item parentItem = null) @@ -144,7 +150,7 @@ namespace Barotrauma { string itemName = itemElement.Attribute("name").Value; DebugConsole.ThrowError("Error in Job config (" + Name + ") - use item identifiers instead of names to configure the items."); - itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab; + itemPrefab = MapEntityPrefab.FindByName(itemName) as ItemPrefab; if (itemPrefab == null) { DebugConsole.ThrowError("Tried to spawn \"" + Name + "\" with the item \"" + itemName + "\". Matching item prefab not found."); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index 5cea1b66f..52d67296f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -591,7 +591,10 @@ namespace Barotrauma public bool IsDead => character.IsDead; public float Health => character.Health; public float HealthPercentage => character.HealthPercentage; + public bool IsHuman => character.IsHuman; + public AIState AIState => character.AIController is EnemyAIController enemyAI ? enemyAI.State : AIState.Idle; + public bool IsFlipped => character.AnimController.IsFlipped; public bool CanBeSeveredAlive { @@ -881,7 +884,9 @@ namespace Barotrauma public void Update(float deltaTime) { UpdateProjSpecific(deltaTime); - + ApplyStatusEffects(ActionType.Always, deltaTime); + ApplyStatusEffects(ActionType.OnActive, deltaTime); + if (InWater) { body.ApplyWaterForces(); @@ -1223,6 +1228,7 @@ namespace Barotrauma if (!statusEffects.TryGetValue(actionType, out var statusEffectList)) { return; } foreach (StatusEffect statusEffect in statusEffectList) { + statusEffect.sourceBody = body; if (statusEffect.type == ActionType.OnDamaged) { if (!statusEffect.HasRequiredAfflictions(character.LastDamage)) { continue; } @@ -1241,65 +1247,64 @@ namespace Barotrauma statusEffect.AddNearbyTargets(WorldPosition, targets); statusEffect.Apply(actionType, deltaTime, character, targets); } - else + else if (statusEffect.targetLimbs != null) { - - if (statusEffect.HasTargetType(StatusEffect.TargetType.Contained) && character.Inventory is { } inventory) + foreach (var limbType in statusEffect.targetLimbs) { - foreach (Item item in inventory.AllItems) + if (statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs)) { - if (statusEffect.TargetIdentifiers != null && - !statusEffect.TargetIdentifiers.Contains(item.Prefab.Identifier) && - statusEffect.TargetIdentifiers.None(id => item.HasTag(id))) + // Target all matching limbs + foreach (var limb in ragdoll.Limbs) { - continue; - } - if (statusEffect.TargetSlot > -1) - { - if (inventory.FindIndex(item) != statusEffect.TargetSlot) { continue; } - } - targets.Add(item); - } - } - else if (statusEffect.HasTargetType(StatusEffect.TargetType.Character)) - { - statusEffect.Apply(actionType, deltaTime, character, character, WorldPosition); - } - else if (statusEffect.targetLimbs != null) - { - foreach (var limbType in statusEffect.targetLimbs) - { - if (statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs)) - { - // Target all matching limbs - foreach (var limb in ragdoll.Limbs) + if (limb.IsSevered) { continue; } + if (limb.type == limbType) { - if (limb.IsSevered) { continue; } - if (limb.type == limbType) - { - statusEffect.Apply(actionType, deltaTime, character, limb); - } + ApplyToLimb(actionType, deltaTime, statusEffect, character, limb); } } - else if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb)) + } + else if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb) || statusEffect.HasTargetType(StatusEffect.TargetType.Character) || statusEffect.HasTargetType(StatusEffect.TargetType.This)) + { + // Target just the first matching limb + Limb limb = ragdoll.GetLimb(limbType); + if (limb != null) { - // Target just the first matching limb - Limb limb = ragdoll.GetLimb(limbType); - statusEffect.Apply(actionType, deltaTime, character, limb); + ApplyToLimb(actionType, deltaTime, statusEffect, character, limb); } - else if (statusEffect.HasTargetType(StatusEffect.TargetType.LastLimb)) + } + else if (statusEffect.HasTargetType(StatusEffect.TargetType.LastLimb)) + { + // Target just the last matching limb + Limb limb = ragdoll.Limbs.LastOrDefault(l => l.type == limbType && !l.IsSevered && !l.Hidden); + if (limb != null) { - // Target just the last matching limb - Limb limb = ragdoll.Limbs.LastOrDefault(l => l.type == limbType && !l.IsSevered && !l.Hidden); - statusEffect.Apply(actionType, deltaTime, character, limb); + ApplyToLimb(actionType, deltaTime, statusEffect, character, limb); } } } - else + } + else if (statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs)) + { + // Target all limbs + foreach (var limb in ragdoll.Limbs) { - statusEffect.Apply(actionType, deltaTime, character, this, WorldPosition); + if (limb.IsSevered) { continue; } + ApplyToLimb(actionType, deltaTime, statusEffect, character, limb); } } + else if (statusEffect.HasTargetType(StatusEffect.TargetType.Character)) + { + statusEffect.Apply(actionType, deltaTime, character, character, WorldPosition); + } + else if (statusEffect.HasTargetType(StatusEffect.TargetType.This) || statusEffect.HasTargetType(StatusEffect.TargetType.Limb)) + { + ApplyToLimb(actionType, deltaTime, statusEffect, character, limb: this); + } + } + static void ApplyToLimb(ActionType actionType, float deltaTime, StatusEffect statusEffect, Character character, Limb limb) + { + statusEffect.sourceBody = limb.body; + statusEffect.Apply(actionType, deltaTime, entity: character, target: limb); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 9289a90d5..0fedaedf3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -143,7 +143,14 @@ namespace Barotrauma protected override string GetName() => "Character Config File"; - public override ContentXElement MainElement => base.MainElement.IsOverride() ? base.MainElement.FirstElement() : base.MainElement; + public override ContentXElement MainElement + { + get + { + if (base.MainElement == null) { return null; } + return base.MainElement.IsOverride() ? base.MainElement.FirstElement() : base.MainElement; + } + } public static XElement CreateVariantXml(XElement variantXML, XElement baseXML) { @@ -182,6 +189,11 @@ namespace Barotrauma { UpdatePath(File.Path); doc = XMLExtensions.TryLoadXml(Path); + if (MainElement == null) + { + DebugConsole.ThrowError("Main element null! Failed to load character params."); + return false; + } Identifier variantOf = MainElement.VariantOf(); if (!variantOf.IsEmpty) { @@ -230,6 +242,11 @@ namespace Barotrauma protected void CreateSubParams() { + if (MainElement == null) + { + DebugConsole.ThrowError("Main element null, cannot create sub params!"); + return; + } SubParams.Clear(); var healthElement = MainElement.GetChildElement("health"); if (healthElement != null) @@ -745,7 +762,7 @@ namespace Barotrauma { if (!TryGetTarget(targetCharacter.SpeciesName, out target)) { - target = targets.FirstOrDefault(t => string.Equals(t.Tag, targetCharacter.Params.Group.ToString(), StringComparison.OrdinalIgnoreCase)); + target = targets.FirstOrDefault(t => t.Tag == targetCharacter.Params.Group); } return target != null; } @@ -791,7 +808,7 @@ namespace Barotrauma public override string Name => "Target"; [Serialize("", IsPropertySaveable.Yes, description: "Can be an item tag, species name or something else. Examples: decoy, provocative, light, dead, human, crawler, wall, nasonov, sonar, door, stronger, weaker, light, human, room..."), Editable()] - public string Tag { get; private set; } + public Identifier Tag { get; private set; } [Serialize(AIState.Idle, IsPropertySaveable.Yes), Editable] public AIState State { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs index 76e138579..03f56b285 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs @@ -71,7 +71,7 @@ namespace Barotrauma element ??= MainElement; if (element == null) { - DebugConsole.ThrowError("[EditableParams] The XML element is null!"); + DebugConsole.ThrowError("[EditableParams] The XML element is null! Failed to save the parameters."); return false; } SerializableProperty.SerializeProperties(this, element, true); @@ -82,7 +82,16 @@ namespace Barotrauma { UpdatePath(file); doc = XMLExtensions.TryLoadXml(Path); - if (doc == null) { return false; } + if (doc == null) + { + DebugConsole.ThrowError("[EditableParams] The document is null! Failed to load the parameters."); + return false; + } + if (MainElement == null) + { + DebugConsole.ThrowError("[EditableParams] The main element is null! Failed to load the parameters."); + return false; + } IsLoaded = Deserialize(MainElement); OriginalElement = new XElement(MainElement).FromPackage(MainElement.ContentPackage); return IsLoaded; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs index be86bd432..9187430ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs @@ -315,20 +315,26 @@ namespace Barotrauma protected void CreateColliders() { Colliders.Clear(); - for (int i = 0; i < MainElement.GetChildElements("collider").Count(); i++) + if (MainElement?.GetChildElements("collider") is { } colliderElements) { - var element = MainElement.GetChildElements("collider").ElementAt(i); - string name = i > 0 ? "Secondary Collider" : "Main Collider"; - Colliders.Add(new ColliderParams(element, this, name)); + for (int i = 0; i < colliderElements.Count(); i++) + { + var element = colliderElements.ElementAt(i); + string name = i > 0 ? "Secondary Collider" : "Main Collider"; + Colliders.Add(new ColliderParams(element, this, name)); + } } } protected void CreateLimbs() { Limbs.Clear(); - foreach (var element in MainElement.GetChildElements("limb")) + if (MainElement?.GetChildElements("limb") is { } childElements) { - Limbs.Add(new LimbParams(element, this)); + foreach (var element in childElements) + { + Limbs.Add(new LimbParams(element, this)); + } } Limbs = Limbs.OrderBy(l => l.ID).ToList(); } @@ -430,8 +436,8 @@ namespace Barotrauma copy.Serialize(); Memento.Store(copy); } - public void Undo() => RevertTo(Memento.Undo() as RagdollParams); - public void Redo() => RevertTo(Memento.Redo() as RagdollParams); + public void Undo() => RevertTo(Memento.Undo()); + public void Redo() => RevertTo(Memento.Redo()); public void ClearHistory() => Memento.Clear(); private void RevertTo(RagdollParams source) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs index 1881758e9..77cc53bad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs @@ -22,13 +22,13 @@ namespace Barotrauma.Abilities private static readonly List WeaponTypeValues = Enum.GetValues(typeof(WeaponType)).Cast().ToList(); private readonly string itemIdentifier; - private readonly string[] tags; + private readonly Identifier[] tags; private readonly WeaponType weapontype; private readonly bool ignoreNonHarmfulAttacks; public AbilityConditionAttackData(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { itemIdentifier = conditionElement.GetAttributeString("itemidentifier", string.Empty); - tags = conditionElement.GetAttributeStringArray("tags", Array.Empty(), convertToLowerInvariant: true); + tags = conditionElement.GetAttributeIdentifierArray("tags", Array.Empty()); ignoreNonHarmfulAttacks = conditionElement.GetAttributeBool("ignorenonharmfulattacks", false); string weaponTypeStr = conditionElement.GetAttributeString("weapontype", "Any"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs index a71667245..f92523e10 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs @@ -41,10 +41,10 @@ namespace Barotrauma.Abilities if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return false; } - foreach (var (factionIdentifier, amount) in mission.ReputationRewards) + foreach (var reputationReward in mission.ReputationRewards) { - if (amount <= 0) { continue; } - if (GetMatchingFaction(factionIdentifier) is { } faction && + if (reputationReward.Amount <= 0) { continue; } + if (GetMatchingFaction(reputationReward.FactionIdentifier) is { } faction && Faction.GetPlayerAffiliationStatus(faction) is FactionAffiliation.Positive) { return CheckMissionType(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAllyHasTalent.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAllyHasTalent.cs new file mode 100644 index 000000000..b1598065c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAllyHasTalent.cs @@ -0,0 +1,22 @@ + +namespace Barotrauma.Abilities +{ + class AbilityConditionAllyHasTalent : AbilityConditionDataless + { + private readonly Identifier talentIdentifier; + + public AbilityConditionAllyHasTalent(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) + { + talentIdentifier = conditionElement.GetAttributeIdentifier("identifier", Identifier.Empty); + } + + protected override bool MatchesConditionSpecific() + { + foreach (Character crewCharacter in Character.GetFriendlyCrew(characterTalent.Character)) + { + if (crewCharacter.HasTalent(talentIdentifier)) { return true; } + } + return false; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs index 8b20847b1..7d75c9f7d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs @@ -5,12 +5,12 @@ namespace Barotrauma.Abilities { class AbilityConditionHasItem : AbilityConditionDataless { - private readonly string[] tags; + private readonly Identifier[] tags; readonly bool requireAll; public AbilityConditionHasItem(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { - tags = conditionElement.GetAttributeStringArray("tags", Array.Empty()); + tags = conditionElement.GetAttributeIdentifierArray("tags", Array.Empty()); requireAll = conditionElement.GetAttributeBool("requireall", false); } @@ -23,7 +23,7 @@ namespace Barotrauma.Abilities if (requireAll) { - foreach (string tag in tags) + foreach (Identifier tag in tags) { if (character.GetEquippedItem(tag) == null) { return false; } } @@ -31,7 +31,7 @@ namespace Barotrauma.Abilities } else { - foreach (string tag in tags) + foreach (Identifier tag in tags) { if (character.GetEquippedItem(tag) != null) { return true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasStatusTag.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasStatusTag.cs index 1b8d54fc3..2ee0a66ee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasStatusTag.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasStatusTag.cs @@ -5,13 +5,13 @@ namespace Barotrauma.Abilities { class AbilityConditionHasStatusTag : AbilityConditionDataless { - private readonly string tag; + private readonly Identifier tag; public AbilityConditionHasStatusTag(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { - tag = conditionElement.GetAttributeString("tag", ""); - if (string.IsNullOrEmpty(tag)) + tag = conditionElement.GetAttributeIdentifier("tag", Identifier.Empty); + if (tag.IsEmpty) { DebugConsole.AddWarning($"Error in talent \"{characterTalent.Prefab.OriginalName}\" - tag not defined in AbilityConditionHasStatusTag."); } @@ -19,7 +19,7 @@ namespace Barotrauma.Abilities protected override bool MatchesConditionSpecific() { - if (!string.IsNullOrEmpty(tag)) + if (!tag.IsEmpty) { return StatusEffect.DurationList.Any(d => d.Targets.Contains(character) && d.Parent.HasTag(tag)) || diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasTalent.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasTalent.cs index de2f98107..0912d9ba4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasTalent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasTalent.cs @@ -12,8 +12,7 @@ namespace Barotrauma.Abilities protected override bool MatchesConditionSpecific() { - bool result = character.HasTalent(talentIdentifier); - return result; + return character.HasTalent(talentIdentifier); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHoldingItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHoldingItem.cs index 5c6c201de..00eb57b23 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHoldingItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHoldingItem.cs @@ -27,8 +27,8 @@ internal sealed class AbilityConditionHoldingItem : AbilityConditionDataless return false; static bool HasItemInHand(Character character, Identifier? tagOrIdentifier) => - character.GetEquippedItem(tagOrIdentifier?.Value, InvSlotType.RightHand) is not null || - character.GetEquippedItem(tagOrIdentifier?.Value, InvSlotType.LeftHand) is not null; + character.GetEquippedItem(tagOrIdentifier, InvSlotType.RightHand) is not null || + character.GetEquippedItem(tagOrIdentifier, InvSlotType.LeftHand) is not null; } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs index 4185832c4..147725f90 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs @@ -37,10 +37,11 @@ namespace Barotrauma.Abilities protected override void ApplyEffect() { - if (Character?.Submarine is null) { return; } + if (Character is null) { return; } - foreach (Item item in Character.Submarine.GetItems(true)) + foreach (Item item in Item.ItemList) { + if (item.Submarine?.TeamID != Character.TeamID) { continue; } if (item.HasTag(tags) || tags.Contains(item.Prefab.Identifier)) { item.StatManager.ApplyStat(stat, stackable, value, CharacterTalent); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs index b62f56296..a2a94cf37 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs @@ -66,12 +66,12 @@ { foreach (Character c in Character.GetFriendlyCrew(Character)) { - c?.Info.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue); + c?.Info?.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue); } } else { - Character?.Info.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue); + Character?.Info?.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs index 29f0cf0cb..b17432528 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs @@ -18,7 +18,7 @@ namespace Barotrauma.Abilities protected override void ApplyEffect() { - IEnumerable enemyCharacters = Character.CharacterList.Where(c => c.TeamID == CharacterTeamType.None); + IEnumerable enemyCharacters = Character.CharacterList.Where(c => !Character.IsFriendly(c)); int timesGiven = 0; foreach (Character enemyCharacter in enemyCharacters) @@ -27,7 +27,6 @@ namespace Barotrauma.Abilities if (enemyCharacter.Submarine == null || enemyCharacter.Submarine != Submarine.MainSub) { continue; } if (enemyCharacter.IsDead) { continue; } if (!enemyCharacter.LockHands) { continue; } - if (timesGiven > max) { continue; } Character.GiveMoney(moneyAmount); GameAnalyticsManager.AddMoneyGainedEvent(moneyAmount, GameAnalyticsManager.MoneySource.Ability, CharacterTalent.Prefab.Identifier.Value); foreach (Character character in Character.GetFriendlyCrew(Character)) @@ -35,6 +34,7 @@ namespace Barotrauma.Abilities character.Info?.GiveExperience(experienceAmount); } timesGiven++; + if (max > 0 && timesGiven >= max) { break; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityRegenerateLoot.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityRegenerateLoot.cs index 75d474784..74f578938 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityRegenerateLoot.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityRegenerateLoot.cs @@ -5,32 +5,43 @@ namespace Barotrauma.Abilities { class CharacterAbilityRegenerateLoot : CharacterAbility { + /// + /// Chance for the loot to be regenerated. We can't use for this, + /// because it'd allow the player to reopen the container until the ability is executed successfully + /// + private readonly float randomChance; + // separate random chance used for the ability itself to prevent the player // from opening/reopening a container until it spawns loot - private readonly float randomChance; + + /// + /// Chance for an individual loot item to be generated. + /// + private readonly float randomChancePerItem = 1.0f; // not maintained through death, so it's possible for players to respawn and re-loot chests // seems like a minor issue for now - private readonly List openedContainers = new List(); + private readonly HashSet openedContainers = new HashSet(); public CharacterAbilityRegenerateLoot(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { - randomChance = abilityElement.GetAttributeFloat("randomchance", 1f); + randomChance = abilityElement.GetAttributeFloat(nameof(randomChance), 1f); + randomChancePerItem = abilityElement.GetAttributeFloat(nameof(randomChancePerItem), 1f); } protected override void ApplyEffect(AbilityObject abilityObject) { - if ((abilityObject as IAbilityItem)?.Item is Item item) - { - if (openedContainers.Contains(item)) { return; } - openedContainers.Add(item); - if (randomChance < Rand.Range(0f, 1f, Rand.RandSync.Unsynced)) { return; } + if ((abilityObject as IAbilityItem)?.Item is not Item item) { return; } + if (openedContainers.Contains(item)) { return; } - if (item.GetComponent() is ItemContainer itemContainer) - { - AutoItemPlacer.RegenerateLoot(item.Submarine, itemContainer); - } + openedContainers.Add(item); + if (randomChance < Rand.Range(0f, 1f, Rand.RandSync.Unsynced)) { return; } + + if (item.GetComponent() is ItemContainer itemContainer) + { + AutoItemPlacer.RegenerateLoot(item.Submarine, itemContainer, skipItemProbability: 1.0f - randomChancePerItem); } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs index ff1e5ccde..2ac8648c7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs @@ -5,10 +5,10 @@ namespace Barotrauma.Abilities class CharacterAbilityTandemFire : CharacterAbilityApplyStatusEffectsToNearestAlly { // this should just be its own class, misleading to inherit here - private readonly string tag; + private readonly Identifier tag; public CharacterAbilityTandemFire(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { - tag = abilityElement.GetAttributeString("tag", ""); + tag = abilityElement.GetAttributeIdentifier("tag", Identifier.Empty); } protected override void ApplyEffect() @@ -37,7 +37,7 @@ namespace Barotrauma.Abilities ApplyEffectSpecific(closestCharacter); } - static bool SelectedItemHasTag(Character character, string tag) => + static bool SelectedItemHasTag(Character character, Identifier tag) => (character.SelectedItem != null && character.SelectedItem.HasTag(tag)) || (character.SelectedSecondaryItem != null && character.SelectedSecondaryItem.HasTag(tag)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs index d14c9df8e..2c1867589 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs @@ -133,6 +133,14 @@ namespace Barotrauma if (IsTalentLocked(talentIdentifier)) { return false; } + if (character.Info.GetUnlockedTalentsInTree().Contains(talentIdentifier)) + { + //if the character already has the talent, it must be viable? + //needed for backwards compatibility, otherwise if we remove e.g. a tier 1 or tier 2 talent, + //all the already-unlocked higher-tier talents will be considered invalid which'll break the talent selection + return true; + } + foreach (var subTree in talentTree!.TalentSubTrees) { if (subTree.AllTalentIdentifiers.Contains(talentIdentifier) && subTree.HasMaxTalents(selectedTalents)) { return false; } @@ -143,11 +151,12 @@ namespace Barotrauma { return !talentOptionStage.HasMaxTalents(selectedTalents) && TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents); } + //if a previous stage hasn't been completed, this talent can't be selected yet bool optionStageCompleted = talentOptionStage.HasEnoughTalents(selectedTalents); if (!optionStageCompleted) { break; - } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxComponent.cs new file mode 100644 index 000000000..eaa828c56 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxComponent.cs @@ -0,0 +1,84 @@ +#nullable enable + +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Xml.Linq; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + internal partial class CircuitBoxComponent : CircuitBoxNode, ICircuitBoxIdentifiable + { + public readonly Item Item; + public ushort ID { get; } + + public readonly ItemPrefab UsedResource; + + public CircuitBoxComponent(ushort id, Item item, Vector2 position, CircuitBox circuitBox, ItemPrefab usedResource): base(circuitBox) + { + if (item.Connections is null) + { + throw new ArgumentNullException(nameof(item.Connections), $"Tried to load a CircuitBoxNode with an item \"{item.Prefab.Name}\" that has no connections."); + } + + var conns = item.Connections.Select(connection => new CircuitBoxNodeConnection(Vector2.Zero, this, connection, circuitBox)).ToList(); + + Vector2 size = CalculateSize(conns); + + ID = id; + Item = item; + Size = size; + Connectors = conns.Cast().ToImmutableArray(); + Position = position; + UsedResource = usedResource; + UpdatePositions(); + } + + public static Option TryLoadFromXML(ContentXElement element, CircuitBox circuitBox) + { + ushort id = element.GetAttributeUInt16("id", ICircuitBoxIdentifiable.NullComponentID); + Vector2 position = element.GetAttributeVector2("position", Vector2.Zero); + var itemIdOption = ItemSlotIndexPair.TryDeserializeFromXML(element, "backingitemid"); + Identifier usedResourceIdentifier = element.GetAttributeIdentifier("usedresource", Identifier.Empty); + + if (!itemIdOption.TryUnwrap(out var itemId) || itemId.FindItemInContainer(circuitBox.ComponentContainer) is not { } backingItem) + { + DebugConsole.ThrowErrorAndLogToGA("CircuitBoxComponent.TryLoadFromXML:IdNotFound", + $"Failed to find item with ID {itemId} for CircuitBoxNode with ID {id}"); + return Option.None; + } + + if (!ItemPrefab.Prefabs.TryGet(usedResourceIdentifier, out var usedResource)) + { + DebugConsole.ThrowErrorAndLogToGA("CircuitBoxComponent.TryLoadXML:UsedResourceNotFound", + $"Failed to find item prefab with identifier {usedResourceIdentifier} for CircuitBoxNode with ID {id}"); + return Option.None; + } + + return Option.Some(new CircuitBoxComponent(id, backingItem, position, circuitBox, usedResource)); + } + + public XElement Save() + { + return new XElement("Component", + new XAttribute("id", ID), + new XAttribute("position", XMLExtensions.Vector2ToString(Position)), + new XAttribute("backingitemid", ItemSlotIndexPair.Serialize(Item)), + new XAttribute("usedresource", UsedResource.Identifier)); + } + + public void Remove() + { + if (Entity.Spawner is { } spawner && Screen.Selected is not { IsEditor: true }) + { + spawner.AddEntityToRemoveQueue(Item); + return; + } + + // if EntitySpawner is not available + Item.Remove(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxConnection.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxConnection.cs new file mode 100644 index 000000000..54eb1ef7e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxConnection.cs @@ -0,0 +1,121 @@ +#nullable enable + +using System.Collections.Generic; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + + internal class CircuitBoxInputConnection : CircuitBoxConnection + { + /// + /// Circuit box input connection is classified as an output because it behaves like an output inside the circuit box. + /// As in you can connect it to a input pin of a component. + /// + public override bool IsOutput => true; + public readonly List ExternallyConnectedTo = new(); + + public CircuitBoxInputConnection(Vector2 position, Connection connection, CircuitBox box) : base(position, connection, box) { } + + public override void ReceiveSignal(Signal signal) + { + foreach (CircuitBoxConnection connector in ExternallyConnectedTo) + { + if (connector is CircuitBoxOutputConnection output) + { + output.ReceiveSignal(signal); + return; + } + Connection.SendSignalIntoConnection(signal, connector.Connection); + } + } + } + + internal class CircuitBoxOutputConnection : CircuitBoxConnection + { + /// + /// Circuit box output connection is classified as an input because it behaves like an input inside the circuit box. + /// As in you can connect it to a output pin of a component. + /// + public override bool IsOutput => false; + + public CircuitBoxOutputConnection(Vector2 position, Connection connection, CircuitBox circuitBox) : base(position, connection, circuitBox) { } + + public override void ReceiveSignal(Signal signal) => CircuitBox.Item.SendSignal(signal, Connection); + } + + internal class CircuitBoxNodeConnection : CircuitBoxConnection + { + public override bool IsOutput => Connection.IsOutput; + + public CircuitBoxComponent Component; + + public bool HasAvailableSlots => Connection.WireSlotsAvailable(); + + public CircuitBoxNodeConnection(Vector2 position, CircuitBoxComponent component, Connection connection, CircuitBox circuitBox) : base(position, connection, circuitBox) + { + Component = component; + } + + public override void ReceiveSignal(Signal signal) => Connection.SendSignalIntoConnection(signal, Connection); + } + + internal abstract partial class CircuitBoxConnection + { + public readonly Connection Connection; + + public abstract bool IsOutput { get; } + + public RectangleF Rect; + + private Vector2 position; + + public List ExternallyConnectedFrom = new(); + + public static float Size = CircuitBoxSizes.ConnectorSize; + + public Vector2 Position + { + get => position; + set + { + Rect.X = value.X - Rect.Width / 2f; + Rect.Y = value.Y - Rect.Height / 2f; + position = value; + } + } + + public float Length { get; private set; } + + public Vector2 AnchorPoint + => new Vector2(IsOutput ? Rect.Right + CircuitBoxSizes.AnchorOffset : Rect.Left - CircuitBoxSizes.AnchorOffset, Rect.Center.Y); + + public readonly CircuitBox CircuitBox; + + protected CircuitBoxConnection(Vector2 position, Connection connection, CircuitBox circuitBox) + { + Connection = connection; + Rect.Width = Rect.Height = Size; + Position = position; + CircuitBox = circuitBox; + InitProjSpecific(circuitBox); + } + + private partial void InitProjSpecific(CircuitBox circuitBox); + + public abstract void ReceiveSignal(Signal signal); + + public bool Contains(Vector2 pos) + { + float x = Rect.X, + y = -(Rect.Y + Rect.Height), + width = Rect.Width, + height = Rect.Height; + + RectangleF rect = new(x, y, width, height); + + return rect.Contains(pos); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxCursor.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxCursor.cs new file mode 100644 index 000000000..1340152a0 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxCursor.cs @@ -0,0 +1,109 @@ +#nullable enable + +using System; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + [NetworkSerialize] + internal readonly record struct NetCircuitBoxCursorInfo(Vector2[] RecordedPositions, Option DragStart, Option HeldItem, ushort CharacterID = 0) : INetSerializableStruct; + + internal sealed class CircuitBoxCursor + { + public NetCircuitBoxCursorInfo Info; + + public CircuitBoxCursor(NetCircuitBoxCursorInfo info) + { + if (Entity.FindEntityByID(info.CharacterID) is Character c) + { + Color = GenerateColor(c.Name); + } + + UpdateInfo(info); + } + + public void UpdateInfo(NetCircuitBoxCursorInfo newInfo) + { + Info = newInfo; + + newInfo.HeldItem.Match( + some: newIdentifier => + HeldPrefab.Match( + some: oldPrefab => + { + if (oldPrefab.Identifier == newIdentifier) { return; } + SetHeldPrefab(newIdentifier); + }, + none: () => SetHeldPrefab(newIdentifier) + ), + none: () => HeldPrefab = Option.None); + + prevPosition = DrawPosition; + + void SetHeldPrefab(Identifier identifier) + { + ItemPrefab? prefab = ItemPrefab.Prefabs.Find(prefab => prefab.Identifier.Equals(identifier)); + HeldPrefab = prefab is null ? Option.None : Option.Some(prefab); + } + } + + public Option HeldPrefab { get; private set; } = Option.None; + + public Color Color = Color.White; + + public static Color GenerateColor(string name) + { + Random random = new Random(ToolBox.StringToInt(name)); + return ToolBox.HSVToRGB(random.NextSingle() * 360f, 1f, 1f); + } + + private const float UpdateTimeout = 5f; + + private float updateTimer; + private float positionTimer; + private Vector2 prevPosition; + public Vector2 DrawPosition; + + public bool IsActive => updateTimer < UpdateTimeout; + + public void Update(float deltaTime) + { + updateTimer += deltaTime; + + Vector2 finalPosition = Info.RecordedPositions[^1]; + + if (positionTimer > 1f) + { + DrawPosition = finalPosition; + prevPosition = Vector2.Zero; + } + else + { + positionTimer += deltaTime; + + float stepTimer = positionTimer * 10f; + int targetPositonIndex = (int)MathF.Floor(stepTimer); + int prevPosIndex = targetPositonIndex - 1; + + Vector2 targetPosition = IsInRange(targetPositonIndex, Info.RecordedPositions.Length) + ? Info.RecordedPositions[targetPositonIndex] + : finalPosition; + + Vector2 prevTargetPosition = IsInRange(prevPosIndex, Info.RecordedPositions.Length) + ? Info.RecordedPositions[prevPosIndex] + : prevPosition; + + DrawPosition = Vector2.Lerp(prevTargetPosition, targetPosition, MathHelper.Clamp(stepTimer % 1f, 0f, 1f)); + } + + static bool IsInRange(int index, int length) + => index >= 0 && index < length; + } + + public void ResetTimers() + { + positionTimer = 0f; + updateTimer = 0f; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxInputOutputNode.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxInputOutputNode.cs new file mode 100644 index 000000000..4c935f53e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxInputOutputNode.cs @@ -0,0 +1,38 @@ +#nullable enable + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Xml.Linq; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + internal sealed class CircuitBoxInputOutputNode : CircuitBoxNode + { + public enum Type + { + Invalid, + Input, + Output + } + + public Type NodeType; + + public CircuitBoxInputOutputNode(IReadOnlyList conns, Vector2 initialPosition, Type type, CircuitBox circuitBox): base(circuitBox) + { + Size = CalculateSize(conns); + Connectors = conns.ToImmutableArray(); + Position = initialPosition; + NodeType = type; + UpdatePositions(); + } + + public XElement Save() => new XElement($"{NodeType}Node", new XAttribute("pos", XMLExtensions.Vector2ToString(Position))); + + public void Load(ContentXElement element) + { + Position = element.GetAttributeVector2("pos", Vector2.Zero); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNetStructs.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNetStructs.cs new file mode 100644 index 000000000..cab96ebee --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNetStructs.cs @@ -0,0 +1,163 @@ +#nullable enable + +using System; +using System.Collections.Immutable; +using System.Xml.Linq; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + public enum CircuitBoxOpcode + { + Error, + Cursor, + AddComponent, + MoveComponent, + AddWire, + RemoveWire, + SelectComponents, + SelectWires, + UpdateSelection, + DeleteComponent, + ServerInitialize + } + + [NetworkSerialize] + internal readonly record struct NetCircuitBoxHeader(CircuitBoxOpcode Opcode, ushort ItemID, byte ComponentIndex) : INetSerializableStruct + { + public Option FindTarget() => CircuitBox.FindCircuitBox(ItemID, ComponentIndex); + } + + [NetworkSerialize] + internal readonly record struct CircuitBoxConnectorIdentifier(Identifier SignalConnection, Option TargetId) : INetSerializableStruct + { + public static CircuitBoxConnectorIdentifier FromConnection(CircuitBoxConnection connection) => + connection switch + { + (CircuitBoxInputConnection or CircuitBoxOutputConnection) + => new CircuitBoxConnectorIdentifier(connection.Name.ToIdentifier(), Option.None), + + CircuitBoxNodeConnection nodeConnection + => new CircuitBoxConnectorIdentifier(connection.Name.ToIdentifier(), Option.Some(nodeConnection.Component.ID)), + + _ => throw new ArgumentOutOfRangeException(nameof(connection)) + }; + + public Option FindConnection(CircuitBox circuitBox) + { + if (!TargetId.TryUnwrap(out var id)) + { + return circuitBox.FindInputOutputConnection(SignalConnection); + } + + foreach (CircuitBoxComponent boxNode in circuitBox.Components) + { + if (boxNode.ID != id) { continue; } + + foreach (var conn in boxNode.Connectors) + { + if (conn.Name != SignalConnection) { continue; } + + return Option.Some(conn); + } + } + + return Option.None; + } + + public XElement Save(string name) => new XElement(name, + new XAttribute("name", SignalConnection), + new XAttribute("target", TargetId.TryUnwrap(out var value) ? value.ToString() : string.Empty)); + + public static CircuitBoxConnectorIdentifier Load(ContentXElement element) + { + string? name = element.GetAttributeString("name", string.Empty); + string? target = element.GetAttributeString("target", string.Empty); + + Option targetId = Option.None; + if (!string.IsNullOrWhiteSpace(target)) + { + targetId = ushort.TryParse(target, out var value) ? Option.Some(value) : Option.None; + } + + return new CircuitBoxConnectorIdentifier(name.ToIdentifier(), targetId); + } + + public override string ToString() + => $"{{Name: {SignalConnection}, ID: {(TargetId.TryUnwrap(out var value) ? value.ToString() : "N/A")}}}"; + } + + [NetworkSerialize] + internal readonly record struct CircuitBoxAddComponentEvent(UInt32 PrefabIdentifier, Vector2 Position) : INetSerializableStruct; + + [NetworkSerialize] + internal readonly record struct CircuitBoxServerCreateComponentEvent(ushort BackingItemId, UInt32 UsedResource, ushort ComponentId, Vector2 Position) : INetSerializableStruct; + + [NetworkSerialize] + internal readonly record struct CircuitBoxRemoveComponentEvent(ImmutableArray TargetIDs) : INetSerializableStruct; + + [NetworkSerialize] + internal readonly record struct CircuitBoxMoveComponentEvent(ImmutableArray TargetIDs, ImmutableArray IOs, Vector2 MoveAmount) : INetSerializableStruct; + + [NetworkSerialize] + internal readonly record struct CircuitBoxSelectNodesEvent(ImmutableArray TargetIDs, ImmutableArray IOs, bool Overwrite, ushort CharacterID) : INetSerializableStruct; + + [NetworkSerialize] + internal readonly record struct CircuitBoxServerUpdateSelection(ImmutableArray ComponentIds, ImmutableArray WireIds, ImmutableArray InputOutputs) : INetSerializableStruct; + + [NetworkSerialize] + internal readonly record struct CircuitBoxIdSelectionPair(ushort ID, Option SelectedBy) : INetSerializableStruct; + + [NetworkSerialize] + internal readonly record struct CircuitBoxTypeSelectionPair(CircuitBoxInputOutputNode.Type Type, Option SelectedBy) : INetSerializableStruct; + + [NetworkSerialize] + internal readonly record struct CircuitBoxSelectWiresEvent(ImmutableArray TargetIDs, bool Overwrite, ushort CharacterID) : INetSerializableStruct; + + [NetworkSerialize] + internal readonly record struct CircuitBoxClientAddWireEvent(Color Color, CircuitBoxConnectorIdentifier Start, CircuitBoxConnectorIdentifier End, UInt32 SelectedWirePrefabIdentifier) : INetSerializableStruct; + + [NetworkSerialize] + internal readonly record struct CircuitBoxServerCreateWireEvent(CircuitBoxClientAddWireEvent Request, ushort WireId, Option BackingItemId) : INetSerializableStruct; + + [NetworkSerialize] + internal readonly record struct CircuitBoxRemoveWireEvent(ImmutableArray TargetIDs) : INetSerializableStruct; + + [NetworkSerialize] + internal readonly record struct CircuitBoxErrorEvent(string Message) : INetSerializableStruct; + + [NetworkSerialize] + internal readonly record struct CircuitBoxInitializeStateFromServerEvent( + ImmutableArray Components, + ImmutableArray Wires, + Vector2 InputPos, + Vector2 OutputPos) : INetSerializableStruct; + + internal readonly record struct CircuitBoxEventData(INetSerializableStruct Data) : ItemComponent.IEventData + { + public CircuitBoxOpcode Opcode => + Data switch + { + (CircuitBoxAddComponentEvent or CircuitBoxServerCreateComponentEvent) + => CircuitBoxOpcode.AddComponent, + CircuitBoxRemoveComponentEvent + => CircuitBoxOpcode.DeleteComponent, + CircuitBoxMoveComponentEvent + => CircuitBoxOpcode.MoveComponent, + CircuitBoxSelectNodesEvent + => CircuitBoxOpcode.SelectComponents, + CircuitBoxSelectWiresEvent + => CircuitBoxOpcode.SelectWires, + CircuitBoxServerUpdateSelection + => CircuitBoxOpcode.UpdateSelection, + (CircuitBoxClientAddWireEvent or CircuitBoxServerCreateWireEvent) + => CircuitBoxOpcode.AddWire, + CircuitBoxRemoveWireEvent + => CircuitBoxOpcode.RemoveWire, + CircuitBoxInitializeStateFromServerEvent + => CircuitBoxOpcode.ServerInitialize, + _ => throw new ArgumentOutOfRangeException(nameof(Data)) + }; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNode.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNode.cs new file mode 100644 index 000000000..0dbb018f8 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNode.cs @@ -0,0 +1,132 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + internal partial class CircuitBoxNode : CircuitBoxSelectable + { + public Vector2 Size; + public RectangleF Rect; + private Vector2 position; + + public Vector2 Position + { + get => position; + set + { + const float clampSize = CircuitBoxSizes.PlayableAreaSize / 2f; + + position = new Vector2(Math.Clamp(value.X, -clampSize, clampSize), + Math.Clamp(value.Y, -clampSize, clampSize)); + UpdatePositions(); + } + } + + public ImmutableArray Connectors; + + public static float Opacity = 0.8f; + + public readonly CircuitBox CircuitBox; + + public CircuitBoxNode(CircuitBox circuitBox) + { + CircuitBox = circuitBox; + } + + public static Vector2 CalculateSize(IReadOnlyList conns) + { + Vector2 leftSize = Vector2.Zero, + rightSize = Vector2.Zero; + + foreach (var c in conns) + { + if (c.IsOutput) + { + rightSize.X = MathF.Max(rightSize.X, c.Length); + } + else + { + leftSize.X = MathF.Max(leftSize.X, c.Length); + } + + if (c.IsOutput) + { + rightSize.Y += CircuitBoxConnection.Size; + } + else + { + leftSize.Y += CircuitBoxConnection.Size; + } + } + + return new Vector2(leftSize.X + CircuitBoxSizes.NodeInitialPadding + rightSize.X, CircuitBoxSizes.NodeInitialPadding + MathF.Max(leftSize.Y, rightSize.Y)); + } + + protected void UpdatePositions() + { + Vector2 rectStart = Position - Size / 2f; + Vector2 rectSize = Size; + rectSize.Y += CircuitBoxSizes.NodeHeaderHeight; + Rect = new RectangleF(rectStart, rectSize); + +#if CLIENT + UpdateDrawRects(); +#endif + + int leftIndex = 0, + rightIndex = 0; + + int inputCount = 0, + outputCount = 0; + + foreach (var c in Connectors) + { + if (c.IsOutput) + { + outputCount++; + } + else + { + inputCount++; + } + } + + Vector2 drawPos = Position; + drawPos.Y = -drawPos.Y; + + foreach (var c in Connectors) + { + bool isOutput = c.IsOutput; + + int yIndex = isOutput ? rightIndex : leftIndex; + int count = isOutput ? outputCount : inputCount; + + float totalHeight = (count * CircuitBoxConnection.Size) / 2f; + float y = (yIndex * CircuitBoxConnection.Size) - totalHeight; + + float halfWidth = Rect.Width / 2f - CircuitBoxConnection.Size / 2f; + + halfWidth -= 16f; + + float xOffset = c.IsOutput ? halfWidth : -halfWidth; + + Vector2 inputPos = drawPos + new Vector2(xOffset, y + c.Rect.Height / 2f); + c.Position = inputPos; + + if (isOutput) + { + rightIndex++; + } + else + { + leftIndex++; + } + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxSelectable.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxSelectable.cs new file mode 100644 index 000000000..913f1756b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxSelectable.cs @@ -0,0 +1,43 @@ +#nullable enable + +using System; + +namespace Barotrauma +{ + internal class CircuitBoxSelectable + { + public bool IsSelected; + public ushort SelectedBy; + + public bool IsSelectedByMe + { + get + { + if (GameMain.NetworkMember is { IsServer: true }) + { + throw new Exception("CircuitBoxSelectable.IsSelectedByMe should never be used by the server."); + } + + if (Character.Controlled is { } controlled) + { + return SelectedBy == controlled.ID; + } + + return false; + } + } + + public void SetSelected(Option selectedBy) + { + if (selectedBy.TryUnwrap(out ushort id)) + { + SelectedBy = id; + IsSelected = true; + return; + } + + IsSelected = false; + SelectedBy = 0; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxSizes.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxSizes.cs new file mode 100644 index 000000000..5f7f211b8 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxSizes.cs @@ -0,0 +1,17 @@ +#nullable enable + +namespace Barotrauma +{ + internal static class CircuitBoxSizes + { + public const int ConnectorSize = 32; + public const int AnchorOffset = 24; + public const int NodeHeaderHeight = 48; + public const int NodeInitialPadding = 64; + public const int WireWidth = 10; + public const int WireKnobLength = 16; + public const int NodeHeaderTextPadding = 8; + + public const float PlayableAreaSize = 8192f; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxWire.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxWire.cs new file mode 100644 index 000000000..843b873fb --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxWire.cs @@ -0,0 +1,186 @@ +#nullable enable + +using System.Xml.Linq; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + internal partial class CircuitBoxWire : CircuitBoxSelectable, ICircuitBoxIdentifiable + { + public CircuitBoxConnection From, To; + public readonly Option BackingWire; + + public readonly Color Color; + public readonly ItemPrefab UsedItemPrefab; + + public ushort ID { get; } + + public CircuitBoxWire(CircuitBox circuitBox, ushort Id, Option backingItem, CircuitBoxConnection from, CircuitBoxConnection to, ItemPrefab prefab) + { + ID = Id; + From = from; + To = to; + BackingWire = backingItem; + Color = prefab.SpriteColor; + UsedItemPrefab = prefab; +#if CLIENT + Renderer = new CircuitBoxWireRenderer(Option.Some(this), to.AnchorPoint, from.AnchorPoint, Color, circuitBox.WireSprite); +#endif + EnsureWireConnected(); + } + + public XElement Save() + { + XElement element = new XElement("Wire", + new XAttribute("id", ID), + new XAttribute("backingitemid", BackingWire.TryUnwrap(out var item) ? ItemSlotIndexPair.Serialize(item) : string.Empty), + new XAttribute("prefab", UsedItemPrefab.Identifier)); + + XElement fromElement = CircuitBoxConnectorIdentifier.FromConnection(From).Save("From"), + toElement = CircuitBoxConnectorIdentifier.FromConnection(To).Save("To"); + + element.Add(fromElement); + element.Add(toElement); + + return element; + } + + public static Option TryLoadFromXML(ContentXElement element, CircuitBox circuitBox) + { + ushort id = element.GetAttributeUInt16("id", ICircuitBoxIdentifiable.NullComponentID); + var backingItemIdOption = ItemSlotIndexPair.TryDeserializeFromXML(element, "backingitemid"); + Identifier usedPrefabIdentifier = element.GetAttributeIdentifier("prefab", Identifier.Empty); + + if (!ItemPrefab.Prefabs.TryGet(usedPrefabIdentifier, out var itemPrefab)) + { + DebugConsole.ThrowErrorAndLogToGA("CircuitBoxWire.TryLoadFromXML:PrefabNotFound", + $"Failed to find prefab used to create wire with identifier {usedPrefabIdentifier} for CircuitBoxWire with ID {id}"); + return Option.None; + } + + Option backingItem = Option.None; + if (backingItemIdOption.TryUnwrap(out var backingItemIdPair)) + { + if (backingItemIdPair.FindItemInContainer(circuitBox.WireContainer) is { } item) + { + backingItem = Option.Some(item); + } + else + { + DebugConsole.ThrowErrorAndLogToGA("CircuitBoxWire.TryLoadFromXML:IdNotFound", + $"Failed to find item with ID {backingItemIdPair} for CircuitBoxWire with ID {id}"); + return Option.None; + } + } + + Option From = Option.None, + To = Option.None; + + foreach (ContentXElement subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "from": + var fromIdentifier = CircuitBoxConnectorIdentifier.Load(subElement); + if (fromIdentifier.FindConnection(circuitBox).TryUnwrap(out var fromConnection)) + { + From = Option.Some(fromConnection); + } + break; + case "to": + var toIdentifier = CircuitBoxConnectorIdentifier.Load(subElement); + if (toIdentifier.FindConnection(circuitBox).TryUnwrap(out var toConnection)) + { + To = Option.Some(toConnection); + } + break; + } + } + + if (From.TryUnwrap(out var from) && To.TryUnwrap(out var to)) + { + return Option.Some(new CircuitBoxWire(circuitBox, id, backingItem, from, to, itemPrefab)); + } + + DebugConsole.ThrowErrorAndLogToGA("CircuitBoxWire.TryLoadFromXML:MissingFromOrTo", + $"Failed to load CircuitBoxWire with ID {id}, missing \"From\" or \"To\" connection."); + + return Option.None; + } + + public void EnsureWireConnected() + { + EnsureExternalConnection(From, To); + EnsureExternalConnection(To, From); + + if (!BackingWire.TryUnwrap(out var item) || item.GetComponent() is not { } wire) { return; } + + wire.DropOnConnect = false; + + From.Connection.ConnectWire(wire); + To.Connection.ConnectWire(wire); + + wire.Connect(From.Connection, 0, addNode: false, sendNetworkEvent: false); + wire.Connect(To.Connection, 1, addNode: false, sendNetworkEvent: false); + + static void EnsureExternalConnection(CircuitBoxConnection one, CircuitBoxConnection two) + { + switch (one) + { + case CircuitBoxInputConnection input: + { + if (input.ExternallyConnectedTo.Contains(two)) { break; } + input.ExternallyConnectedTo.Add(two); + break; + } + case CircuitBoxOutputConnection output: + { + if (output.ExternallyConnectedFrom.Contains(two)) { break; } + output.ExternallyConnectedFrom.Add(two); + break; + } + case CircuitBoxNodeConnection node when two is CircuitBoxOutputConnection output: + { + if (node.Connection.CircuitBoxConnections.Contains(output)) { break; } + node.Connection.CircuitBoxConnections.Add(output); + break; + } + case CircuitBoxNodeConnection node when two is CircuitBoxInputConnection input: + { + if (node.ExternallyConnectedFrom.Contains(input)) { break; } + node.ExternallyConnectedFrom.Add(input); + break; + } + } + } + } + + public void Remove() + { + // client should not remove wires + if (GameMain.NetworkMember is { IsClient: true }) { return; } + + if (!BackingWire.TryUnwrap(out var wireItem)) { return; } + + if (Entity.Spawner is { } spawner && Screen.Selected is not { IsEditor: true }) + { + spawner.AddEntityToRemoveQueue(wireItem); + return; + } + + Wire? wire = wireItem.GetComponent(); + if (wire is not null) + { + From.Connection.DisconnectWire(wire); + To.Connection.DisconnectWire(wire); + } + // if EntitySpawner is not available + wireItem.Remove(); + } + + public static ItemPrefab DefaultWirePrefab => ItemPrefab.Prefabs[Tags.RedWire]; + public static ItemPrefab SelectedWirePrefab = DefaultWirePrefab; + public static readonly Color DefaultWireColor = DefaultWirePrefab.SpriteColor; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/ICircuitBoxIdentifiable.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/ICircuitBoxIdentifiable.cs new file mode 100644 index 000000000..4b86da1c0 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/ICircuitBoxIdentifiable.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Barotrauma +{ + public interface ICircuitBoxIdentifiable + { + public const ushort NullComponentID = ushort.MaxValue; + + public ushort ID { get; } + + public static ushort FindFreeID(IReadOnlyCollection ids) where T : ICircuitBoxIdentifiable + { + var sortedIds = ids.Select(static i => i.ID).ToImmutableHashSet(); + + for (ushort i = 0; i < NullComponentID - 1; i++) + { + if (!sortedIds.Contains(i)) + { + return i; + } + } + + return NullComponentID; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/ItemSlotIndexPair.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/ItemSlotIndexPair.cs new file mode 100644 index 000000000..c2ccdb8bb --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/ItemSlotIndexPair.cs @@ -0,0 +1,44 @@ +#nullable enable + +using System; +using System.Linq; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + internal readonly record struct ItemSlotIndexPair(int Slot, int StackIndex) + { + public static Option TryDeserializeFromXML(ContentXElement element, string elementName) + { + string? elementStr = element.GetAttributeString(elementName, string.Empty); + if (string.IsNullOrEmpty(elementStr)) { return Option.None; } + + var point = XMLExtensions.ParsePoint(elementStr); + return Option.Some(new ItemSlotIndexPair(point.X, point.Y)); + } + + public static string Serialize(Item item) + { + Inventory parent = item.ParentInventory; + if (item.ParentInventory is null) + { + throw new Exception($"Item \"{item.Name}\" is not in an inventory."); + } + + int slotIndex = parent.FindIndex(item); + + int stackIndex = parent.GetItemStackSlotIndex(item, slotIndex); + + if (slotIndex < 0 || stackIndex < 0) + { + throw new Exception($"Unable to find item \"{item.Name}\" in its parent inventory."); + } + + return XMLExtensions.PointToString(new Point(slotIndex, stackIndex)); + } + + public Item? FindItemInContainer(ItemContainer? container) + => container?.Inventory.GetItemsAt(Slot).ElementAt(StackIndex); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs index 15d0bb76a..f2470ed6a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs @@ -67,6 +67,22 @@ namespace Barotrauma .ToImmutableHashSet(); } + public static bool IsLegacyContentType(XElement contentFileElement, ContentPackage package, bool logWarning) + { + Identifier elemName = contentFileElement.NameAsIdentifier(); + if (elemName == "TraitorMissions") + { + if (logWarning) + { + DebugConsole.AddWarning( + $"The content type \"TraitorMission\" in content package \"{package.Name}\" is no longer supported." + + $" Traitor missions should be implemented using the scripted event system and the content type TraitorEvents."); + } + return true; + } + return false; + } + public static Result CreateFromXElement(ContentPackage contentPackage, XElement element) { static Result fail(string error, Exception? exception = null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RandomEventsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RandomEventsFile.cs index 6f636c4e8..4c25ad989 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RandomEventsFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RandomEventsFile.cs @@ -28,8 +28,16 @@ namespace Barotrauma { foreach (var subElement in parentElement.Elements()) { - var prefab = new EventPrefab(subElement, this); - EventPrefab.Prefabs.Add(prefab, overriding); + if (subElement.NameAsIdentifier() == "traitorevent") + { + var prefab = new TraitorEventPrefab(subElement, this); + EventPrefab.Prefabs.Add(prefab, overriding); + } + else + { + var prefab = new EventPrefab(subElement, this); + EventPrefab.Prefabs.Add(prefab, overriding); + } } } else if (elemName == "eventsprites") diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SubmarineFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SubmarineFile.cs index 22c40b0a8..f3a777cb0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SubmarineFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SubmarineFile.cs @@ -28,6 +28,10 @@ namespace Barotrauma { //Overrides for subs don't exist! Should we change this? } + + // Use byte-perfect hash because this is compressed, trimming whitespace is incorrect and needlessly slow here + public override Md5Hash CalculateHash() + => Md5Hash.CalculateForFile(Path.FullPath, Md5Hash.StringHashOptions.BytePerfect); } [NotSyncedInMultiplayer] diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TraitorMissionsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TraitorMissionsFile.cs deleted file mode 100644 index a43c10379..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TraitorMissionsFile.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Xml.Linq; - -#warning TODO: This file is just about the only thing that's actually somewhat okay about the current traitor system. Gut the whole thing. - -#if CLIENT -using PrefabType = Barotrauma.TraitorMissionPrefab; -#elif SERVER -using PrefabType = Barotrauma.TraitorMissionPrefab.TraitorMissionEntry; -#endif - -namespace Barotrauma -{ - [RequiredByCorePackage] - sealed class TraitorMissionsFile : GenericPrefabFile - { - public TraitorMissionsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } - - protected override bool MatchesSingular(Identifier identifier) => identifier == "TraitorMission"; - protected override bool MatchesPlural(Identifier identifier) => identifier == "TraitorMissions"; - protected override PrefabCollection Prefabs => PrefabType.Prefabs; - protected override PrefabType CreatePrefab(ContentXElement element) - { - return new PrefabType(element, this); - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs index 01cd9319c..3d78d1da8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs @@ -23,7 +23,7 @@ namespace Barotrauma : string.Empty); } - public static readonly Version MinimumHashCompatibleVersion = new Version(1, 0, 13, 2); + public static readonly Version MinimumHashCompatibleVersion = new Version(1, 1, 0, 0); public const string LocalModsDir = "LocalMods"; public static readonly string WorkshopModsDir = Barotrauma.IO.Path.Combine( @@ -110,6 +110,7 @@ namespace Barotrauma InstallTime = rootElement.GetAttributeDateTime("installtime"); var fileResults = rootElement.Elements() + .Where(e => !ContentFile.IsLegacyContentType(e, this, logWarning: true)) .Select(e => ContentFile.CreateFromXElement(this, e)) .ToArray(); @@ -337,6 +338,7 @@ namespace Barotrauma XElement rootElement = doc.Root ?? throw new NullReferenceException("XML document is invalid: root element is null."); var fileResults = rootElement.Elements() + .Where(e => !ContentFile.IsLegacyContentType(e, this, logWarning: true)) .Select(e => ContentFile.CreateFromXElement(this, e)) .ToArray(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs index 3f3249990..f113e04f1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs @@ -32,6 +32,11 @@ namespace Barotrauma private static readonly List regular = new List(); public static IReadOnlyList Regular => regular; + /// + /// Combined hash of the currently enabled packages. Can be used to check if the enabled mods or their load order has changed. + /// + public static Md5Hash MergedHash { get; private set; } = Md5Hash.Blank; + public static IEnumerable All => Core != null ? (Core as ContentPackage).ToEnumerable().CollectionConcat(Regular) @@ -149,6 +154,7 @@ namespace Barotrauma .SelectMany(r => r.Files) .Distinct(new TypeComparer()) .ForEach(f => f.Sort()); + MergedHash = Md5Hash.MergeHashes(All.Select(cp => cp.Hash)); } public static int IndexOf(ContentPackage contentPackage) diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs index 18da93f92..149395221 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs @@ -1,10 +1,9 @@ #nullable enable +using Barotrauma.IO; using System; -using System.Globalization; using System.Linq; using System.Text.RegularExpressions; -using Barotrauma.IO; namespace Barotrauma { @@ -93,10 +92,21 @@ namespace Barotrauma } public static ContentPath FromRaw(string? rawValue) - => new ContentPath(null, rawValue); - + => FromRaw(null, rawValue); + + private static ContentPath? prevCreatedRaw; + public static ContentPath FromRaw(ContentPackage? contentPackage, string? rawValue) - => new ContentPath(contentPackage, rawValue); + { + var newRaw = new ContentPath(contentPackage, rawValue); + if (prevCreatedRaw is not null && prevCreatedRaw.ContentPackage == contentPackage && + prevCreatedRaw.RawValue == rawValue) + { + newRaw.cachedValue = prevCreatedRaw.Value; + } + prevCreatedRaw = newRaw; + return newRaw; + } public static ContentPath FromEvaluated(ContentPackage? contentPackage, string? evaluatedValue) { @@ -146,8 +156,8 @@ namespace Barotrauma return HashCode.Combine(RawValue, ContentPackage, cachedValue, cachedFullPath); } - public bool IsNullOrEmpty() => string.IsNullOrEmpty(Value); - public bool IsNullOrWhiteSpace() => string.IsNullOrWhiteSpace(Value); + public bool IsPathNullOrEmpty() => string.IsNullOrEmpty(Value); + public bool IsPathNullOrWhiteSpace() => string.IsNullOrWhiteSpace(Value); public bool EndsWith(string suffix) => Value.EndsWith(suffix, StringComparison.OrdinalIgnoreCase); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs index 74937c8ce..c97b534ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs @@ -52,7 +52,7 @@ namespace Barotrauma => Element.Descendants().Select(e => new ContentXElement(ContentPackage, e)); public IEnumerable GetChildElements(string name) - => Elements().Where(e => string.Equals(name, e.Name.LocalName, StringComparison.InvariantCultureIgnoreCase)); + => Elements().Where(e => string.Equals(name, e.Name.LocalName, StringComparison.OrdinalIgnoreCase)); public XAttribute? GetAttribute(string name) => Element.GetAttribute(name); @@ -76,6 +76,7 @@ namespace Barotrauma public string[]? GetAttributeStringArray(string key, string[]? def, bool convertToLowerInvariant = false) => Element.GetAttributeStringArray(key, def, convertToLowerInvariant); public ContentPath? GetAttributeContentPath(string key) => Element.GetAttributeContentPath(key, ContentPackage); public int GetAttributeInt(string key, int def) => Element.GetAttributeInt(key, def); + public ushort GetAttributeUInt16(string key, ushort def) => Element.GetAttributeUInt16(key, def); public int[]? GetAttributeIntArray(string key, int[]? def) => Element.GetAttributeIntArray(key, def); public ushort[]? GetAttributeUshortArray(string key, ushort[]? def) => Element.GetAttributeUshortArray(key, def); public float GetAttributeFloat(string key, float def) => Element.GetAttributeFloat(key, def); diff --git a/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs b/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs index e381b89ac..d3b52fe14 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs @@ -1,4 +1,6 @@ -using System; +#nullable enable + +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -15,20 +17,20 @@ namespace Barotrauma public abstract bool EndsCoroutine(CoroutineHandle handle); } - class EnumCoroutineStatus : CoroutineStatus + sealed class EnumCoroutineStatus : CoroutineStatus { private enum StatusValue { Running, Success, Failure } - private readonly StatusValue Value; + private readonly StatusValue value; - private EnumCoroutineStatus(StatusValue value) { Value = value; } + private EnumCoroutineStatus(StatusValue value) { this.value = value; } - public new readonly static EnumCoroutineStatus Running = new EnumCoroutineStatus(StatusValue.Running); - public new readonly static EnumCoroutineStatus Success = new EnumCoroutineStatus(StatusValue.Success); - public new readonly static EnumCoroutineStatus Failure = new EnumCoroutineStatus(StatusValue.Failure); + public new static readonly EnumCoroutineStatus Running = new EnumCoroutineStatus(StatusValue.Running); + public new static readonly EnumCoroutineStatus Success = new EnumCoroutineStatus(StatusValue.Success); + public new static readonly EnumCoroutineStatus Failure = new EnumCoroutineStatus(StatusValue.Failure); public override bool CheckFinished(float deltaTime) { @@ -37,43 +39,74 @@ namespace Barotrauma public override bool EndsCoroutine(CoroutineHandle handle) { - if (Value == StatusValue.Failure) + if (value == StatusValue.Failure) { DebugConsole.ThrowError("Coroutine \"" + handle.Name + "\" has failed"); } - return Value != StatusValue.Running; + return value != StatusValue.Running; } - public override bool Equals(object obj) + public override bool Equals(object? obj) { - return obj is EnumCoroutineStatus other && Value == other.Value; + return obj is EnumCoroutineStatus other && value == other.value; } public override int GetHashCode() { - return Value.GetHashCode(); + return value.GetHashCode(); } public override string ToString() { - return Value.ToString(); + return value.ToString(); + } + } + + sealed class WaitForSeconds : CoroutineStatus + { + public readonly float TotalTime; + + private float timer; + private readonly bool ignorePause; + + public WaitForSeconds(float time, bool ignorePause = true) + { + timer = time; + TotalTime = time; + this.ignorePause = ignorePause; + } + + public override bool CheckFinished(float deltaTime) + { +#if !SERVER + if (ignorePause || !CoroutineManager.Paused) + { + timer -= deltaTime; + } +#else + timer -= deltaTime; +#endif + return timer <= 0.0f; + } + + public override bool EndsCoroutine(CoroutineHandle handle) + { + return false; } } - class CoroutineHandle + sealed class CoroutineHandle { public readonly IEnumerator Coroutine; public readonly string Name; - public Exception Exception; - public volatile bool AbortRequested; + public Exception? Exception; + public bool AbortRequested; - public Thread Thread; - - public CoroutineHandle(IEnumerator coroutine, string name = "", bool useSeparateThread = false) + public CoroutineHandle(IEnumerator coroutine, string name = "") { Coroutine = coroutine; - Name = string.IsNullOrWhiteSpace(name) ? coroutine.ToString() : name; + Name = string.IsNullOrWhiteSpace(name) ? (coroutine.ToString() ?? "") : name; Exception = null; } @@ -88,7 +121,7 @@ namespace Barotrauma public static bool Paused { get; private set; } - public static CoroutineHandle StartCoroutine(IEnumerable func, string name = "", bool useSeparateThread = false) + public static CoroutineHandle StartCoroutine(IEnumerable func, string name = "") { var handle = new CoroutineHandle(func.GetEnumerator(), name); lock (Coroutines) @@ -96,17 +129,6 @@ namespace Barotrauma Coroutines.Add(handle); } - handle.Thread = null; - if (useSeparateThread) - { - handle.Thread = new Thread(() => { ExecuteCoroutineThread(handle); }) - { - Name = "Coroutine Thread (" + handle.Name + ")", - IsBackground = true - }; - handle.Thread.Start(); - } - return handle; } @@ -115,11 +137,12 @@ namespace Barotrauma return StartCoroutine(DoInvokeAfter(action, delay)); } - private static IEnumerable DoInvokeAfter(Action action, float delay) + private static IEnumerable DoInvokeAfter(Action? action, float delay) { if (action == null) { yield return CoroutineStatus.Failure; + yield break; } if (delay > 0.0f) @@ -169,20 +192,12 @@ namespace Barotrauma private static void HandleCoroutineStopping(Func filter) { + // No lock here because all callers lock for us foreach (CoroutineHandle coroutine in Coroutines) { if (filter(coroutine)) { coroutine.AbortRequested = true; - if (coroutine.Thread != null) - { - bool joined = false; - while (!joined) - { - CrossThread.ProcessTasks(); - joined = coroutine.Thread.Join(TimeSpan.FromMilliseconds(500)); - } - } } } } @@ -199,49 +214,13 @@ namespace Barotrauma return false; } - public static void ExecuteCoroutineThread(CoroutineHandle handle) - { - try - { - while (!handle.AbortRequested) - { - if (PerformCoroutineStep(handle)) { return; } - Thread.Sleep((int)(DeltaTime * 1000)); - } - } - catch (ThreadAbortException) - { - //not an error, don't worry about it - } - catch (Exception e) - { - handle.Exception = e; - DebugConsole.ThrowError("Coroutine \"" + handle.Name + "\" has thrown an exception", e); - } - } - private static bool IsDone(CoroutineHandle handle) { #if !DEBUG try { #endif - if (handle.Thread == null) - { - return PerformCoroutineStep(handle); - } - else - { - if (handle.Thread.ThreadState.HasFlag(ThreadState.Stopped)) - { - if (handle.Exception != null || handle.Coroutine.Current == CoroutineStatus.Failure) - { - DebugConsole.ThrowError("Coroutine \"" + handle.Name + "\" has failed"); - } - return true; - } - return false; - } + return PerformCoroutineStep(handle); #if !DEBUG } catch (Exception e) @@ -256,27 +235,27 @@ namespace Barotrauma #endif } // Updating just means stepping through all the coroutines + private static readonly List coroutinePass = new List(); public static void Update(bool paused, float deltaTime) { Paused = paused; DeltaTime = deltaTime; - List coroutineList; + // Do not optimize this as a for loop directly over the Coroutines list! + // Coroutines are able to spawn new coroutines! lock (Coroutines) { - coroutineList = Coroutines.ToList(); + coroutinePass.AddRange(Coroutines); } - - foreach (var coroutine in coroutineList) + foreach (var coroutine in coroutinePass) { - if (IsDone(coroutine)) + if (!IsDone(coroutine)) { continue; } + lock (Coroutines) { - lock (Coroutines) - { - Coroutines.Remove(coroutine); - } + Coroutines.Remove(coroutine); } } + coroutinePass.Clear(); } public static void ListCoroutines() @@ -292,37 +271,4 @@ namespace Barotrauma } } } - - class WaitForSeconds : CoroutineStatus - { - public readonly float TotalTime; - - private float timer; - private readonly bool ignorePause; - - public WaitForSeconds(float time, bool ignorePause = true) - { - timer = time; - TotalTime = time; - this.ignorePause = ignorePause; - } - - public override bool CheckFinished(float deltaTime) - { -#if !SERVER - if (ignorePause || !CoroutineManager.Paused) - { - timer -= deltaTime; - } -#else - timer -= deltaTime; -#endif - return timer <= 0.0f; - } - - public override bool EndsCoroutine(CoroutineHandle handle) - { - return false; - } - } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index b7c419430..af66e1865 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -336,7 +336,14 @@ namespace Barotrauma NewMessage("Enemy AI enabled", Color.Green); }, isCheat: true)); - commands.Add(new Command("starttraitormissionimmediately", "starttraitormissionimmediately: Skip the initial delay of the traitor mission and start one immediately.", null)); + commands.Add(new Command("triggertraitorevent|starttraitoreventimmediately", "triggertraitorevent [eventidentifier]: Skip the initial delay of the traitor events and start one immediately. You can optionally specify which event to start (otherwise a random event is chosen).", null, + () => + { + return new string[][] + { + EventPrefab.Prefabs.Where(p => p is TraitorEventPrefab).Select(p => p.Identifier.ToString()).ToArray() + }; + })); commands.Add(new Command("botcount", "botcount [x]: Set the number of bots in the crew in multiplayer.", null)); @@ -644,7 +651,7 @@ namespace Barotrauma commands.Add(new Command("findentityids", "findentityids [entityname]", (string[] args) => { if (args.Length == 0) { return; } - foreach (MapEntity mapEntity in MapEntity.mapEntityList) + foreach (MapEntity mapEntity in MapEntity.MapEntityList) { if (mapEntity.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase)) { @@ -815,8 +822,12 @@ namespace Barotrauma if (GameMain.GameSession?.EventManager != null && args.Length > 0) { EventPrefab eventPrefab = eventPrefabs.Find(prefab => prefab.Identifier == args[0]); - - if (eventPrefab != null) + if (eventPrefab is TraitorEventPrefab) + { + ThrowError($"{eventPrefab.Identifier} is a traitor event. You need to use the 'triggertraitorevent' command to start it."); + return; + } + else if (eventPrefab != null) { var newEvent = eventPrefab.CreateInstance(); if (newEvent == null) @@ -824,8 +835,7 @@ namespace Barotrauma NewMessage($"Could not initialize event {args[0]} because level did not meet requirements"); return; } - GameMain.GameSession.EventManager.ActiveEvents.Add(newEvent); - newEvent.Init(); + GameMain.GameSession.EventManager.ActivateEvent(newEvent); NewMessage($"Initialized event {eventPrefab.Identifier}", Color.Aqua); return; } @@ -844,9 +854,36 @@ namespace Barotrauma }; })); + commands.Add(new Command("debugevent", "debugevent [identifier]: outputs debug info about a specific event that's currently active. Mainly intended for debugging events in multiplayer: in single player, the same information is available by enabling debugdraw.", (string[] args) => + { + if (GameMain.GameSession?.EventManager is EventManager eventManager && args.Length > 0) + { + var ev = eventManager.ActiveEvents.FirstOrDefault(ev => ev.Prefab?.Identifier == args[0]); + if (ev == null) + { + ThrowError($"Event \"{args[0]}\" not found."); + } + else + { + string info = ev.GetDebugInfo(); +#if SERVER + //strip rich text tags + RichTextData.GetRichTextData(info, out info); +#endif + NewMessage(info); + } + } + }, isCheat: true, getValidArgs: () => + { + return new[] + { + GameMain.GameSession?.EventManager?.ActiveEvents.Select(ev => ev.Prefab.Identifier.ToString()).ToArray() ?? Array.Empty() + }; + })); + commands.Add(new Command("unlockmission", "unlockmission [identifier/tag]: Unlocks a mission in a random adjacent level.", (string[] args) => { - if (!(GameMain.GameSession?.GameMode is CampaignMode campaign)) + if (GameMain.GameSession?.GameMode is not CampaignMode campaign) { ThrowError("The unlockmission command is only usable in the campaign mode."); return; @@ -1640,7 +1677,7 @@ namespace Barotrauma List pumps = new List(); foreach (Item item in Submarine.MainSub.GetItems(true)) { - if (item.CurrentHull != null && item.HasTag("ballast") && item.GetComponent() is { } pump) + if (item.CurrentHull != null && item.HasTag(Tags.Ballast) && item.GetComponent() is { } pump) { if (item.CurrentHull.BallastFlora != null) { continue; } pumps.Add(pump); @@ -2224,8 +2261,8 @@ namespace Barotrauma string itemNameOrId = args[0].ToLowerInvariant(); ItemPrefab itemPrefab = - (MapEntityPrefab.Find(itemNameOrId, identifier: null, showErrorMessages: false) ?? - MapEntityPrefab.Find(null, identifier: itemNameOrId, showErrorMessages: false)) as ItemPrefab; + (MapEntityPrefab.FindByName(itemNameOrId) ?? + MapEntityPrefab.FindByIdentifier(itemNameOrId.ToIdentifier())) as ItemPrefab; if (itemPrefab == null) { errorMsg = "Item \"" + itemNameOrId + "\" not found!"; @@ -2320,6 +2357,19 @@ namespace Barotrauma } } + /// + /// Throws the error in debug builds. In non-debug builds, logs it instead. + /// Use for handling non-critical errors that shouldn't go unnoticed in debug builds (like warnings might), but which don't break the game and thus doesn't have to open the console. + /// + public static void AddSafeError(string error) + { +#if DEBUG + DebugConsole.ThrowError(error); +#else + DebugConsole.LogError(error); +#endif + } + public static void LogError(string msg, Color? color = null) { color ??= Color.Red; @@ -2493,7 +2543,16 @@ namespace Barotrauma LogError(error); } - + + public static void ThrowErrorAndLogToGA(string gaIdentifier, string errorMsg) + { + ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce( + gaIdentifier, + GameAnalyticsManager.ErrorSeverity.Error, + errorMsg); + } + public static void AddWarning(string warning) { System.Diagnostics.Debug.WriteLine(warning); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs index dcd1deeea..16622dee8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs @@ -64,7 +64,16 @@ namespace Barotrauma spawnPending = true; } - + + public override string GetDebugInfo() + { + return + $"Finished: {IsFinished.ColorizeObject()}\n" + + $"Item: {Item.ColorizeObject()}\n" + + $"Spawn pending: {SpawnPending.ColorizeObject()}\n" + + $"Spawn position: {SpawnPos.ColorizeObject()}"; + } + private void SpawnItem() { item = new Item(itemPrefab, spawnPos, null); @@ -73,7 +82,7 @@ namespace Barotrauma //try to find an artifact holder and place the artifact inside it foreach (Item it in Item.ItemList) { - if (it.Submarine != null || !it.HasTag("artifactholder")) continue; + if (it.Submarine != null || !it.HasTag(Tags.ArtifactHolder)) { continue; } var itemContainer = it.GetComponent(); if (itemContainer == null) continue; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs index 1b41e3bf6..1df74bcb8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs @@ -52,6 +52,11 @@ namespace Barotrauma ParentSet = parentSet; } + public virtual string GetDebugInfo() + { + return $"Finished: {IsFinished.ColorizeObject()}"; + } + public virtual void Update(float deltaTime) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckAfflictionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckAfflictionAction.cs index ca7fcad9d..fb75303ee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckAfflictionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckAfflictionAction.cs @@ -12,6 +12,9 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes)] public Identifier TargetTag { get; set; } = Identifier.Empty; + [Serialize("", IsPropertySaveable.Yes, description: "Tag referring to the character who caused the affliction.")] + public Identifier SourceCharacter { get; set; } = Identifier.Empty; + [Serialize(LimbType.None, IsPropertySaveable.Yes, "Only check afflictions on the specified limb type")] public LimbType TargetLimb { get; set; } @@ -33,8 +36,7 @@ namespace Barotrauma if (target.CharacterHealth == null) { continue; } if (TargetLimb == LimbType.None) { - var affliction = target.CharacterHealth.GetAffliction(Identifier, AllowLimbAfflictions); - if (affliction != null && affliction.Strength >= MinStrength) { return true; } + if (target.CharacterHealth.GetAfflictionStrengthByIdentifier(Identifier, AllowLimbAfflictions) >= MinStrength) { return true; } } IEnumerable afflictions = target.CharacterHealth.GetAllAfflictions().Where(affliction => { @@ -43,9 +45,13 @@ namespace Barotrauma LimbType? limbType = target.CharacterHealth.GetAfflictionLimb(affliction)?.type; if (limbType == null || limbType != TargetLimb) { return false; } } + if (!SourceCharacter.IsEmpty) + { + if (!ParentEvent.GetTargets(SourceCharacter).Contains(affliction.Source)) { return false; } + } + return affliction.Strength >= MinStrength; }); - if (afflictions.Any(a => a.Identifier == Identifier)) { return true; } } return false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs index af4f0c713..5fed3e0ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs @@ -46,7 +46,7 @@ namespace Barotrauma } if (target == null) { - DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction but no valid target was found for tag \"{TargetTag}\"! This will cause the check to automatically succeed."); + DebugConsole.LogError($"{nameof(CheckConditionalAction)} error: {GetEventName()} uses a {nameof(CheckConditionalAction)} but no valid target was found for tag \"{TargetTag}\"! This will cause the check to automatically succeed."); } if (target == null || Conditional == null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs index bd30a9b4c..df038e70c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs @@ -85,7 +85,7 @@ namespace Barotrauma case bool bool1 when metadata2 is bool bool2: return CompareBool(bool1, bool2) ?? false; case float float1 when metadata2 is float float2: - return CompareFloat(float1, float2) ?? false; + return PropertyConditional.CompareFloat(float1, float2, Operator); } } @@ -143,36 +143,13 @@ namespace Barotrauma { if (float.TryParse(value, out float f)) { - return CompareFloat(GetFloat(campaignMode), f); + return PropertyConditional.CompareFloat(GetFloat(campaignMode), f, Operator); } DebugConsole.Log($"{value} != float"); return null; } - private bool? CompareFloat(float val1, float val2) - { - value1 = val1; - value2 = val2; - switch (Operator) - { - case PropertyConditional.ComparisonOperatorType.Equals: - return MathUtils.NearlyEqual(val1, val2); - case PropertyConditional.ComparisonOperatorType.GreaterThan: - return val1 > val2; - case PropertyConditional.ComparisonOperatorType.GreaterThanEquals: - return val1 >= val2; - case PropertyConditional.ComparisonOperatorType.LessThan: - return val1 < val2; - case PropertyConditional.ComparisonOperatorType.LessThanEquals: - return val1 <= val2; - case PropertyConditional.ComparisonOperatorType.NotEquals: - return !MathUtils.NearlyEqual(val1, val2); - } - - return null; - } - private bool? TryString(CampaignMode campaignMode, string value) { return CompareString(GetString(campaignMode), value); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs index d202db5b8..4d53e6eca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs @@ -1,8 +1,8 @@ using Barotrauma.Extensions; using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -20,9 +20,15 @@ namespace Barotrauma [Serialize(1, IsPropertySaveable.Yes)] public int Amount { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Optional tag of a hull the target must be inside.")] + public Identifier HullTag { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the first target when the check succeeds.")] public Identifier ApplyTagToTarget { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the found item(s) when the check succeeds.")] + public Identifier ApplyTagToItem { get; set; } + [Serialize(false, IsPropertySaveable.Yes)] public bool RequireEquipped { get; set; } @@ -32,6 +38,24 @@ namespace Barotrauma [Serialize(-1, IsPropertySaveable.Yes)] public int ItemContainerIndex { get; set; } + private readonly bool checkPercentage; + + private float requiredConditionalMatchPercentage; + + [Serialize(100.0f, IsPropertySaveable.Yes)] + + /// + /// What percentage of targets do the conditionals need to match for the check to succeed? + /// + public float RequiredConditionalMatchPercentage + { + get { return requiredConditionalMatchPercentage; } + set { requiredConditionalMatchPercentage = MathHelper.Clamp(value, 0.0f, 100.0f); } + } + + [Serialize(false, IsPropertySaveable.Yes)] + public bool CompareToInitialAmount { get; set; } + private readonly IReadOnlyList conditionals; private readonly Identifier[] itemIdentifierSplit; @@ -49,16 +73,84 @@ namespace Barotrauma } conditionals = conditionalList; - if (itemTags.None() && ItemIdentifiers.None()) + if (itemTags.None() && + ItemIdentifiers.None() && + TargetTag.IsEmpty) { DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". {nameof(CheckItemAction)} does't define either tags or identifiers of the item to check."); } + + checkPercentage = element.GetAttribute(nameof(RequiredConditionalMatchPercentage)) is not null; + if (Amount != 1 && checkPercentage) + { + DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". Cannot define both '{Amount}' and '{RequiredConditionalMatchPercentage}' in {nameof(CheckItemAction)}."); + } } + private bool EnoughTargets(int totalTargets, int targetsWithConditionalsMatched) + { + if (CompareToInitialAmount) + { + totalTargets = ParentEvent.GetInitialTargetCount(TargetTag); + } + if (checkPercentage) + { + return MathUtils.Percentage(targetsWithConditionalsMatched, totalTargets) >= RequiredConditionalMatchPercentage; + } + else + { + return targetsWithConditionalsMatched >= Amount; + } + } + + private readonly List tempTargetItems = new List(); protected override bool? DetermineSuccess() { var targets = ParentEvent.GetTargets(TargetTag); - if (!targets.Any()) { return null; } + + if (!HullTag.IsEmpty) + { + var hulls = ParentEvent.GetTargets(HullTag).OfType(); + targets = targets.Where(t => + (t is Item it && hulls.Contains(it.CurrentHull)) || + (t is Character c && hulls.Contains(c.CurrentHull))); + } + + if (!targets.Any()) + { + if (conditionals.Any()) + { + //conditionals can't be met if there's no targets + return false; + } + return null; + } + + //check if the target(s) are the items we're looking for (instead of characters/containers the items are inside) + int targetCount = targets.Count(); + if (targetCount >= Amount) + { + tempTargetItems.Clear(); + foreach (var target in targets) + { + if (target is not Item item) { continue; } + if (itemTags.Any() && itemTags.None(item.HasTag) && + itemIdentifierSplit.Any() && !itemIdentifierSplit.Contains(item.Prefab.Identifier)) + { + continue; + } + if (ConditionalsMatch(item, character: null)) + { + tempTargetItems.Add(item); + } + } + if (EnoughTargets(targetCount, tempTargetItems.Count)) + { + TryApplyTagToItems(tempTargetItems); + return true; + } + } + foreach (var target in targets) { if (target is Character character) @@ -99,8 +191,9 @@ namespace Barotrauma private bool CheckInventory(Inventory inventory, Character character) { if (inventory == null) { return false; } - int count = 0; + int targetCount = 0; HashSet eventTargets = new HashSet(); + tempTargetItems.Clear(); foreach (Identifier tag in itemTags) { foreach (var target in ParentEvent.GetTargets(tag)) @@ -117,19 +210,39 @@ namespace Barotrauma eventTargets.Contains(it), recursive: Recursive)) { - if (!ConditionalsMatch(item, character)) { continue; } - count++; - if (count >= Amount) { return true; } + targetCount++; + if (ConditionalsMatch(item, character)) + { + tempTargetItems.Add(item); + } + } + + if (EnoughTargets(targetCount, tempTargetItems.Count)) + { + TryApplyTagToItems(tempTargetItems); + return true; } return false; + + } + + private void TryApplyTagToItems(IEnumerable items) + { + if (!ApplyTagToItem.IsEmpty) + { + foreach (var targetItem in items) + { + ParentEvent.AddTarget(ApplyTagToItem, targetItem); + } + } } private bool ConditionalsMatch(Item item, Character character = null) { if (item == null) { return false; } foreach (PropertyConditional conditional in conditionals) - { - if (!conditional.Matches(item)) + { + if (!item.ConditionalMatches(conditional)) { return false; } @@ -145,8 +258,8 @@ namespace Barotrauma public override string ToDebugString() { return $"{ToolBox.GetDebugSymbol(HasBeenDetermined())} {nameof(CheckItemAction)} -> (TargetTag: {TargetTag.ColorizeObject()}, " + - $"ItemIdentifiers: {ItemIdentifiers.ColorizeObject()}" + - $"Succeeded: {succeeded.ColorizeObject()})"; + (ItemTags.Any() ? $"ItemTags: {ItemTags.ColorizeObject()}, " : $"ItemIdentifiers: {ItemIdentifiers.ColorizeObject()}, ") + + $"Succeeded: {succeeded.ColorizeObject()})"; } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorEventStateAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorEventStateAction.cs new file mode 100644 index 000000000..f3833fa6d --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorEventStateAction.cs @@ -0,0 +1,35 @@ +#nullable enable + +namespace Barotrauma +{ + class CheckTraitorEventStateAction : BinaryOptionAction + { + [Serialize(TraitorEvent.State.Completed, IsPropertySaveable.Yes)] + public TraitorEvent.State State { get; set; } + + private readonly TraitorEvent? traitorEvent; + + public CheckTraitorEventStateAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + if (parentEvent is TraitorEvent traitorEvent) + { + this.traitorEvent = traitorEvent; + } + else + { + DebugConsole.ThrowError($"Cannot use the action {nameof(CheckTraitorEventStateAction)} in the event \"{parentEvent.Prefab.Identifier}\" because it's not a traitor event."); + } + } + + protected override bool? DetermineSuccess() + { + return traitorEvent?.CurrentState == State; + } + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(HasBeenDetermined())} {nameof(CheckTraitorEventStateAction)} -> " + + $"State: {State.ColorizeObject()}, Succeeded: {succeeded.ColorizeObject()})"; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorVoteAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorVoteAction.cs new file mode 100644 index 000000000..7f8e26526 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorVoteAction.cs @@ -0,0 +1,40 @@ +#nullable enable +using Barotrauma.Networking; +using System.Linq; + +namespace Barotrauma +{ + /// + /// Checks whether the specific target was voted as the traitor. + /// + class CheckTraitorVoteAction : BinaryOptionAction + { + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Target { get; set; } + + public CheckTraitorVoteAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + if (parentEvent is not TraitorEvent) + { + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\" - {nameof(CheckTraitorVoteAction)} can only be used in traitor events."); + } + } + + protected override bool? DetermineSuccess() + { + var targetEntities = ParentEvent.GetTargets(Target); +#if SERVER + if (GameMain.Server?.TraitorManager?.GetClientAccusedAsTraitor() is Client traitorClient) + { + return targetEntities.Any(e => e is Character character && traitorClient?.Character == character); + } +#endif + return false; + } + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(succeeded.HasValue)} {nameof(CheckTraitorVoteAction)} -> (TargetTag: {Target.ColorizeObject()}"; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckVisibilityAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckVisibilityAction.cs new file mode 100644 index 000000000..35e4eeb9c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckVisibilityAction.cs @@ -0,0 +1,60 @@ +#nullable enable + + +namespace Barotrauma +{ + class CheckVisibilityAction : BinaryOptionAction + { + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the entity to do the visibility check from.")] + public Identifier EntityTag { get; set; } + + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the entity to do the visibility check to.")] + public Identifier TargetTag { get; set; } + + [Serialize(false, IsPropertySaveable.Yes, description: "Does the entity need to be facing the target? Only valid if the entity is a character.")] + public bool CheckFacing { get; set; } + + [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the entity who saw the target when the check succeeds.")] + public Identifier ApplyTagToEntity { get; set; } + + [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the entity that was seen when the check succeeds.")] + public Identifier ApplyTagToTarget { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "If both the seeing entity and the target are the same, does it count as success?")] + public bool AllowSameEntity { get; set; } + + public CheckVisibilityAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + } + + protected override bool? DetermineSuccess() + { + foreach (var entity in ParentEvent.GetTargets(EntityTag)) + { + foreach (var target in ParentEvent.GetTargets(TargetTag)) + { + if (!AllowSameEntity && entity == target) { continue; } + if (Character.IsTargetVisible(target, entity, CheckFacing)) + { + if (!ApplyTagToEntity.IsEmpty) + { + ParentEvent.AddTarget(ApplyTagToEntity, entity); + } + if (!ApplyTagToTarget.IsEmpty) + { + ParentEvent.AddTarget(ApplyTagToTarget, target); + } + return true; + } + } + } + + return false; + } + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(HasBeenDetermined())} {nameof(CheckVisibilityAction)} -> (TargetTags: {EntityTag.ColorizeObject()}, {TargetTag.ColorizeObject()})"; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CombatAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CombatAction.cs index 26320d4f9..9f1242e13 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CombatAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CombatAction.cs @@ -43,13 +43,14 @@ namespace Barotrauma foreach (var npc in affectedNpcs) { - if (!(npc.AIController is HumanAIController humanAiController)) { continue; } + if (npc.Removed) { continue; } + if (npc.AIController is not HumanAIController humanAiController) { continue; } Character enemy = null; float closestDist = float.MaxValue; foreach (Entity target in ParentEvent.GetTargets(EnemyTag)) { - if (!(target is Character character)) { continue; } + if (target is not Character character) { continue; } float dist = Vector2.DistanceSquared(npc.WorldPosition, target.WorldPosition); if (dist < closestDist) { @@ -82,7 +83,7 @@ namespace Barotrauma { foreach (var npc in affectedNpcs) { - if (npc.Removed || !(npc.AIController is HumanAIController humanAiController)) { continue; } + if (npc.Removed || npc.AIController is not HumanAIController humanAiController) { continue; } foreach (var combatObjective in humanAiController.ObjectiveManager.GetActiveObjectives()) { combatObjective.Abandon = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs index a01b1abd4..8092bdb91 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs @@ -59,6 +59,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes)] public bool ContinueConversation { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the event will not stop to wait for the conversation to be dismissed.")] + public bool ContinueAutomatically { get; set; } + [Serialize(false, IsPropertySaveable.Yes)] public bool IgnoreInterruptDistance { get; set; } @@ -86,6 +89,8 @@ namespace Barotrauma private bool interrupt; + private readonly XElement textElement; + public ConversationAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { actionCount++; @@ -93,15 +98,41 @@ namespace Barotrauma Options = new List(); foreach (var elem in element.Elements()) { - if (elem.Name.LocalName.Equals("option", StringComparison.InvariantCultureIgnoreCase)) + if (elem.Name.LocalName.Equals("option", StringComparison.OrdinalIgnoreCase)) { Options.Add(new SubactionGroup(ParentEvent, elem)); } - else if (elem.Name.LocalName.Equals("interrupt", StringComparison.InvariantCultureIgnoreCase)) + else if (elem.Name.LocalName.Equals("interrupt", StringComparison.OrdinalIgnoreCase)) { Interrupted = new SubactionGroup(ParentEvent, elem); } + else if (elem.Name.LocalName.Equals("text", StringComparison.OrdinalIgnoreCase)) + { + Text = elem.GetAttributeString("tag", string.Empty); + textElement = elem; + } } + if (element.GetChildElement("Replace") != null) + { + DebugConsole.ThrowError( + $"Error in {nameof(EventObjectiveAction)} in the event \"{parentEvent.Prefab.Identifier}\"" + + $" - unrecognized child element \"Replace\"."); + } + } + + public LocalizedString GetDisplayText() + { + LocalizedString text = string.Empty; + + if (textElement != null) + { + TextManager.ConstructDescription(ref text, textElement, ParentEvent.GetTextForReplacementElement); + } + else + { + text = TextManager.Get(Text).Fallback(Text); + } + return ParentEvent.ReplaceVariablesInEventText(text); } public override IEnumerable GetSubActions() @@ -145,9 +176,14 @@ namespace Barotrauma } } + if (ContinueAutomatically && Options.None()) + { + return dialogOpened; + } + if (selectedOption >= 0) { - if (!Options.Any() || Options[selectedOption].IsFinished(ref goTo)) + if (Options.None() || Options[selectedOption].IsFinished(ref goTo)) { ResetSpeaker(); return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CountTargetsAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CountTargetsAction.cs new file mode 100644 index 000000000..c2287af04 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CountTargetsAction.cs @@ -0,0 +1,120 @@ +#nullable enable + +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + class CountTargetsAction : BinaryOptionAction + { + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } + + [Serialize("", IsPropertySaveable.Yes, description: "Optional second tag. Can be used if the target must have two different tags.")] + public Identifier SecondRequiredTargetTag { get; set; } + + [Serialize("", IsPropertySaveable.Yes, description: "Optional tag of a hull the target must be inside.")] + public Identifier HullTag { get; set; } + + [Serialize(-1, IsPropertySaveable.Yes)] + public int MinAmount { get; set; } + + [Serialize(-1, IsPropertySaveable.Yes)] + public int MaxAmount { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier CompareToTarget { get; set; } + + [Serialize(-1.0f, IsPropertySaveable.Yes)] + + /// + /// Minimum amount of targets, as a percentage of the number of entities tagged with CompareToTarget + /// E.g. you could compare the number of entities tagged as "discoveredhull" to entities tagged as "anyhull" to require 50% of hulls to be discovered. + /// + public float MinPercentageRelativeToTarget { get; set; } + + [Serialize(-1.0f, IsPropertySaveable.Yes)] + + /// + /// Maximum amount of targets, as a percentage of the number of entities tagged with CompareToTarget + /// E.g. you could compare the number of entities tagged as "floodedhull" to entities tagged as "anyhull" to require less than 50% of hulls to be flooded. + /// + public float MaxPercentageRelativeToTarget { get; set; } + + private readonly IReadOnlyList conditionals; + + public CountTargetsAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + var conditionalList = new List(); + foreach (ContentXElement subElement in element.GetChildElements("conditional")) + { + conditionalList.AddRange(PropertyConditional.FromXElement(subElement!)); + } + conditionals = conditionalList; + + if (CompareToTarget.IsEmpty) + { + int amount = element.GetAttributeInt("amount", -1); + if (amount > -1) + { + MinAmount = MaxAmount = amount; + } + if (MinAmount > MaxAmount && MaxAmount > -1) + { + DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". {MinAmount} is larger than {MaxAmount} in {nameof(CountTargetsAction)}."); + } + } + else + { + if (MinPercentageRelativeToTarget < 0.0f && MaxPercentageRelativeToTarget < 0.0f) + { + DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". Comparing to another target, but neither {nameof(MinPercentageRelativeToTarget)} or {nameof(MaxPercentageRelativeToTarget)} is set."); + } + } + } + + protected override bool? DetermineSuccess() + { + var potentialTargets = ParentEvent.GetTargets(TargetTag); + + if (!SecondRequiredTargetTag.IsEmpty) + { + potentialTargets = potentialTargets.Where(t => ParentEvent.GetTargets(SecondRequiredTargetTag).Contains(t)); + } + if (!HullTag.IsEmpty) + { + var hulls = ParentEvent.GetTargets(HullTag).OfType(); + potentialTargets = potentialTargets.Where(t => + (t is Item it && hulls.Contains(it.CurrentHull)) || + (t is Character c && hulls.Contains(c.CurrentHull))); + } + + if (conditionals.Any()) + { + potentialTargets = potentialTargets.Where(t => conditionals.Any(c => c.Matches(t as ISerializableEntity))); + } + + int targetCount = potentialTargets.Count(); + + if (CompareToTarget.IsEmpty) + { + if (MinAmount > -1 && targetCount < MinAmount) { return false; } + if (MaxAmount > -1 && targetCount > MaxAmount) { return false; } + } + else + { + int compareToTargetCount = ParentEvent.GetTargets(CompareToTarget).Count(); + float percentage = MathUtils.Percentage(targetCount, compareToTargetCount); + if (MinPercentageRelativeToTarget > -1 && percentage < MinPercentageRelativeToTarget) { return false; } + if (MaxPercentageRelativeToTarget > -1 && percentage > MaxPercentageRelativeToTarget) { return false; } + } + return true; + } + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(HasBeenDetermined())} {nameof(CountTargetsAction)} -> (TargetTag: {TargetTag.ColorizeObject()}, " + + $"Succeeded: {succeeded.ColorizeObject()})"; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs index b570a750d..dd086ec74 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Xml.Linq; namespace Barotrauma { @@ -138,12 +137,17 @@ namespace Barotrauma Type actionType; try { - actionType = Type.GetType("Barotrauma." + element.Name, true, true); + Identifier typeName = element.Name.ToString().ToIdentifier(); + if (typeName == "TutorialSegmentAction") + { + typeName = "EventObjectiveAction".ToIdentifier(); + } + actionType = Type.GetType("Barotrauma." + typeName, throwOnError: true, ignoreCase: true); if (actionType == null) { throw new NullReferenceException(); } } catch { - DebugConsole.ThrowError("Could not find an event class of the type \"" + element.Name + "\"."); + DebugConsole.ThrowError($"Could not find an {nameof(EventAction)} class of the type \"{element.Name}\"."); return null; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventLogAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventLogAction.cs new file mode 100644 index 000000000..7fc3af8b5 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventLogAction.cs @@ -0,0 +1,94 @@ +#nullable enable + +using System; +using System.Xml.Linq; + +namespace Barotrauma +{ + partial class EventLogAction : EventAction + { + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Id { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public string Text { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } + + public bool ShowInServerLog { get; set; } + + private readonly XElement? textElement; + + public EventLogAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + if (Id == Identifier.Empty) + { + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\". {nameof(EventLogAction)} with no id."); + } + //append the target tag so logs targeted to different players don't interfere with each other even if they use the same Id + Id = (Id.ToString() + TargetTag).ToIdentifier(); + + foreach (var elem in element.Elements()) + { + if (elem.Name.LocalName.Equals("text", StringComparison.OrdinalIgnoreCase)) + { + textElement = elem; + break; + } + } + Text ??= string.Empty; + if (textElement == null) + { + if (Text.IsNullOrEmpty()) + { + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\". {nameof(EventLogAction)} with no text set ({element})."); + } + else + { + Text = TextManager.Get(Text).Fallback(Text).Value; + } + } + ShowInServerLog = element.GetAttributeBool(nameof(ShowInServerLog), ParentEvent is TraitorEvent); + } + + private bool isFinished; + + public override bool IsFinished(ref string goTo) + { + return isFinished; + } + + public override void Reset() + { + isFinished = false; + } + + + public LocalizedString GetDisplayText() + { + LocalizedString text = Text; + if (textElement != null) + { + LocalizedString tempDescription = string.Empty; + TextManager.ConstructDescription(ref tempDescription, textElement, ParentEvent.GetTextForReplacementElement); + text = tempDescription.Value; + } + return ParentEvent.ReplaceVariablesInEventText(text); + } + + public override void Update(float deltaTime) + { + if (isFinished) { return; } + AddEntryProjSpecific(GameMain.GameSession?.EventManager?.EventLog, GetDisplayText().Value); + isFinished = true; + } + + partial void AddEntryProjSpecific(EventLog? eventLog, string displayText); + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(EventLogAction)} -> (Id: {Id})"; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialSegmentAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventObjectiveAction.cs similarity index 62% rename from Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialSegmentAction.cs rename to Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventObjectiveAction.cs index f2128f82e..17316466f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialSegmentAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventObjectiveAction.cs @@ -1,8 +1,8 @@ namespace Barotrauma { - partial class TutorialSegmentAction : EventAction + partial class EventObjectiveAction : EventAction { - public enum SegmentActionType { Trigger, Add, Complete, CompleteAndRemove, Remove }; + public enum SegmentActionType { Trigger, Add, Complete, CompleteAndRemove, Remove, Fail, FailAndRemove }; [Serialize(SegmentActionType.Trigger, IsPropertySaveable.Yes)] public SegmentActionType Type { get; set; } @@ -34,14 +34,29 @@ namespace Barotrauma [Serialize(80, IsPropertySaveable.Yes)] public int Height { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } + private bool isFinished; - public TutorialSegmentAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + public EventObjectiveAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { if (Identifier.IsEmpty) { Identifier = element.GetAttributeIdentifier("id", Identifier.Empty); } + if (Type != SegmentActionType.Trigger && !TextTag.IsEmpty) + { + DebugConsole.ThrowError( + $"Error in {nameof(EventObjectiveAction)} in the event \"{parentEvent.Prefab.Identifier}\""+ + $" - {nameof(TextTag)} will do nothing unless the action triggers a message box or a video."); + } + if (element.GetChildElement("Replace") != null) + { + DebugConsole.ThrowError( + $"Error in {nameof(EventObjectiveAction)} in the event \"{parentEvent.Prefab.Identifier}\"" + + $" - unrecognized child element \"Replace\"."); + } } public override void Update(float deltaTime) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveExpAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveExpAction.cs new file mode 100644 index 000000000..7031d0daf --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveExpAction.cs @@ -0,0 +1,49 @@ +using System.Linq; + +namespace Barotrauma +{ + class GiveExpAction : EventAction + { + [Serialize(0, IsPropertySaveable.Yes)] + public int Amount { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } + + public GiveExpAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + if (TargetTag.IsEmpty) + { + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": {nameof(GiveExpAction)} without a target tag (the action needs to know whose skill to check)."); + } + } + + private bool isFinished = false; + + public override bool IsFinished(ref string goTo) + { + return isFinished; + } + public override void Reset() + { + isFinished = false; + } + + public override void Update(float deltaTime) + { + if (isFinished) { return; } + var targets = ParentEvent.GetTargets(TargetTag).Where(e => e is Character).Select(e => e as Character); + foreach (var target in targets) + { + target.Info?.GiveExperience(Amount); + } + isFinished = true; + } + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(GiveExpAction)} -> (TargetTag: {TargetTag.ColorizeObject()}, " + + $"Amount: {Amount.ColorizeObject()})"; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs index 12290178a..b051966cd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs @@ -1,6 +1,4 @@ -using Microsoft.Xna.Framework; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -19,7 +17,7 @@ namespace Barotrauma { if (TargetTag.IsEmpty) { - DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": GiveSkillExpAction without a target tag (the action needs to know whose skill to check)."); + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": {nameof(GiveSkillExpAction)} without a target tag (the action needs to know whose skill to check)."); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs index 6f186b127..95c317203 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs @@ -1,5 +1,3 @@ -using System.Xml.Linq; - namespace Barotrauma { class GoTo : EventAction diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs index e86414d96..64861f6e4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs @@ -100,7 +100,7 @@ namespace Barotrauma WayPoint.WayPointList.Find(wp => wp.Submarine == sub && wp.SpawnType == SpawnType.Human); if (subWaypoint != null) { - npc.GiveIdCardTags(subWaypoint, createNetworkEvent: true); + npc.GiveIdCardTags(subWaypoint, requireSpawnPointTagsNotGiven: false, createNetworkEvent: true); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs index 6546199a0..94111f0fd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs @@ -25,8 +25,8 @@ namespace Barotrauma public NPCFollowAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } - private List affectedNpcs = null; - private Entity target = null; + private IEnumerable affectedNpcs; + private Entity target; public override void Update(float deltaTime) { @@ -36,9 +36,10 @@ namespace Barotrauma if (target == null) { return; } int targetCount = 0; - affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character).ToList(); + affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character); foreach (var npc in affectedNpcs) { + if (npc.Removed) { continue; } if (npc.AIController is not HumanAIController humanAiController) { continue; } if (Follow) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCOperateItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCOperateItemAction.cs index b39eb465f..e3e8c9ade 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCOperateItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCOperateItemAction.cs @@ -48,6 +48,7 @@ namespace Barotrauma affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character).ToList(); foreach (var npc in affectedNpcs) { + if (npc.Removed) { continue; } if (npc.AIController is not HumanAIController humanAiController) { continue; } if (Operate) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs index 3a6f61fe5..28db501f7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs @@ -27,6 +27,7 @@ namespace Barotrauma foreach (var npc in affectedNpcs) { + if (npc.Removed) { continue; } if (npc.AIController is not HumanAIController humanAiController) { continue; } if (Wait) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/OnRoundEndAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/OnRoundEndAction.cs new file mode 100644 index 000000000..23bb3b63a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/OnRoundEndAction.cs @@ -0,0 +1,42 @@ +#nullable enable + +namespace Barotrauma +{ + class OnRoundEndAction : EventAction + { + private readonly SubactionGroup subActions; + + public OnRoundEndAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + subActions = new SubactionGroup(parentEvent, element); + } + + public override bool IsFinished(ref string goToLabel) + { + return false; + } + + public override void Update(float deltaTime) + { + int remainingTries = 100; + string? throwaway = null; + //normally the ref string goTo passed to IsFinished should be used to jump another place in the event, + //but in this case we don't want that (the subactions should just run once when the round ends) + while (remainingTries > 0 && !subActions.IsFinished(ref throwaway)) + { + subActions.Update(deltaTime); + Entity.Spawner?.Update(createNetworkEvents: false); + remainingTries--; + } + } + + public override void Reset() + { + } + + public override string ToDebugString() + { + return nameof(OnRoundEndAction); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetTraitorEventStateAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetTraitorEventStateAction.cs new file mode 100644 index 000000000..5361d5696 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetTraitorEventStateAction.cs @@ -0,0 +1,48 @@ +#nullable enable + +namespace Barotrauma +{ + class SetTraitorEventStateAction : EventAction + { + private readonly TraitorEvent? traitorEvent; + + public SetTraitorEventStateAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + if (parentEvent is TraitorEvent traitorEvent) + { + this.traitorEvent = traitorEvent; + } + else + { + DebugConsole.ThrowError($"Cannot use the action {nameof(SetTraitorEventStateAction)} in the event \"{parentEvent.Prefab.Identifier}\" because it's not a traitor event."); + } + } + + [Serialize(TraitorEvent.State.Completed, IsPropertySaveable.Yes)] + public TraitorEvent.State State { get; set; } + + private bool isFinished; + + public override bool IsFinished(ref string goTo) + { + return isFinished; + } + + public override void Reset() + { + isFinished = false; + } + + public override void Update(float deltaTime) + { + if (isFinished || traitorEvent == null) { return; } + traitorEvent.CurrentState = State; + isFinished = true; + } + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(SetTraitorEventStateAction)} -> (State: {State})"; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs index f015fc26b..e9c3a624e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -16,7 +16,8 @@ namespace Barotrauma MainPath, Ruin, Wreck, - BeaconStation + BeaconStation, + NearMainSub } [Serialize("", IsPropertySaveable.Yes, description: "Species name of the character to spawn.")] @@ -60,6 +61,9 @@ namespace Barotrauma [Serialize(true, IsPropertySaveable.Yes, description: "If false, we won't spawn another character if one with the same identifier has already been spawned.")] public bool AllowDuplicates { get; set; } + [Serialize(1, IsPropertySaveable.Yes)] + public int Amount { get; set; } + [Serialize(100.0f, IsPropertySaveable.Yes)] public float Offset { get; set; } @@ -84,6 +88,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes, description: "Should the AI ignore this item. This will prevent outpost NPCs cleaning up or otherwise using important items intended to be left for the players.")] public bool IgnoreByAI { get; set; } + [Serialize(true, IsPropertySaveable.Yes, description: "If disabled, the action will choose a spawn position away from players' views if one is available.")] + public bool AllowInPlayerView { get; set; } + private bool spawned; private Entity spawnedEntity; @@ -159,40 +166,43 @@ namespace Barotrauma ISpatialEntity spawnPos = GetSpawnPos(); if (spawnPos != null) { - Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), humanPrefab.CreateCharacterInfo(), onSpawn: newCharacter => + for (int i = 0; i < Amount; i++) { - if (newCharacter == null) { return; } - newCharacter.HumanPrefab = humanPrefab; - newCharacter.TeamID = TeamID; - newCharacter.EnableDespawn = false; - humanPrefab.GiveItems(newCharacter, newCharacter.Submarine, spawnPos as WayPoint); - if (LootingIsStealing) + Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), humanPrefab.CreateCharacterInfo(), onSpawn: newCharacter => { - foreach (Item item in newCharacter.Inventory.FindAllItems(recursive: true)) + if (newCharacter == null) { return; } + newCharacter.HumanPrefab = humanPrefab; + newCharacter.TeamID = TeamID; + newCharacter.EnableDespawn = false; + humanPrefab.GiveItems(newCharacter, newCharacter.Submarine, spawnPos as WayPoint); + if (LootingIsStealing) { - item.SpawnedInCurrentOutpost = true; - item.AllowStealing = false; + foreach (Item item in newCharacter.Inventory.FindAllItems(recursive: true)) + { + item.SpawnedInCurrentOutpost = true; + item.AllowStealing = false; + } } - } - humanPrefab.InitializeCharacter(newCharacter, spawnPos); - if (!TargetTag.IsEmpty && newCharacter != null) - { - ParentEvent.AddTarget(TargetTag, newCharacter); - } - spawnedEntity = newCharacter; - if (Level.Loaded?.StartOutpost?.Info is { } outPostInfo) - { - outPostInfo.AddOutpostNPCIdentifierOrTag(newCharacter, humanPrefab.Identifier); - foreach (Identifier tag in humanPrefab.GetTags()) + humanPrefab.InitializeCharacter(newCharacter, spawnPos); + if (!TargetTag.IsEmpty && newCharacter != null) { - outPostInfo.AddOutpostNPCIdentifierOrTag(newCharacter, tag); + ParentEvent.AddTarget(TargetTag, newCharacter); + } + spawnedEntity = newCharacter; + if (Level.Loaded?.StartOutpost?.Info is { } outPostInfo) + { + outPostInfo.AddOutpostNPCIdentifierOrTag(newCharacter, humanPrefab.Identifier); + foreach (Identifier tag in humanPrefab.GetTags()) + { + outPostInfo.AddOutpostNPCIdentifierOrTag(newCharacter, tag); + } } - } #if SERVER - newCharacter.LoadTalents(); - GameMain.NetworkMember.CreateEntityEvent(newCharacter, new Character.UpdateTalentsEventData()); + newCharacter.LoadTalents(); + GameMain.NetworkMember.CreateEntityEvent(newCharacter, new Character.UpdateTalentsEventData()); #endif - }); + }); + } } } } @@ -206,14 +216,17 @@ namespace Barotrauma ISpatialEntity spawnPos = GetSpawnPos(); if (spawnPos != null) { - Entity.Spawner.AddCharacterToSpawnQueue(SpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), onSpawn: newCharacter => + for (int i = 0; i < Amount; i++) { - if (!TargetTag.IsEmpty && newCharacter != null) + Entity.Spawner.AddCharacterToSpawnQueue(SpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), onSpawn: newCharacter => { - ParentEvent.AddTarget(TargetTag, newCharacter); - } - spawnedEntity = newCharacter; - }); + if (!TargetTag.IsEmpty && newCharacter != null) + { + ParentEvent.AddTarget(TargetTag, newCharacter); + } + spawnedEntity = newCharacter; + }); + } } } else if (!ItemIdentifier.IsEmpty) @@ -243,7 +256,7 @@ namespace Barotrauma if (spawnInventory == null) { - DebugConsole.ThrowError($"Could not spawn \"{ItemIdentifier}\" in target inventory \"{TargetInventory}\""); + DebugConsole.ThrowError($"Could not spawn \"{ItemIdentifier}\" in target inventory \"{TargetInventory}\" - matching target not found."); } } @@ -252,12 +265,19 @@ namespace Barotrauma ISpatialEntity spawnPos = GetSpawnPos(); if (spawnPos != null) { - Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), onSpawned: onSpawned); + for (int i = 0; i < Amount; i++) + { + Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), onSpawned: onSpawned); + } } } else { - Entity.Spawner.AddItemToSpawnQueue(itemPrefab, spawnInventory, onSpawned: onSpawned); + for (int i = 0; i < Amount; i++) + { + Entity.Spawner.AddItemToSpawnQueue(itemPrefab, spawnInventory, onSpawned: onSpawned); + + } } void onSpawned(Item newItem) { @@ -298,18 +318,27 @@ namespace Barotrauma { if (!SpawnPointTag.IsEmpty) { - List potentialItems = Item.ItemList.FindAll(it => IsValidSubmarineType(SpawnLocation, it.Submarine)); + IEnumerable potentialItems = Item.ItemList.Where(it => IsValidSubmarineType(SpawnLocation, it.Submarine)); + if (!AllowInPlayerView) + { + potentialItems = GetEntitiesNotInPlayerView(potentialItems); + } var item = potentialItems.Where(it => it.HasTag(SpawnPointTag)).GetRandomUnsynced(); if (item != null) { return item; } - var target = ParentEvent.GetTargets(SpawnPointTag).Where(t => IsValidSubmarineType(SpawnLocation, t.Submarine)).GetRandomUnsynced(); + var potentialTargets = ParentEvent.GetTargets(SpawnPointTag).Where(t => IsValidSubmarineType(SpawnLocation, t.Submarine)); + if (!AllowInPlayerView) + { + potentialTargets = GetEntitiesNotInPlayerView(potentialTargets); + } + var target = potentialTargets.GetRandomUnsynced(); if (target != null) { return target; } } SpawnType? spawnPointType = null; if (!ignoreSpawnPointType) { spawnPointType = SpawnPointType; } - return GetSpawnPos(SpawnLocation, spawnPointType, targetModuleTags, SpawnPointTag.ToEnumerable(), requireTaggedSpawnPoint: RequireSpawnPointTag); + return GetSpawnPos(SpawnLocation, spawnPointType, targetModuleTags, SpawnPointTag.ToEnumerable(), requireTaggedSpawnPoint: RequireSpawnPointTag, allowInPlayerView: AllowInPlayerView); } private static bool IsValidSubmarineType(SpawnLocationType spawnLocation, Submarine submarine) @@ -318,16 +347,39 @@ namespace Barotrauma { SpawnLocationType.Any => true, SpawnLocationType.MainSub => submarine == Submarine.MainSub, + SpawnLocationType.NearMainSub => submarine == null, SpawnLocationType.MainPath => submarine == null, - SpawnLocationType.Outpost => submarine is { Info: { IsOutpost: true } }, - SpawnLocationType.Wreck => submarine is { Info: { IsWreck: true } }, - SpawnLocationType.Ruin => submarine is { Info: { IsRuin: true } }, + SpawnLocationType.Outpost => submarine is { Info.IsOutpost: true }, + SpawnLocationType.Wreck => submarine is { Info.IsWreck: true }, + SpawnLocationType.Ruin => submarine is { Info.IsRuin: true }, SpawnLocationType.BeaconStation => submarine?.Info?.BeaconStationInfo != null, _ => throw new NotImplementedException(), }; } - public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable moduleFlags = null, IEnumerable spawnpointTags = null, bool asFarAsPossibleFromAirlock = false, bool requireTaggedSpawnPoint = false) + /// + /// Returns those of the entities that aren't in any player's view. If there are none, all the entities are returned. + /// + private static IEnumerable GetEntitiesNotInPlayerView(IEnumerable entities) where T : ISpatialEntity + { + if (entities.Any(e => !IsInPlayerView(e))) + { + return entities.Where(e => !IsInPlayerView(e)); + } + return entities; + } + + private static bool IsInPlayerView(ISpatialEntity entity) + { + foreach (var character in Character.CharacterList) + { + if (!character.IsPlayer || character.IsDead) { continue; } + if (character.CanSeeTarget(entity)) { return true; } + } + return false; + } + + public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable moduleFlags = null, IEnumerable spawnpointTags = null, bool asFarAsPossibleFromAirlock = false, bool requireTaggedSpawnPoint = false, bool allowInPlayerView = true) { bool requireHull = spawnLocation == SpawnLocationType.MainSub || spawnLocation == SpawnLocationType.Outpost; List potentialSpawnPoints = WayPoint.WayPointList.FindAll(wp => IsValidSubmarineType(spawnLocation, wp.Submarine) && (wp.CurrentHull != null || !requireHull)); @@ -385,6 +437,12 @@ namespace Barotrauma return potentialSpawnPoints.GetRandomUnsynced(); } + if (spawnLocation == SpawnLocationType.MainPath || spawnLocation == SpawnLocationType.NearMainSub) + { + validSpawnPoints = validSpawnPoints.Where(p => + Submarine.Loaded.None(s => ToolBox.GetWorldBounds(s.Borders.Center, s.Borders.Size).ContainsWorld(p.WorldPosition))); + } + //avoid using waypoints if there's any actual spawnpoints available if (validSpawnPoints.Any(wp => wp.SpawnType != SpawnType.Path)) { @@ -401,7 +459,27 @@ namespace Barotrauma } } - if (asFarAsPossibleFromAirlock && airlockSpawnPoints.Any()) + if (!allowInPlayerView) + { + validSpawnPoints = GetEntitiesNotInPlayerView(validSpawnPoints); + } + + if (spawnLocation == SpawnLocationType.NearMainSub && Submarine.MainSub != null) + { + WayPoint closestPoint = validSpawnPoints.First(); + float closestDist = float.PositiveInfinity; + foreach (WayPoint wp in validSpawnPoints) + { + float dist = Vector2.DistanceSquared(wp.WorldPosition, Submarine.MainSub.WorldPosition); + if (dist < closestDist) + { + closestDist = dist; + closestPoint = wp; + } + } + return closestPoint; + } + else if (asFarAsPossibleFromAirlock && airlockSpawnPoints.Any()) { WayPoint furthestPoint = validSpawnPoints.First(); float furthestDist = 0.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs index ef66959e5..bb17f02c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs @@ -1,5 +1,6 @@ using Barotrauma.Extensions; using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -24,14 +25,25 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes)] public bool AllowHiddenItems { get; set; } + [Serialize(false, IsPropertySaveable.Yes)] + public bool ChooseRandom { get; set; } + + [Serialize(0.0f, IsPropertySaveable.Yes, description: "If larger than 0, the specified percentage of the matching targets are tagged. Between 0-100.")] + public float ChoosePercentage { get; set; } + private bool isFinished = false; + private bool targetNotFound = false; + public TagAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { Taggers = new (string k, Action v)[] { ("players", v => TagPlayers()), ("player", v => TagPlayers()), + ("traitor", v => TagTraitors()), + ("nontraitor", v => TagNonTraitors()), + ("nontraitorplayer", v => TagNonTraitorPlayers()), ("bot", v => TagBots(playerCrewOnly: false)), ("crew", v => TagCrew()), ("humanprefabidentifier", TagHumansByIdentifier), @@ -40,8 +52,10 @@ namespace Barotrauma ("structurespecialtag", TagStructuresBySpecialTag), ("itemidentifier", TagItemsByIdentifier), ("itemtag", TagItemsByTag), + ("hull", v => TagHulls()), ("hullname", TagHullsByName), ("submarine", TagSubmarinesByType), + ("eventtag", TagByEventTag), }.Select(t => (t.k.ToIdentifier(), t.v)).ToImmutableDictionary(); } @@ -54,34 +68,44 @@ namespace Barotrauma isFinished = false; } + private void TagByEventTag(Identifier eventTag) + { + AddTarget(Tag, ParentEvent.GetTargets(eventTag).Where(t => SubmarineTypeMatches(t.Submarine))); + } + private void TagPlayers() { - if (IgnoreIncapacitatedCharacters) - { - ParentEvent.AddTargetPredicate(Tag, e => e is Character c && c.IsPlayer && !c.IsIncapacitated); - } - else - { - ParentEvent.AddTargetPredicate(Tag, e => e is Character c && c.IsPlayer); - } + AddTargetPredicate(Tag, e => e is Character c && c.IsPlayer && (!c.IsIncapacitated || !IgnoreIncapacitatedCharacters)); + } + + private void TagTraitors() + { + AddTargetPredicate(Tags.Traitor, e => e is Character c && (c.IsPlayer || c.IsBot) && c.IsTraitor && !c.IsIncapacitated); + } + + private void TagNonTraitors() + { + AddTargetPredicate(Tags.NonTraitor, e => e is Character c && (c.IsPlayer || c.IsBot) && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated); + } + + private void TagNonTraitorPlayers() + { + AddTargetPredicate(Tags.NonTraitorPlayer, e => e is Character c && c.IsPlayer && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated); } private void TagBots(bool playerCrewOnly) { - if (IgnoreIncapacitatedCharacters) - { - ParentEvent.AddTargetPredicate(Tag, e => e is Character c && c.IsBot && !c.IsIncapacitated && (!playerCrewOnly || c.TeamID == CharacterTeamType.Team1)); - } - else - { - ParentEvent.AddTargetPredicate(Tag, e => e is Character c && c.IsBot && (!playerCrewOnly || c.TeamID == CharacterTeamType.Team1)); - } + AddTargetPredicate(Tag, e => + e is Character c && + c.IsBot && + (!c.IsIncapacitated || !IgnoreIncapacitatedCharacters) && + (!playerCrewOnly || c.TeamID == CharacterTeamType.Team1)); } private void TagCrew() { #if CLIENT - GameMain.GameSession.CrewManager.GetCharacters().ForEach(c => ParentEvent.AddTarget(Tag, c)); + AddTarget(Tag, GameMain.GameSession.CrewManager.GetCharacters()); #else TagPlayers(); TagBots(playerCrewOnly: true); @@ -90,54 +114,47 @@ namespace Barotrauma private void TagHumansByIdentifier(Identifier identifier) { - foreach (Character c in Character.CharacterList) - { - if (c.HumanPrefab?.Identifier == identifier) - { - ParentEvent.AddTarget(Tag, c); - } - } + AddTarget(Tag, Character.CharacterList.Where(c => c.HumanPrefab?.Identifier == identifier)); } private void TagHumansByJobIdentifier(Identifier jobIdentifier) { - foreach (Character c in Character.CharacterList) - { - if (c.HasJob(jobIdentifier)) - { - ParentEvent.AddTarget(Tag, c); - } - } + AddTarget(Tag, Character.CharacterList.Where(c => c.HasJob(jobIdentifier))); } private void TagStructuresByIdentifier(Identifier identifier) { - ParentEvent.AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.Prefab.Identifier == identifier); + AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.Prefab.Identifier == identifier); } private void TagStructuresBySpecialTag(Identifier tag) { - ParentEvent.AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.SpecialTag.ToIdentifier() == tag); + AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.SpecialTag.ToIdentifier() == tag); } private void TagItemsByIdentifier(Identifier identifier) { - ParentEvent.AddTargetPredicate(Tag, e => e is Item it && IsValidItem(it) && it.Prefab.Identifier == identifier); + AddTargetPredicate(Tag, e => e is Item it && IsValidItem(it) && it.Prefab.Identifier == identifier); } private void TagItemsByTag(Identifier tag) { - ParentEvent.AddTargetPredicate(Tag, e => e is Item it && IsValidItem(it) && it.HasTag(tag)); + AddTargetPredicate(Tag, e => e is Item it && IsValidItem(it) && it.HasTag(tag)); + } + + private void TagHulls() + { + AddTargetPredicate(Tag, e => e is Hull h && SubmarineTypeMatches(h.Submarine)); } private void TagHullsByName(Identifier name) { - ParentEvent.AddTargetPredicate(Tag, e => e is Hull h && SubmarineTypeMatches(h.Submarine) && h.RoomName.Contains(name.Value, StringComparison.OrdinalIgnoreCase)); + AddTargetPredicate(Tag, e => e is Hull h && SubmarineTypeMatches(h.Submarine) && h.RoomName.Contains(name.Value, StringComparison.OrdinalIgnoreCase)); } private void TagSubmarinesByType(Identifier type) { - ParentEvent.AddTargetPredicate(Tag, e => e is Submarine s && SubmarineTypeMatches(s) && (type.IsEmpty || type == s.Info?.Type.ToIdentifier())); + AddTargetPredicate(Tag, e => e is Submarine s && SubmarineTypeMatches(s) && (type.IsEmpty || type == s.Info?.Type.ToIdentifier())); } private bool IsValidItem(Item it) @@ -152,7 +169,7 @@ namespace Barotrauma switch (sub.Info.Type) { case Barotrauma.SubmarineType.Player: - return SubmarineType.HasFlag(SubType.Player); + return SubmarineType.HasFlag(SubType.Player) && sub != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle; case Barotrauma.SubmarineType.Outpost: case Barotrauma.SubmarineType.OutpostModule: return SubmarineType.HasFlag(SubType.Outpost); @@ -165,14 +182,86 @@ namespace Barotrauma } } + private void AddTargetPredicate(Identifier tag, Predicate predicate) + { + if (ChoosePercentage > 0.0f) + { + TagPercentage(tag, Entity.GetEntities().Where(e => predicate(e))); + } + else if (ChooseRandom) + { + TagRandom(tag, Entity.GetEntities().Where(e => predicate(e))); + } + else + { + ParentEvent.AddTargetPredicate(tag, predicate); + } + } + + private void AddTarget(Identifier tag, IEnumerable entities) + { + if (entities.None()) + { + targetNotFound = true; + return; + } + if (ChoosePercentage > 0.0f) + { + TagPercentage(tag, entities); + } + else if (ChooseRandom) + { + TagRandom(tag, entities); + } + else + { + foreach (var entity in entities) + { + ParentEvent.AddTarget(tag, entity); + } + } + } + + private List tempEntities; + private void TagPercentage(Identifier tag, IEnumerable entities) + { + if (entities.None()) + { + targetNotFound = true; + return; + } + + int amountToChoose = (int)Math.Ceiling(entities.Count() * (ChoosePercentage / 100.0f)); + + tempEntities ??= new List(); + tempEntities.Clear(); + for (int i = 0; i < amountToChoose; i++) + { + var entity = entities.GetRandomUnsynced(); + tempEntities.Remove(entity); + ParentEvent.AddTarget(tag, entity); + } + } + + private void TagRandom(Identifier tag, IEnumerable entities) + { + if (entities.None()) + { + targetNotFound = true; + return; + } + ParentEvent.AddTarget(tag, entities.GetRandomUnsynced()); + } + private readonly ImmutableDictionary> Taggers; public override void Update(float deltaTime) { - if (isFinished) { return; } + if (isFinished || targetNotFound) { return; } string[] criteriaSplit = Criteria.Split(';'); + targetNotFound = false; foreach (string entry in criteriaSplit) { string[] kvp = entry.Split(':'); @@ -190,7 +279,7 @@ namespace Barotrauma } } - isFinished = true; + isFinished = !targetNotFound; } public override string ToDebugString() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs index 8f7a849ac..5c8bce3d0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs @@ -2,7 +2,6 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -50,6 +49,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes, description: "If true and using multiple targets, all targets must be inside/outside the radius.")] public bool CheckAllTargets { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "If true, interacting with the target will make the character select it.")] + public bool SelectOnTrigger { get; set; } + private float distance; public TriggerAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } @@ -186,7 +188,12 @@ namespace Barotrauma { if (npc != null) { - if (npc.CampaignInteractionType != CampaignMode.InteractionType.Examine) + if (npc.CampaignInteractionType == CampaignMode.InteractionType.Talk) + { + //if the NPC has a conversation available, don't assign the trigger until the conversation is done + continue; + } + else if (npc.CampaignInteractionType != CampaignMode.InteractionType.Examine) { if (!npcsOrItems.Any(n => n.TryGet(out Character npc2) && npc2 == npc)) { @@ -213,7 +220,8 @@ namespace Barotrauma { npcsOrItems.Add(item); } - item.CampaignInteractionType = CampaignMode.InteractionType.Examine; + item.AssignCampaignInteractionType(CampaignMode.InteractionType.Examine, + GameMain.NetworkMember?.ConnectedClients.Where(c => c.Character != null && targets2.Contains(c.Character))); if (player.SelectedItem == item || player.SelectedSecondaryItem == item || (player.Inventory != null && player.Inventory.Contains(item)) || @@ -276,7 +284,7 @@ namespace Barotrauma } else if (npcOrItem.TryGet(out Item item)) { - item.CampaignInteractionType = CampaignMode.InteractionType.None; + item.AssignCampaignInteractionType(CampaignMode.InteractionType.None); } } } @@ -352,6 +360,37 @@ namespace Barotrauma ParentEvent.AddTarget(ApplyToTarget2, entity2); } + Character player = null; + Entity target = null; + if (entity1 is Character { IsPlayer: true }) + { + player = entity1 as Character; + target = entity2; + } + else if (entity2 is Character { IsPlayer: true }) + { + player = entity2 as Character; + target = entity1; + } + if (player != null && SelectOnTrigger) + { + if (target is Character targetCharacter) + { + player.SelectCharacter(targetCharacter); + } + else if (target is Item targetItem) + { + if (targetItem.IsSecondaryItem) + { + player.SelectedSecondaryItem = targetItem; + } + else + { + player.SelectedItem = targetItem; + } + } + } + isRunning = false; isFinished = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialHighlightAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialHighlightAction.cs index eebac860d..190f7fdc0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialHighlightAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialHighlightAction.cs @@ -10,7 +10,13 @@ partial class TutorialHighlightAction : EventAction private bool isFinished; - public TutorialHighlightAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } + public TutorialHighlightAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + if (GameMain.NetworkMember != null) + { + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": {nameof(TutorialHighlightAction)} is not supported in multiplayer."); + } + } public override void Update(float deltaTime) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemFabricatedAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemFabricatedAction.cs new file mode 100644 index 000000000..601981cb6 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemFabricatedAction.cs @@ -0,0 +1,75 @@ +#nullable enable + +using Barotrauma.Items.Components; +using System.Linq; + +namespace Barotrauma +{ + class WaitForItemFabricatedAction : EventAction + { + [Serialize("", IsPropertySaveable.Yes)] + public Identifier CharacterTag { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier ItemIdentifier { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier ItemTag { get; set; } + + [Serialize(1, IsPropertySaveable.Yes)] + public int Amount { get; set; } + + [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the fabricated item(s).")] + public Identifier ApplyTagToItem { get; set; } + + private int counter; + + public WaitForItemFabricatedAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + if (ItemTag.IsEmpty && ItemIdentifier.IsEmpty) + { + DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". {nameof(WaitForItemFabricatedAction)} does't define either a tag or an identifier of the item to check."); + } + foreach (var item in Item.ItemList) + { + var fabricator = item.GetComponent(); + if (fabricator != null) + { + fabricator.OnItemFabricated += OnItemFabricated; + } + } + } + + public void OnItemFabricated(Item item, Character character) + { + if (item == null) { return; } + if (!CharacterTag.IsEmpty) + { + if (!ParentEvent.GetTargets(CharacterTag).Contains(character)) { return; } + } + if (item.ContainerIdentifier == ItemTag || item.HasTag(ItemTag)) + { + if (!ApplyTagToItem.IsEmpty) + { + ParentEvent.AddTarget(ApplyTagToItem, item); + } + counter++; + } + } + + public override bool IsFinished(ref string goTo) + { + return counter >= Amount; + } + + public override void Reset() + { + counter = 0; + } + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(counter >= Amount)} {nameof(WaitForItemFabricatedAction)} -> ({ItemTag}, {counter}/{Amount})"; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemUsedAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemUsedAction.cs new file mode 100644 index 000000000..d7e48a299 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemUsedAction.cs @@ -0,0 +1,153 @@ +#nullable enable + +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + class WaitForItemUsedAction : EventAction + { + [Serialize("", IsPropertySaveable.Yes)] + public Identifier ItemTag { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier UserTag { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetItemComponent { get; set; } + + [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the target item when it's used.")] + public Identifier ApplyTagToItem { get; set; } + + [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the user when the target item is used.")] + public Identifier ApplyTagToUser{ get; set; } + + [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the hull the target item is inside when the item is used.")] + public Identifier ApplyTagToHull { get; set; } + + [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the hull the target item is inside, and all the hulls it's linked to, when the item is used.")] + public Identifier ApplyTagToLinkedHulls { get; set; } + + private bool isFinished; + + private readonly HashSet targets = new HashSet(); + private readonly HashSet targetComponents = new HashSet(); + + private Identifier onUseEventIdentifier; + private Identifier OnUseEventIdentifier + { + get + { + if (onUseEventIdentifier.IsEmpty) + { + onUseEventIdentifier = (ParentEvent.Prefab.Identifier + ParentEvent.Actions.IndexOf(this).ToString()).ToIdentifier(); + } + return onUseEventIdentifier; + } + } + + public WaitForItemUsedAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + if (ItemTag.IsEmpty) + { + DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". {nameof(ItemTag)} not set in {nameof(WaitForItemUsedAction)}."); + } + } + + private void OnItemUsed(Item item, Character user) + { + if (!ApplyTagToItem.IsEmpty) + { + ParentEvent.AddTarget(ApplyTagToItem, item); + } + if (!ApplyTagToUser.IsEmpty && user != null) + { + ParentEvent.AddTarget(ApplyTagToUser, user); + } + if (item.CurrentHull != null) + { + if (!ApplyTagToHull.IsEmpty) + { + ParentEvent.AddTarget(ApplyTagToHull, item.CurrentHull); + } + if (!ApplyTagToLinkedHulls.IsEmpty) + { + ParentEvent.AddTarget(ApplyTagToLinkedHulls, item.CurrentHull); + foreach (var linkedHull in item.CurrentHull.GetLinkedEntities()) + { + ParentEvent.AddTarget(ApplyTagToLinkedHulls, linkedHull); + } + } + } + + DeregisterTargets(); + isFinished = true; + } + + public override void Update(float deltaTime) + { + TryRegisterTargets(); + } + + private void TryRegisterTargets() + { + foreach (Entity target in ParentEvent.GetTargets(ItemTag)) + { + //already registered, ignore + if (targets.Contains(target)) { continue; } + if (target is not Item item) { continue; } + if (TargetItemComponent.IsEmpty) + { + item.GetComponents().ForEach(ic => Register(ic)); + } + else if (item.Components.FirstOrDefault(ic => ic.Name == TargetItemComponent) is ItemComponent targetItemComponent) + { + Register(targetItemComponent); + } + else + { +#if DEBUG + DebugConsole.ThrowError($"Failed to find the component {TargetItemComponent} on item {item.Prefab.Identifier}"); +#endif + } + } + void Register(ItemComponent ic) + { + targets.Add(ic.Item); + targetComponents.Add(ic); + ic.OnUsed.RegisterOverwriteExisting( + OnUseEventIdentifier, + i => { OnItemUsed(i.Item, i.User); }); + } + } + + private void DeregisterTargets() + { + foreach (ItemComponent ic in targetComponents) + { + ic.OnUsed.Deregister(OnUseEventIdentifier); + } + targetComponents.Clear(); + targets.Clear(); + } + + + public override bool IsFinished(ref string goTo) + { + return isFinished; + } + + public override void Reset() + { + isFinished = false; + DeregisterTargets(); + } + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(WaitForItemUsedAction)} -> ({ItemTag})"; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventLog.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventLog.cs new file mode 100644 index 000000000..eea50bcf0 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventLog.cs @@ -0,0 +1,65 @@ +#nullable enable + +using Barotrauma.Networking; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + /// + /// Used to store logs of scripted events (a sort of "quest log") + /// + partial class EventLog + { + public class Event + { + public readonly Identifier EventIdentifier; + public readonly List Entries = new List(); + + public Event(Identifier eventPrefabId) + { + EventIdentifier = eventPrefabId; + } + } + + public class Entry + { + public readonly Identifier Identifier; + public string Text; + + public Entry(Identifier identifier, string text) + { + Identifier = identifier; + Text = text; + } + } + + private readonly Dictionary events = new Dictionary(); + + private bool TryAddEntryInternal(Identifier eventPrefabId, Identifier entryId, string text) + { + if (!events.TryGetValue(eventPrefabId, out Event? ev)) + { + ev = new Event(eventPrefabId); + events.Add(eventPrefabId, ev); + } + Entry? entry = ev.Entries.FirstOrDefault(e => e.Identifier == entryId); + if (entry == null) + { + ev.Entries.Add(new Entry(entryId, text)); + return true; + } + else if (entry.Text != text) + { + entry.Text = text; + return true; + } + return false; + } + + public void Clear() + { + events.Clear(); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index b0655f113..c7c89bd26 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -17,9 +17,24 @@ namespace Barotrauma CONVERSATION_SELECTED_OPTION, STATUSEFFECT, MISSION, - UNLOCKPATH + UNLOCKPATH, + EVENTLOG, + EVENTOBJECTIVE, } + [NetworkSerialize] + public readonly record struct NetEventLogEntry(Identifier EventPrefabId, Identifier LogEntryId, string Text) : INetSerializableStruct; + + [NetworkSerialize] + public readonly record struct NetEventObjective( + EventObjectiveAction.SegmentActionType Type, + Identifier Identifier, + Identifier ObjectiveTag, + Identifier TextTag, + Identifier ParentObjectiveId, + bool CanBeCompleted) : INetSerializableStruct; + + const float IntensityUpdateInterval = 5.0f; const float CalculateDistanceTraveledInterval = 5.0f; @@ -95,7 +110,7 @@ namespace Barotrauma get { return musicIntensity; } } - public List ActiveEvents + public IEnumerable ActiveEvents { get { return activeEvents; } } @@ -119,6 +134,8 @@ namespace Barotrauma private readonly List timeStamps = new List(); public void AddTimeStamp(Event e) => timeStamps.Add(new TimeStamp(e)); + public readonly EventLog EventLog = new EventLog(); + public EventManager() { isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; @@ -198,7 +215,7 @@ namespace Barotrauma if (unlockPathEventPrefab != null) { var newEvent = unlockPathEventPrefab.CreateInstance(); - ActiveEvents.Add(newEvent); + activeEvents.Add(newEvent); } else { @@ -256,7 +273,18 @@ namespace Barotrauma CumulativeMonsterStrengthWrecks = 0; CumulativeMonsterStrengthCaves = 0; } - + + public void ActivateEvent(Event newEvent) + { + activeEvents.Add(newEvent); + newEvent.Init(); + } + + public void ClearEvents() + { + activeEvents.Clear(); + } + private void SelectSettings() { if (!EventManagerSettings.Prefabs.Any()) @@ -364,6 +392,14 @@ namespace Barotrauma } } + public void TriggerOnEndRoundActions() + { + foreach (var ev in activeEvents) + { + (ev as ScriptedEvent)?.OnRoundEndAction?.Update(1.0f); + } + } + public void EndRound() { pendingEventSets.Clear(); @@ -478,14 +514,6 @@ namespace Barotrauma } } - bool isPrefabSuitable(EventPrefab e) => - (e.BiomeIdentifier.IsEmpty || e.BiomeIdentifier == level.LevelData?.Biome?.Identifier) && - !level.LevelData.NonRepeatableEvents.Contains(e.Identifier) && - isFactionSuitable(e.Faction); - - bool isFactionSuitable(Identifier factionId) => - factionId.IsEmpty || factionId == level.StartLocation?.Faction?.Prefab.Identifier || factionId == level.StartLocation?.SecondaryFaction?.Prefab.Identifier; - foreach (var subEventPrefab in eventSet.EventPrefabs) { foreach (Identifier missingId in subEventPrefab.GetMissingIdentifiers()) @@ -495,7 +523,7 @@ namespace Barotrauma } var suitablePrefabSubsets = eventSet.EventPrefabs.Where( - e => isFactionSuitable(e.Faction) && e.EventPrefabs.Any(isPrefabSuitable)).ToArray(); + e => IsFactionSuitable(e.Faction, level) && e.EventPrefabs.Any(ep => IsSuitable(ep, level))).ToArray(); for (int i = 0; i < applyCount; i++) { @@ -512,7 +540,7 @@ namespace Barotrauma (IEnumerable eventPrefabs, float commonness, float probability) = subEventPrefab; if (eventPrefabs != null && random.NextDouble() <= probability) { - var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, random); + var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(e => IsSuitable(e, level)), e => e.Commonness, random); var newEvent = eventPrefab.CreateInstance(); if (newEvent == null) { continue; } newEvent.RandomSeed = randomSeed; @@ -529,10 +557,25 @@ namespace Barotrauma } if (eventSet.ChildSets.Any()) { - var newEventSet = SelectRandomEvents(eventSet.ChildSets, random: random); - if (newEventSet != null) + int setCount = eventSet.SubSetCount; + if (setCount > 1) { - CreateEvents(newEventSet); + var unusedSets = eventSet.ChildSets.ToList(); + for (int j = 0; j < setCount; j++) + { + var newEventSet = SelectRandomEvents(unusedSets, random: random); + if (newEventSet == null) { break; } + unusedSets.Remove(newEventSet); + CreateEvents(newEventSet); + } + } + else + { + var newEventSet = SelectRandomEvents(eventSet.ChildSets, random: random); + if (newEventSet != null) + { + CreateEvents(newEventSet); + } } } } @@ -542,7 +585,7 @@ namespace Barotrauma { if (random.NextDouble() > probability) { continue; } - var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, random); + var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(e => IsSuitable(e, level)), e => e.Commonness, random); var newEvent = eventPrefab.CreateInstance(); if (newEvent == null) { continue; } if (!selectedEvents.ContainsKey(eventSet)) @@ -636,6 +679,23 @@ namespace Barotrauma return null; } + public static bool IsSuitable(EventPrefab e, Level level) + { + return IsLevelSuitable(e, level) && IsFactionSuitable(e.Faction, level); + } + + public static bool IsLevelSuitable(EventPrefab e, Level level) + { + return + (e.BiomeIdentifier.IsEmpty || e.BiomeIdentifier == level.LevelData?.Biome?.Identifier) && + !level.LevelData.NonRepeatableEvents.Contains(e.Identifier); + } + + private static bool IsFactionSuitable(Identifier factionId, Level level) + { + return factionId.IsEmpty || factionId == level.StartLocation?.Faction?.Prefab.Identifier || factionId == level.StartLocation?.SecondaryFaction?.Prefab.Identifier; + } + private static bool IsValidForLevel(EventSet eventSet, Level level) { return @@ -1031,35 +1091,6 @@ namespace Barotrauma } } - /// - /// Finds all actions in a ScriptedEvent - /// - private static List> FindActions(ScriptedEvent scriptedEvent) - { - var list = new List>(); - foreach (EventAction eventAction in scriptedEvent.Actions) - { - list.AddRange(FindActionsRecursive(eventAction)); - } - - return list; - - static List> FindActionsRecursive(EventAction eventAction, int ident = 1) - { - var eventActions = new List> { Tuple.Create(ident, eventAction) }; - - ident++; - - foreach (var action in eventAction.GetSubActions()) - { - eventActions.AddRange(FindActionsRecursive(action, ident)); - } - - return eventActions; - } - } - - /// /// Get the entity that should be used in determining how far the player has progressed in the level. /// = The submarine or player character that has progressed the furthest. diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs index 7bd5e7e01..e1c076d3c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs @@ -1,7 +1,6 @@ using System; using System.Linq; using System.Reflection; -using System.Reflection.Emit; namespace Barotrauma { @@ -16,12 +15,25 @@ namespace Barotrauma public readonly float Commonness; public readonly Identifier BiomeIdentifier; public readonly Identifier Faction; - public readonly float SpawnDistance; + + public readonly LocalizedString Name; public readonly bool UnlockPathEvent; public readonly string UnlockPathTooltip; public readonly int UnlockPathReputation; + public static EventPrefab Create(ContentXElement element, RandomEventsFile file, Identifier fallbackIdentifier = default) + { + if (element.NameAsIdentifier() == nameof(TraitorEvent)) + { + return new TraitorEventPrefab(element, file, fallbackIdentifier); + } + else + { + return new EventPrefab(element, file, fallbackIdentifier); + } + } + public EventPrefab(ContentXElement element, RandomEventsFile file, Identifier fallbackIdentifier = default) : base(file, element.GetAttributeIdentifier("identifier", fallbackIdentifier)) { @@ -40,6 +52,8 @@ namespace Barotrauma DebugConsole.ThrowError("Could not find an event class of the type \"" + ConfigElement.Name + "\"."); } + Name = TextManager.Get($"eventname.{Identifier}").Fallback(Identifier.ToString()); + BiomeIdentifier = ConfigElement.GetAttributeIdentifier("biome", Identifier.Empty); Faction = ConfigElement.GetAttributeIdentifier("faction", Identifier.Empty); Commonness = element.GetAttributeFloat("commonness", 1.0f); @@ -49,19 +63,17 @@ namespace Barotrauma UnlockPathEvent = element.GetAttributeBool("unlockpathevent", false); UnlockPathTooltip = element.GetAttributeString("unlockpathtooltip", "lockedpathtooltip"); UnlockPathReputation = element.GetAttributeInt("unlockpathreputation", 0); - - SpawnDistance = element.GetAttributeFloat("spawndistance", 0); } public bool TryCreateInstance(out T instance) where T : Event { instance = CreateInstance() as T; - return instance is T; + return instance is not null; } public Event CreateInstance() { - ConstructorInfo constructor = EventType.GetConstructor(new[] { typeof(EventPrefab) }); + ConstructorInfo constructor = EventType.GetConstructor(new[] { GetType() }); Event instance = null; try { @@ -79,7 +91,7 @@ namespace Barotrauma public override string ToString() { - return $"EventPrefab ({Identifier})"; + return $"{nameof(EventPrefab)} ({Identifier})"; } public static EventPrefab GetUnlockPathEvent(Identifier biomeIdentifier, Faction faction) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index b5409cc22..e43b63056 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -94,6 +94,7 @@ namespace Barotrauma public readonly bool ChooseRandom; private readonly int eventCount = 1; + public readonly int SubSetCount = 1; private readonly Dictionary overrideEventCount = new Dictionary(); /// @@ -280,6 +281,7 @@ namespace Barotrauma ChooseRandom = element.GetAttributeBool("chooserandom", false); eventCount = element.GetAttributeInt("eventcount", 1); + SubSetCount = element.GetAttributeInt("setcount", 1); Exhaustible = element.GetAttributeBool("exhaustible", false); MinDistanceTraveled = element.GetAttributeFloat("mindistancetraveled", 0.0f); MinMissionTime = element.GetAttributeFloat("minmissiontime", 0.0f); @@ -295,7 +297,7 @@ namespace Barotrauma OncePerLevel = element.GetAttributeBool("onceperlevel", element.GetAttributeBool("onceperoutpost", false)); TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true); IsCampaignSet = element.GetAttributeBool("campaign", LevelType == LevelData.LevelType.Outpost || (parentSet?.IsCampaignSet ?? false)); - ResetTime = element.GetAttributeFloat("resettime", 0); + ResetTime = element.GetAttributeFloat(nameof(ResetTime), parentSet?.ResetTime ?? 0); CampaignTutorialOnly = element.GetAttributeBool(nameof(CampaignTutorialOnly), false); ForceAtDiscoveredNr = element.GetAttributeInt(nameof(ForceAtDiscoveredNr), -1); @@ -474,7 +476,7 @@ namespace Barotrauma if (eventPrefab.EventType == typeof(MonsterEvent) && eventPrefab.TryCreateInstance(out MonsterEvent monsterEvent)) { if (filter != null && !filter(monsterEvent)) { return; } - float spawnProbability = monsterEvent.Prefab.Probability; + float spawnProbability = monsterEvent.Prefab?.Probability ?? 0.0f; if (Rand.Value() > spawnProbability) { return; } int count = Rand.Range(monsterEvent.MinAmount, monsterEvent.MaxAmount + 1); if (count <= 0) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs index 58d243d43..a208a7e50 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs @@ -16,7 +16,7 @@ namespace Barotrauma protected readonly HashSet requireKill = new HashSet(); protected readonly HashSet requireRescue = new HashSet(); - private readonly string itemTag; + private readonly Identifier itemTag; private readonly XElement itemConfig; private readonly List items = new List(); @@ -90,7 +90,7 @@ namespace Barotrauma hostagesKilledMessage = TextManager.Get(msgTag).Fallback(msgTag); itemConfig = prefab.ConfigElement.GetChildElement("Items"); - itemTag = prefab.ConfigElement.GetAttributeString("targetitem", ""); + itemTag = prefab.ConfigElement.GetAttributeIdentifier("targetitem", Identifier.Empty); } protected override void StartMissionSpecific(Level level) @@ -118,7 +118,7 @@ namespace Barotrauma private void InitItems(Submarine submarine) { - if (!string.IsNullOrEmpty(itemTag)) + if (!itemTag.IsEmpty) { var itemsToDestroy = Item.ItemList.FindAll(it => it.Submarine?.Info.Type != SubmarineType.Player && it.HasTag(itemTag)); if (!itemsToDestroy.Any()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs index d0cf6aea3..224dc40f1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs @@ -73,7 +73,7 @@ namespace Barotrauma { get { - if (level.BeaconStation == null) + if (level.BeaconStation == null || state > 0) { yield break; } @@ -95,9 +95,10 @@ namespace Barotrauma foreach (Item item in Item.ItemList) { if (!connectedSubs.Contains(item.Submarine) || item.Submarine?.Info is { IsPlayer: true }) { continue; } - if (item.GetComponent() != null || + bool isReactor = item.GetComponent() != null; + if ((isReactor && GameMain.GameSession is not { TraitorsEnabled: true }) || + item.GetComponent() != null || item.GetComponent() != null || - item.GetComponent() != null || item.GetComponent() != null) { item.InvulnerableToDamage = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs index 847a9a923..71dcf88a7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs @@ -262,6 +262,12 @@ namespace Barotrauma SpawnedInCurrentOutpost = true, AllowStealing = false }; + item.AddTag("cargomission"); + item.AddTag(Prefab.Identifier); + foreach (var tag in Prefab.Tags) + { + item.AddTag(tag); + } item.FindHull(); items.Add(item); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs index daa064131..8eaba7820 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs @@ -23,6 +23,7 @@ namespace Barotrauma private readonly CharacterPrefab minionPrefab; private readonly Identifier spawnPointTag; + private WayPoint bossSpawnPoint; private readonly Identifier destructibleItemTag; private readonly string endCinematicSound; @@ -68,7 +69,13 @@ namespace Barotrauma { if (boss != null && !boss.Removed) { + Vector2 prevPos = boss.AnimController.Collider.SimPosition; boss.AnimController.ColliderIndex = 1; + if (bossSpawnPoint != null) + { + //ensure the new collider stays in the same position (the 2nd one has a different shape than the 1st one) + boss.AnimController.Collider.SetTransform(prevPos, 0.0f); + } } }, delay: wakeUpCinematicDelay + bossWakeUpDelay + 2); } @@ -142,21 +149,21 @@ namespace Barotrauma protected override void StartMissionSpecific(Level level) { - var spawnPoint = WayPoint.WayPointList.FirstOrDefault(wp => wp.Tags.Contains(spawnPointTag)); - if (spawnPoint == null) + bossSpawnPoint = WayPoint.WayPointList.FirstOrDefault(wp => wp.Tags.Contains(spawnPointTag)); + if (bossSpawnPoint == null) { DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Could not find a spawn point \"{spawnPointTag}\"."); return; } if (!IsClient) { - boss = Character.Create(bossPrefab.Identifier, spawnPoint.WorldPosition, ToolBox.RandomSeed(8), createNetworkEvent: false); + boss = Character.Create(bossPrefab.Identifier, bossSpawnPoint.WorldPosition, ToolBox.RandomSeed(8), createNetworkEvent: false); var minionList = new List(); float angle = 0; float angleStep = MathHelper.TwoPi / Math.Max(minionCount, 1); for (int i = 0; i < minionCount; i++) { - minionList.Add(Character.Create(minionPrefab.Identifier, MathUtils.GetPointOnCircumference(spawnPoint.WorldPosition, minionScatter, angle), ToolBox.RandomSeed(8), createNetworkEvent: false)); + minionList.Add(Character.Create(minionPrefab.Identifier, MathUtils.GetPointOnCircumference(bossSpawnPoint.WorldPosition, minionScatter, angle), ToolBox.RandomSeed(8), createNetworkEvent: false)); angle += angleStep; } SwarmBehavior.CreateSwarm(minionList.Cast()); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs index 51dd38d21..10fa64cb8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs @@ -315,27 +315,21 @@ namespace Barotrauma } } - private bool Survived(Character character) + private static bool Survived(Character character) { return IsAlive(character) && character.CurrentHull?.Submarine != null && (character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)); } - private bool IsAlive(Character character) + private static bool IsAlive(Character character) { return character != null && !character.Removed && !character.IsDead; } - private bool IsCaptured(Character character) - { - return character.LockHands && character.HasTeamChange(TerroristTeamChangeIdentifier); - } - protected override bool DetermineCompleted() { if (Submarine.MainSub != null && Submarine.MainSub.AtEndExit) { - bool terroristsSurvived = terroristCharacters.Any(c => Survived(c) && !IsCaptured(c)); bool friendliesSurvived = characters.Except(terroristCharacters).All(c => Survived(c)); bool vipDied = false; @@ -345,7 +339,7 @@ namespace Barotrauma vipDied = !Survived(vipCharacter); } - if (friendliesSurvived && !terroristsSurvived && !vipDied) + if (friendliesSurvived && !vipDied) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs index 90ac22989..fbbdc98c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs @@ -139,7 +139,7 @@ namespace Barotrauma { if (cave.Area.Contains(spawnedResource.WorldPosition)) { - cave.DisplayOnSonar = true; + cave.MissionsToDisplayOnSonar.Add(this); caves.Add(cave); break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index 5da1a323a..b2976762c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -95,7 +95,7 @@ namespace Barotrauma } } - public Dictionary ReputationRewards + public ImmutableList ReputationRewards { get { return Prefab.ReputationRewards; } } @@ -268,7 +268,7 @@ namespace Barotrauma delayedTriggerEvents.Clear(); foreach (string categoryToShow in Prefab.UnhideEntitySubCategories) { - foreach (MapEntity entityToShow in MapEntity.mapEntityList.Where(me => me.Prefab?.HasSubCategory(categoryToShow) ?? false)) + foreach (MapEntity entityToShow in MapEntity.MapEntityList.Where(me => me.Prefab?.HasSubCategory(categoryToShow) ?? false)) { entityToShow.HiddenInGame = false; } @@ -353,8 +353,7 @@ namespace Barotrauma if (GameMain.GameSession?.EventManager != null) { var newEvent = eventPrefab.CreateInstance(); - GameMain.GameSession.EventManager.ActiveEvents.Add(newEvent); - newEvent.Init(); + GameMain.GameSession.EventManager.ActivateEvent(newEvent); } } @@ -372,7 +371,19 @@ namespace Barotrauma { ChangeLocationType(Prefab.LocationTypeChangeOnCompleted); } - GiveReward(); + try + { + GiveReward(); + } + catch (Exception e) + { + string errorMsg = "Unknown error while giving mission rewards."; + DebugConsole.ThrowError(errorMsg, e); + GameAnalyticsManager.AddErrorEventOnce("Mission.End:GiveReward", GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + e.StackTrace); +#if SERVER + GameMain.Server?.SendChatMessage(errorMsg + "\n" + e.StackTrace, Networking.ChatMessageType.Error); +#endif + } } TimesAttempted++; @@ -423,30 +434,7 @@ namespace Barotrauma crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnAllyGainMissionExperience, experienceGainMultiplier)); crewCharacters.ForEach(c => experienceGainMultiplier.Value += c.GetStatValue(StatTypes.MissionExperienceGainMultiplier)); - int experienceGain = (int)(baseExperienceGain * experienceGainMultiplier.Value); -#if CLIENT - foreach (Character character in crewCharacters) - { - GiveMissionExperience(character.Info); - } -#else - foreach (Barotrauma.Networking.Client c in GameMain.Server.ConnectedClients) - { - //give the experience to the stored characterinfo if the client isn't currently controlling a character - GiveMissionExperience(c.Character?.Info ?? c.CharacterInfo); - } - foreach (Character bot in GameSession.GetSessionCrewCharacters(CharacterType.Bot)) - { - GiveMissionExperience(bot.Info); - } -#endif - - void GiveMissionExperience(CharacterInfo info) - { - var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f); - info?.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual); - info?.GiveExperience((int)((experienceGain * experienceGainMultiplier.Value) * experienceGainMultiplierIndividual.Value)); - } + DistributeExperienceToCrew(crewCharacters, (int)(baseExperienceGain * experienceGainMultiplier.Value)); CalculateFinalReward(Submarine.MainSub); #if SERVER @@ -465,17 +453,32 @@ namespace Barotrauma character.Info.MissionsCompletedSinceDeath++; } - foreach (KeyValuePair reputationReward in ReputationRewards) + foreach (var reputationReward in ReputationRewards) { - if (reputationReward.Key == "location") + if (reputationReward.FactionIdentifier == "location") { - OriginLocation.Reputation?.AddReputation(reputationReward.Value); + OriginLocation.Reputation?.AddReputation(reputationReward.Amount); + TryGiveReputationForOpposingFaction(OriginLocation.Faction, reputationReward.AmountForOpposingFaction); } else { - Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == reputationReward.Key); - float prevValue = faction.Reputation.Value; - faction?.Reputation.AddReputation(reputationReward.Value); + Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == reputationReward.FactionIdentifier); + if (faction != null) + { + faction.Reputation.AddReputation(reputationReward.Amount); + TryGiveReputationForOpposingFaction(faction, reputationReward.AmountForOpposingFaction); + } + } + } + + void TryGiveReputationForOpposingFaction(Faction thisFaction, float amount) + { + if (MathUtils.NearlyEqual(amount, 0.0f)) { return; } + if (thisFaction?.Prefab != null && + !thisFaction.Prefab.OpposingFaction.IsEmpty) + { + Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == thisFaction.Prefab.OpposingFaction); + faction?.Reputation.AddReputation(amount); } } } @@ -489,30 +492,10 @@ namespace Barotrauma } } -#if SERVER - public static int DistributeRewardsToCrew(IEnumerable crew, int totalReward) - { - int remainingRewards = totalReward; - float sum = GetRewardDistibutionSum(crew); - if (MathUtils.NearlyEqual(sum, 0)) { return remainingRewards; } - foreach (Character character in crew) - { - int rewardDistribution = character.Wallet.RewardDistribution; - float rewardWeight = sum > 100 ? rewardDistribution / sum : rewardDistribution / 100f; - int reward = (int)(totalReward * rewardWeight); - reward = Math.Min(remainingRewards, reward); - character.Wallet.Give(reward); - remainingRewards -= reward; - if (remainingRewards <= 0) { break; } - } - - return remainingRewards; - } -#endif + partial void DistributeExperienceToCrew(IEnumerable crew, int experienceGain); public static int GetRewardDistibutionSum(IEnumerable crew, int rewardDistribution = 0) => crew.Sum(c => c.Wallet.RewardDistribution) + rewardDistribution; - public static (int Amount, int Percentage, float Sum) GetRewardShare(int rewardDistribution, IEnumerable crew, Option reward) { float sum = GetRewardDistibutionSum(crew, rewardDistribution); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index 90e4d007b..5add25d3c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -4,7 +4,6 @@ using System.Collections.Immutable; using System.Linq; using System.Reflection; using System.Xml.Linq; -using Microsoft.Xna.Framework; namespace Barotrauma { @@ -54,6 +53,20 @@ namespace Barotrauma { MissionType.Combat, typeof(CombatMission) } }; + public class ReputationReward + { + public readonly Identifier FactionIdentifier; + public readonly float Amount; + public readonly float AmountForOpposingFaction; + + public ReputationReward(XElement element) + { + FactionIdentifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); + Amount = element.GetAttributeFloat(nameof(Amount), 0.0f); + AmountForOpposingFaction = element.GetAttributeFloat(nameof(AmountForOpposingFaction), 0.0f); + } + } + public static readonly HashSet HiddenMissionClasses = new HashSet() { MissionType.GoTo, MissionType.End }; private readonly ConstructorInfo constructor; @@ -75,7 +88,7 @@ namespace Barotrauma public readonly Identifier AchievementIdentifier; - public readonly Dictionary ReputationRewards = new Dictionary(); + public readonly ImmutableList ReputationRewards; public readonly List<(Identifier Identifier, object Value, SetDataAction.OperationType OperationType)> DataRewards = new List<(Identifier Identifier, object Value, SetDataAction.OperationType OperationType)>(); @@ -252,7 +265,8 @@ namespace Barotrauma messages.Add(message); } } - + + List reputationRewards = new List(); int messageIndex = 0; foreach (var subElement in element.Elements()) { @@ -292,14 +306,7 @@ namespace Barotrauma break; case "reputation": case "reputationreward": - Identifier factionIdentifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); - float amount = subElement.GetAttributeFloat("amount", 0.0f); - if (ReputationRewards.ContainsKey(factionIdentifier)) - { - DebugConsole.ThrowError($"Error in mission prefab \"{Identifier}\". Multiple reputation changes defined for the identifier \"{factionIdentifier}\"."); - continue; - } - ReputationRewards.Add(factionIdentifier, amount); + reputationRewards.Add(new ReputationReward(subElement)); break; case "metadata": Identifier identifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); @@ -325,6 +332,7 @@ namespace Barotrauma } Headers = headers.ToImmutableArray(); Messages = messages.ToImmutableArray(); + ReputationRewards = reputationRewards.ToImmutableList(); Identifier missionTypeName = element.GetAttributeIdentifier("type", Identifier.Empty); //backwards compatibility @@ -399,7 +407,7 @@ namespace Barotrauma else if (Type == MissionType.ScanAlienRuins || Type == MissionType.ClearAlienRuins) { var connection = from.Connections.Find(c => c.Locations.Contains(from) && c.Locations.Contains(to)); - if (connection?.LevelData == null || connection.LevelData.GenerationParams.RuinCount < 1) { return false; } + if (connection?.LevelData == null || connection.LevelData.GenerationParams.GetMaxRuinCount() < 1) { return false; } } return false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs index a9ab792ca..8b3d03131 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs @@ -147,7 +147,7 @@ namespace Barotrauma monster.Params.AI.FleeHealthThreshold = 0; foreach (var targetParam in monster.Params.AI.Targets) { - if (targetParam.Tag.Equals("engine", StringComparison.OrdinalIgnoreCase)) { continue; } + if (targetParam.Tag == "engine") { continue; } switch (targetParam.State) { case AIState.Avoid: diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs index e849a5499..78f840cf9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs @@ -4,7 +4,6 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; using Voronoi2; namespace Barotrauma @@ -30,6 +29,8 @@ namespace Barotrauma private Vector2 nestPosition; + private Level.Cave selectedCave; + public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { @@ -125,11 +126,9 @@ namespace Barotrauma } if (closestCave != null) { - closestCave.DisplayOnSonar = true; - SpawnNestObjects(level, closestCave); -#if SERVER selectedCave = closestCave; -#endif + selectedCave.MissionsToDisplayOnSonar.Add(this); + SpawnNestObjects(level, closestCave); } var nearbyCells = Level.Loaded.GetCells(nestPosition, searchDepth: 3); if (nearbyCells.Any()) @@ -172,8 +171,8 @@ namespace Barotrauma foreach (var subElement in itemConfig.Elements()) { - string itemIdentifier = subElement.GetAttributeString("identifier", ""); - if (!(MapEntityPrefab.Find(null, itemIdentifier) is ItemPrefab itemPrefab)) + var itemIdentifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); + if (MapEntityPrefab.FindByIdentifier(itemIdentifier) is not ItemPrefab itemPrefab) { DebugConsole.ThrowError("Couldn't spawn item for nest mission: item prefab \"" + itemIdentifier + "\" not found"); continue; @@ -183,25 +182,34 @@ namespace Barotrauma float rotation = 0.0f; if (spawnEdges.Any()) { - var edge = spawnEdges.GetRandom(Rand.RandSync.ServerAndClient); - spawnPos = Vector2.Lerp(edge.Point1, edge.Point2, Rand.Range(0.1f, 0.9f, Rand.RandSync.ServerAndClient)); - Vector2 normal = Vector2.UnitY; - if (edge.Cell1 != null && edge.Cell1.CellType == CellType.Solid) + const float MinDistanceFromOtherItems = 30.0f; + const int MaxTries = 10; + for (int i = 0; i < MaxTries; i++) { - normal = edge.GetNormal(edge.Cell1); + var edge = spawnEdges.GetRandom(Rand.RandSync.ServerAndClient); + spawnPos = Vector2.Lerp(edge.Point1, edge.Point2, Rand.Range(0.1f, 0.9f, Rand.RandSync.ServerAndClient)); + Vector2 normal = Vector2.UnitY; + if (edge.Cell1 != null && edge.Cell1.CellType == CellType.Solid) + { + normal = edge.GetNormal(edge.Cell1); + } + else if (edge.Cell2 != null && edge.Cell2.CellType == CellType.Solid) + { + normal = edge.GetNormal(edge.Cell2); + } + spawnPos += normal * 10.0f; + rotation = MathUtils.VectorToAngle(normal) - MathHelper.PiOver2; + + if (items.All(it => Vector2.DistanceSquared(it.WorldPosition, spawnPos) > MinDistanceFromOtherItems)) { break; } } - else if (edge.Cell2 != null && edge.Cell2.CellType == CellType.Solid) - { - normal = edge.GetNormal(edge.Cell2); - } - spawnPos += normal * 10.0f; - rotation = MathUtils.VectorToAngle(normal) - MathHelper.PiOver2; } var item = new Item(itemPrefab, spawnPos, null); item.body.FarseerBody.BodyType = BodyType.Kinematic; item.body.SetTransformIgnoreContacts(item.body.SimPosition, rotation); item.FindHull(); + item.AddTag("nestmission"); + item.AddTag(Prefab.Identifier); items.Add(item); var statusEffectElement = @@ -286,7 +294,10 @@ namespace Barotrauma } //continue when all items are in the sub or destroyed - if (AllItemsDestroyedOrRetrieved()) { State = 1; } + if (AllItemsDestroyedOrRetrieved()) + { + State = 1; + } break; case 1: diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs index d52095f5b..80392f0ef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs @@ -239,7 +239,7 @@ namespace Barotrauma private void InitPirateShip() { enemySub.NeutralizeBallast(); - if (enemySub.GetItems(alsoFromConnectedSubs: false).Find(i => i.HasTag("reactor") && !i.NonInteractable)?.GetComponent() is Reactor reactor) + if (enemySub.GetItems(alsoFromConnectedSubs: false).Find(i => i.HasTag(Tags.Reactor) && !i.NonInteractable)?.GetComponent() is Reactor reactor) { reactor.PowerUpImmediately(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index 7138946c3..7c54af4ab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -30,8 +30,8 @@ namespace Barotrauma public readonly ItemPrefab ItemPrefab; public readonly Level.PositionType SpawnPositionType; - public readonly string ContainerTag; - public readonly string ExistingItemTag; + public readonly Identifier ContainerTag; + public readonly Identifier ExistingItemTag; public readonly bool RemoveItem; @@ -87,7 +87,7 @@ namespace Barotrauma public Target(ContentXElement element, SalvageMission mission) { this.mission = mission; - ContainerTag = element.GetAttributeString("containertag", ""); + ContainerTag = element.GetAttributeIdentifier("containertag", Identifier.Empty); RequiredRetrievalState = element.GetAttributeEnum("requireretrieval", RetrievalState.RetrievedToSub); AllowContinueBeforeRetrieved = element.GetAttributeBool("allowcontinuebeforeretrieved", false); HideLabelAfterRetrieved = element.GetAttributeBool("hidelabelafterretrieved", false); @@ -100,7 +100,7 @@ namespace Barotrauma .Fallback(TextManager.Get(sonarLabelTag)) .Fallback(element.GetAttributeString("sonarlabel", "")); } - ExistingItemTag = element.GetAttributeString("existingitemtag", ""); + ExistingItemTag = element.GetAttributeIdentifier("existingitemtag", Identifier.Empty); RemoveItem = element.GetAttributeBool("removeitem", true); @@ -109,7 +109,7 @@ namespace Barotrauma DebugConsole.ThrowError("Error in SalvageMission - use item identifier instead of the name of the item."); string itemName = element.GetAttributeString("itemname", ""); ItemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab; - if (ItemPrefab == null && ExistingItemTag.IsNullOrEmpty()) + if (ItemPrefab == null && ExistingItemTag.IsEmpty) { DebugConsole.ThrowError($"Error in SalvageMission: couldn't find an item prefab with the name \"{itemName}\""); } @@ -126,7 +126,7 @@ namespace Barotrauma string itemTag = element.GetAttributeString("itemtag", ""); ItemPrefab = MapEntityPrefab.GetRandom(p => p.Tags.Contains(itemTag), Rand.RandSync.Unsynced) as ItemPrefab; } - if (ItemPrefab == null && ExistingItemTag.IsNullOrEmpty()) + if (ItemPrefab == null && ExistingItemTag.IsEmpty) { DebugConsole.ThrowError($"Error in SalvageMission - couldn't find an item prefab with the identifier \"{itemIdentifier}\""); } @@ -233,7 +233,7 @@ namespace Barotrauma Vector2.Zero : Level.Loaded.GetRandomItemPos(target.SpawnPositionType, 100.0f, minDistance, 30.0f); - if (!string.IsNullOrEmpty(target.ExistingItemTag)) + if (!target.ExistingItemTag.IsEmpty) { var suitableItems = Item.ItemList.Where(it => it.HasTag(target.ExistingItemTag)); if (GameMain.GameSession?.Missions != null) @@ -284,9 +284,9 @@ namespace Barotrauma if (target.Item == null) { - if (target.ItemPrefab == null && string.IsNullOrEmpty(target.ContainerTag)) + if (target.ItemPrefab == null && target.ContainerTag.IsEmpty) { - DebugConsole.ThrowError($"Failed to find a target item for the mission \"{Prefab.Identifier}\". Item tag: {target.ExistingItemTag ?? "null"}"); + DebugConsole.ThrowError($"Failed to find a target item for the mission \"{Prefab.Identifier}\". Item tag: {target.ExistingItemTag}"); continue; } target.Item = new Item(target.ItemPrefab, position, null); @@ -312,8 +312,10 @@ namespace Barotrauma #endif } + target.Item.IsSalvageMissionItem = true; + //try to find a container and place the item inside it - if (!string.IsNullOrEmpty(target.ContainerTag) && target.Item.ParentInventory == null) + if (!target.ContainerTag.IsEmpty && target.Item.ParentInventory == null) { List validContainers = new List(); foreach (Item it in Item.ItemList) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs index b8a2f0936..364675efb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs @@ -218,7 +218,7 @@ namespace Barotrauma #endif } - private bool IsValidScanPosition(Scanner scanner, KeyValuePair scanStatus, float scanRadiusSquared) + private static bool IsValidScanPosition(Scanner scanner, KeyValuePair scanStatus, float scanRadiusSquared) { if (scanStatus.Value) { return false; } if (scanStatus.Key.Submarine != scanner.Item.Submarine) { return false; } @@ -232,39 +232,15 @@ namespace Barotrauma switch (State) { case 0: - if (!AllTargetsScanned) { return; } - State = 1; - break; - case 1: - if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; } - State = 2; + if (AllTargetsScanned) + { + State = 1; + } break; } } - protected override bool DetermineCompleted() - { - return State == 2 && AllScannersReturned(); - - bool AllScannersReturned() - { - foreach (var scanner in scanners) - { - if (scanner?.Item == null || scanner.Item.Removed) { return false; } - var owner = scanner.Item.GetRootInventoryOwner(); - if (owner.Submarine != null && owner.Submarine.Info.Type == SubmarineType.Player) - { - continue; - } - else if (owner is Character c && c.Info != null && GameMain.GameSession.CrewManager.CharacterInfos.Contains(c.Info)) - { - continue; - } - return false; - } - return true; - } - } + protected override bool DetermineCompleted() => State > 0; protected override void EndMissionSpecific(bool completed) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index e45b7b4cc..00572c070 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using Barotrauma.Extensions; using Barotrauma.Items.Components; +using FarseerPhysics; namespace Barotrauma { @@ -13,6 +14,7 @@ namespace Barotrauma public readonly int MinAmount, MaxAmount; private readonly List monsters = new List(); + public readonly float SpawnDistance; private readonly float scatter; private readonly float offset; private readonly float delayBetweenSpawns; @@ -56,7 +58,7 @@ namespace Barotrauma } public MonsterEvent(EventPrefab prefab) - : base (prefab) + : base(prefab) { string speciesFile = prefab.ConfigElement.GetAttributeString("characterfile", ""); CharacterPrefab characterPrefab = CharacterPrefab.FindByFilePath(speciesFile); @@ -94,7 +96,7 @@ namespace Barotrauma } spawnPointTag = prefab.ConfigElement.GetAttributeString("spawnpointtag", string.Empty); - + SpawnDistance = prefab.ConfigElement.GetAttributeFloat("spawndistance", 0); offset = prefab.ConfigElement.GetAttributeFloat("offset", 0); scatter = Math.Clamp(prefab.ConfigElement.GetAttributeFloat("scatter", 500), 0, 3000); delayBetweenSpawns = prefab.ConfigElement.GetAttributeFloat("delaybetweenspawns", 0.1f); @@ -174,6 +176,15 @@ namespace Barotrauma } } + public override string GetDebugInfo() + { + return + $"Finished: {IsFinished.ColorizeObject()}\n" + + $"Amount: {MinAmount.ColorizeObject()} - {MaxAmount.ColorizeObject()}\n" + + $"Spawn pending: {SpawnPending.ColorizeObject()}\n" + + $"Spawn position: {SpawnPos.ColorizeObject()}"; + } + private List GetAvailableSpawnPositions() { var availablePositions = Level.Loaded.PositionsOfInterest.FindAll(p => SpawnPosType.HasFlag(p.PositionType)); @@ -214,13 +225,14 @@ namespace Barotrauma return availablePositions; } + private Level.InterestingPosition chosenPosition; private void FindSpawnPosition(bool affectSubImmediately) { if (disallowed) { return; } spawnPos = Vector2.Zero; var availablePositions = GetAvailableSpawnPositions(); - var chosenPosition = new Level.InterestingPosition(Point.Zero, Level.PositionType.MainPath, isValid: false); + chosenPosition = new Level.InterestingPosition(Point.Zero, Level.PositionType.MainPath, isValid: false); bool isRuinOrWreck = SpawnPosType.HasFlag(Level.PositionType.Ruin) || SpawnPosType.HasFlag(Level.PositionType.Wreck); if (affectSubImmediately && !isRuinOrWreck && !SpawnPosType.HasFlag(Level.PositionType.Abyss)) { @@ -454,52 +466,109 @@ namespace Barotrauma { if (submarine.Info.Type != SubmarineType.Player) { continue; } float minDist = GetMinDistanceToSub(submarine); - if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist) { return; } + if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist) + { + // Too close to a player sub. + return; + } } } - float minDistance = Prefab.SpawnDistance; - if (minDistance <= 0) + float spawnDistance = SpawnDistance; + if (spawnDistance <= 0) { if (SpawnPosType.HasFlag(Level.PositionType.Cave)) { - minDistance = 8000; + spawnDistance = 8000; } else if (SpawnPosType.HasFlag(Level.PositionType.Ruin)) { - minDistance = 5000; + spawnDistance = 5000; } else if (SpawnPosType.HasFlag(Level.PositionType.Wreck) || SpawnPosType.HasFlag(Level.PositionType.BeaconStation)) { - minDistance = 3000; + spawnDistance = 3000; } } - if (minDistance > 0) + if (spawnDistance > 0) { bool someoneNearby = false; foreach (Submarine submarine in Submarine.Loaded) { if (submarine.Info.Type != SubmarineType.Player) { continue; } - if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < MathUtils.Pow2(minDistance)) + float distanceSquared = Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value); + if (distanceSquared < MathUtils.Pow2(spawnDistance)) { someoneNearby = true; - break; + if (chosenPosition.Submarine != null) + { + Vector2 from = Submarine.GetRelativeSimPositionFromWorldPosition(spawnPos.Value, chosenPosition.Submarine, chosenPosition.Submarine); + Vector2 to = Submarine.GetRelativeSimPositionFromWorldPosition(submarine.WorldPosition, chosenPosition.Submarine, submarine); + if (CheckLineOfSight(from, to, chosenPosition.Submarine)) + { + // Line of sight to a player sub -> don't spawn yet. + return; + } + } + else + { + break; + } } } foreach (Character c in Character.CharacterList) { if (c == Character.Controlled || c.IsRemotePlayer) { - if (Vector2.DistanceSquared(c.WorldPosition, spawnPos.Value) < MathUtils.Pow2(minDistance)) + float distanceSquared = Vector2.DistanceSquared(c.WorldPosition, spawnPos.Value); + if (distanceSquared < MathUtils.Pow2(spawnDistance)) { someoneNearby = true; - break; + if (chosenPosition.Submarine != null) + { + Vector2 from = Submarine.GetRelativeSimPositionFromWorldPosition(spawnPos.Value, chosenPosition.Submarine, chosenPosition.Submarine); + Vector2 to = Submarine.GetRelativeSimPositionFromWorldPosition(c.WorldPosition, chosenPosition.Submarine, c.Submarine); + if (CheckLineOfSight(from, to, chosenPosition.Submarine)) + { + // Line of sight to a player character -> don't spawn. Disable the event to prevent monsters "magically" spawning here. + Finish(); + return; + } + } + else + { + break; + } } } } if (!someoneNearby) { return; } + + static bool CheckLineOfSight(Vector2 from, Vector2 to, Submarine targetSub) + { + var bodies = Submarine.PickBodies(from, to, ignoredBodies: null, Physics.CollisionWall); + foreach (var b in bodies) + { + if (b.UserData is ISpatialEntity spatialEntity && spatialEntity.Submarine != targetSub) + { + // Different sub -> ignore + continue; + } + if (b.UserData is Structure s && !s.IsPlatform && s.CastShadow) + { + return false; + } + if (b.UserData is Item item && item.GetComponent() is Door door) + { + if (!door.IsBroken && !door.IsOpen) + { + return false; + } + } + } + return true; + } } - if (SpawnPosType.HasFlag(Level.PositionType.Abyss) || SpawnPosType.HasFlag(Level.PositionType.AbyssCave)) { bool anyInAbyss = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs index 703aa19ce..85c735b0d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs @@ -1,4 +1,5 @@ -using System; +using Barotrauma.Extensions; +using System; using System.Collections.Generic; using System.Linq; @@ -9,10 +10,19 @@ namespace Barotrauma private readonly Dictionary>> targetPredicates = new Dictionary>>(); private readonly Dictionary> cachedTargets = new Dictionary>(); + + /// + /// How many targets were there when they were tagged for the first time? Can be used by some EventActions to check how many entities + /// there are still left (e.g. how much of the initial cargo still exists) + /// + private readonly Dictionary initialAmounts = new Dictionary(); + private int prevEntityCount; private int prevPlayerCount, prevBotCount; private Character prevControlled; + public readonly OnRoundEndAction OnRoundEndAction; + private readonly string[] requiredDestinationTypes; public readonly bool RequireBeaconStation; @@ -20,16 +30,25 @@ namespace Barotrauma public List Actions { get; } = new List(); public Dictionary> Targets { get; } = new Dictionary>(); + protected virtual IEnumerable NonActionChildElementNames => Enumerable.Empty(); + public override string ToString() { - return $"ScriptedEvent ({prefab.Identifier})"; + return $"{nameof(ScriptedEvent)} ({prefab.Identifier})"; } public ScriptedEvent(EventPrefab prefab) : base(prefab) { foreach (var element in prefab.ConfigElement.Elements()) { - if (element.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase)) + Identifier elementId = element.Name.ToIdentifier(); + if (NonActionChildElementNames.Contains(elementId)) { continue; } + if (elementId == nameof(Barotrauma.OnRoundEndAction)) + { + OnRoundEndAction = EventAction.Instantiate(this, element) as OnRoundEndAction; + continue; + } + if (elementId == "statuseffect") { DebugConsole.ThrowError($"Error in event prefab \"{prefab.Identifier}\". Status effect configured as an action. Please configure status effects as child elements of a StatusEffectAction."); continue; @@ -46,31 +65,125 @@ namespace Barotrauma requiredDestinationTypes = prefab.ConfigElement.GetAttributeStringArray("requireddestinationtypes", null); RequireBeaconStation = prefab.ConfigElement.GetAttributeBool("requirebeaconstation", false); + var allActions = GetAllActions().Select(a => a.action); + foreach (var gotoAction in allActions.OfType()) + { + if (allActions.None(a => a is Label label && label.Name == gotoAction.Name)) + { + DebugConsole.ThrowError($"Error in event \"{prefab.Identifier}\". Could not find a label matching the GoTo \"{gotoAction.Name}\"."); + } + } + GameAnalyticsManager.AddDesignEvent($"ScriptedEvent:{prefab.Identifier}:Start"); } + public override string GetDebugInfo() + { + EventAction currentAction = !IsFinished ? Actions[CurrentActionIndex] : null; + + string text = $"Finished: {IsFinished.ColorizeObject()}\n" + + $"Action index: {CurrentActionIndex.ColorizeObject()}\n" + + $"Current action: {currentAction?.ToDebugString() ?? ToolBox.ColorizeObject(null)}\n"; + + text += "All actions:\n"; + text += GetAllActions().Aggregate(string.Empty, (current, action) => current + $"{new string(' ', action.indent * 6)}{action.action.ToDebugString()}\n"); + + text += "Targets:\n"; + foreach (var (key, value) in Targets) + { + text += $" {key.ColorizeObject()}: {value.Aggregate(string.Empty, (current, entity) => current + $"{entity.ColorizeObject()} ")}\n"; + } + return text; + } + + public virtual string GetTextForReplacementElement(string tag) + { + if (tag.StartsWith("eventtag:")) + { + string targetTag = tag["eventtag:".Length..]; + Entity target = GetTargets(targetTag.ToIdentifier()).FirstOrDefault(); + if (target != null) + { + if (target is Item item) { return item.Name; } + if (target is Character character) { return character.Name; } + if (target is Hull hull) { return hull.DisplayName.Value; } + if (target is Submarine sub) { return sub.Info.DisplayName.Value; } + DebugConsole.AddWarning($"Failed to get the name of the event target {target} as a replacement for the tag {tag} in an event text."); + return target.ToString(); + } + else + { + return $"[target \"{targetTag}\" not found]"; + } + } + return string.Empty; + } + + public virtual LocalizedString ReplaceVariablesInEventText(LocalizedString str) + { + return str; + } + + /// + /// Finds all actions in the ScriptedEvent (recursively going through the subactions as well). + /// Returns a list of tuples where the first value is the indentation level (or "how deep in the hierarchy") the action is. + /// + public List<(int indent, EventAction action)> GetAllActions() + { + var list = new List<(int indent, EventAction action)>(); + foreach (EventAction eventAction in Actions) + { + list.AddRange(FindActionsRecursive(eventAction)); + } + return list; + + static List<(int indent, EventAction action)> FindActionsRecursive(EventAction eventAction, int indent = 1) + { + var eventActions = new List<(int indent, EventAction action)> { (indent, eventAction) }; + indent++; + foreach (var action in eventAction.GetSubActions()) + { + eventActions.AddRange(FindActionsRecursive(action, indent)); + } + return eventActions; + } + } + public void AddTarget(Identifier tag, Entity target) { if (target == null) { - throw new System.ArgumentException("Target was null"); + throw new ArgumentException($"Target was null (tag: {tag})"); } if (target.Removed) { - throw new System.ArgumentException("Target has been removed"); + throw new ArgumentException($"Target has been removed (tag: {tag})"); } - if (!Targets.ContainsKey(tag)) + if (Targets.ContainsKey(tag)) { - Targets.Add(tag, new List()); - } - Targets[tag].Add(target); - if (cachedTargets.ContainsKey(tag)) - { - cachedTargets[tag].Add(target); + if (!Targets[tag].Contains(target)) + { + Targets[tag].Add(target); + } } else { - cachedTargets.Add(tag, new List { target }); + Targets.Add(tag, new List() { target }); + } + if (cachedTargets.ContainsKey(tag)) + { + if (!cachedTargets[tag].Contains(target)) + { + cachedTargets[tag].Add(target); + } + } + else + { + cachedTargets.Add(tag, Targets[tag].ToList()); + } + if (!initialAmounts.ContainsKey(tag)) + { + initialAmounts.Add(tag, cachedTargets[tag].Count); } } @@ -88,6 +201,15 @@ namespace Barotrauma } } + public int GetInitialTargetCount(Identifier tag) + { + if (initialAmounts.TryGetValue(tag, out int count)) + { + return count; + } + return 0; + } + public IEnumerable GetTargets(Identifier tag) { if (cachedTargets.ContainsKey(tag)) @@ -136,10 +258,25 @@ namespace Barotrauma } } - cachedTargets.Add(tag, targetsToReturn); + cachedTargets.Add(tag, targetsToReturn); + if (!initialAmounts.ContainsKey(tag)) + { + initialAmounts.Add(tag, targetsToReturn.Count); + } return targetsToReturn; } + public void InheritTags(Entity originalEntity, Entity newEntity) + { + foreach (var kvp in Targets) + { + if (kvp.Value.Contains(originalEntity)) + { + kvp.Value.Add(newEntity); + } + } + } + public void RemoveTag(Identifier tag) { if (tag.IsEmpty) { return; } @@ -152,8 +289,14 @@ namespace Barotrauma { int botCount = 0; int playerCount = 0; + bool forceRefreshTargets = false; foreach (Character c in Character.CharacterList) { + if (c.Removed) + { + forceRefreshTargets = true; + continue; + } if (c.IsPlayer) { playerCount++; @@ -163,7 +306,7 @@ namespace Barotrauma botCount++; } } - if (Entity.EntityCount != prevEntityCount || botCount != prevBotCount || playerCount != prevPlayerCount || prevControlled != Character.Controlled) + if (forceRefreshTargets || Entity.EntityCount != prevEntityCount || botCount != prevBotCount || playerCount != prevPlayerCount || prevControlled != Character.Controlled) { cachedTargets.Clear(); prevEntityCount = Entity.EntityCount; @@ -204,6 +347,10 @@ namespace Barotrauma break; } } + if (CurrentActionIndex == -1) + { + DebugConsole.AddWarning($"Could not find the GoTo label \"{goTo}\" in the event \"{Prefab.Identifier}\". Ending the event."); + } } if (CurrentActionIndex >= Actions.Count || CurrentActionIndex < 0) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/ColorExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/ColorExtensions.cs index 89fc4cefa..d8c7935d1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/ColorExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/ColorExtensions.cs @@ -20,5 +20,35 @@ namespace Barotrauma.Extensions { return new Color(color.R, color.G, color.B, (byte)255); } + + private static bool IsFirstColorChannelDominant(byte first, byte second, byte third, float minimumRatio = 2) + => first > second * minimumRatio && first > third * minimumRatio; + + /// + /// Is the value of the red channel at least 'minimumRatio' larger than the blue and green + /// + public static bool IsRedDominant(Color color, float minimumRatio = 2, byte minimumAlpha = 0) + => color.A > minimumAlpha && + IsFirstColorChannelDominant( + first: color.R, + color.G, color.B, minimumRatio); + + /// + /// Is the value of the green channel at least 'minimumRatio' larger than the red and blue + /// + public static bool IsGreenDominant(Color color, float minimumRatio = 2, byte minimumAlpha = 0) + => color.A > minimumAlpha && + IsFirstColorChannelDominant( + first: color.G, + color.R, color.B, minimumRatio); + + /// + /// Is the value of the blue channel at least 'minimumRatio' larger than the red and green + /// + public static bool IsBlueDominant(Color color, float minimumRatio = 2, byte minimumAlpha = 0) + => color.A > minimumAlpha && + IsFirstColorChannelDominant( + first: color.B, + color.G, color.R, minimumRatio); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs index 272d38833..abbb65f70 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs @@ -101,27 +101,9 @@ namespace Barotrauma.Extensions return source.Where(predicate).OrderBy(p => p.UintIdentifier).ToArray().GetRandom(randSync); } - - public static T RandomElementByWeight(this IList source, Func weightSelector, Rand.RandSync randSync = Rand.RandSync.Unsynced) + public static T GetRandomByWeight(this IEnumerable source, Func weightSelector, Rand.RandSync randSync) { - float totalWeight = source.Sum(weightSelector); - - float itemWeightIndex = Rand.Range(0f, 1f, randSync) * totalWeight; - float currentWeightIndex = 0; - - for (int i = 0; i < source.Count; i++) - { - T weightedItem = source[i]; - float weight = weightSelector(weightedItem); - currentWeightIndex += weight; - - if (currentWeightIndex >= itemWeightIndex) - { - return weightedItem; - } - } - - return default; + return ToolBox.SelectWeightedRandom(source, weightSelector, randSync); } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringExtensions.cs index 7322cb985..c6c3e3a92 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringExtensions.cs @@ -10,8 +10,8 @@ namespace Barotrauma public static bool IsNullOrEmpty([NotNullWhen(returnValue: false)]this string? s) => string.IsNullOrEmpty(s); public static bool IsNullOrWhiteSpace([NotNullWhen(returnValue: false)]this string? s) => string.IsNullOrWhiteSpace(s); - public static bool IsNullOrEmpty([NotNullWhen(returnValue: false)]this ContentPath? p) => p?.IsNullOrEmpty() ?? true; - public static bool IsNullOrWhiteSpace([NotNullWhen(returnValue: false)]this ContentPath? p) => p?.IsNullOrWhiteSpace() ?? true; + public static bool IsNullOrEmpty([NotNullWhen(returnValue: false)]this ContentPath? p) => p?.IsPathNullOrEmpty() ?? true; + public static bool IsNullOrWhiteSpace([NotNullWhen(returnValue: false)]this ContentPath? p) => p?.IsPathNullOrWhiteSpace() ?? true; public static bool IsNullOrEmpty([NotNullWhen(returnValue: false)]this LocalizedString? s) => s is null || string.IsNullOrEmpty(s.Value); public static bool IsNullOrWhiteSpace([NotNullWhen(returnValue: false)]this LocalizedString? s) => s is null || string.IsNullOrWhiteSpace(s.Value); public static bool IsNullOrEmpty([NotNullWhen(returnValue: false)]this RichString? s) => s is null || s.NestedStr.IsNullOrEmpty(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs index c44ded911..b87dd7027 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs @@ -1,12 +1,20 @@ +#nullable enable using Barotrauma.Steam; using RestSharp; using System; using System.Net; +using System.Threading.Tasks; namespace Barotrauma { static partial class GameAnalyticsManager { + /// + /// The protocol used to communicate with the remote consent server may change. + /// This number tells the server which version the game is using so we can implement backwards-compatibility. + /// + private const string RemoteRequestVersion = "2"; + public enum Consent { /// @@ -42,13 +50,14 @@ namespace Barotrauma private static bool consentTextAvailable => TextManager.ContainsTag("statisticsconsentheader") && TextManager.ContainsTag("statisticsconsenttext"); + + private const string consentServerUrl = "https://barotraumagame.com/baromaster/"; + private const string consentServerFile = "consentserver.php"; - private readonly static string consentServerUrl = "https://barotraumagame.com/baromaster/"; - private readonly static string consentServerFile = "consentserver.php"; - - private static string GetAuthTicket() + private static async Task GetAuthTicket() { - Steamworks.AuthTicket authTicket = SteamManager.GetAuthSessionTicket(); + var ticketOption = await SteamManager.GetAuthTicketForGameAnalyticsConsent(); + if (!ticketOption.TryUnwrap(out var authTicket) || authTicket.Data is null) { return ""; } //convert byte array to hex return BitConverter.ToString(authTicket.Data).Replace("-", ""); } @@ -59,23 +68,27 @@ namespace Barotrauma /// the database or the user accepting via the privacy policy /// prompt should enable it. /// - public static void SetConsent(Consent consent) + public static void SetConsent(Consent consent, Action? onAnswerSent = null) { if (consent == Consent.Yes) { throw new Exception( "Cannot call SetConsent with value Consent.Yes, must only be set to this value via consent prompt"); } - SetConsentInternal(consent); + SetConsentInternal(consent, onAnswerSent); } /// /// Implementation of the bulk of SetConsent. /// DO NOT CALL THIS UNLESS NEEDED. /// - private static void SetConsentInternal(Consent consent) + private static void SetConsentInternal(Consent consent, Action? onAnswerSent) { - if (UserConsented == consent) { return; } + if (UserConsented == consent) + { + onAnswerSent?.Invoke(); + return; + } if (consent == Consent.Ask) { @@ -94,42 +107,72 @@ namespace Barotrauma ShutDown(); } + TaskPool.Add( + "GameAnalyticsConsent.SendAnswerToRemoteDatabase", + SendAnswerToRemoteDatabase(consent), + t => + { + onAnswerSent?.Invoke(); + if (!t.TryGetResult(out bool success) || !success) { return; } + + UserConsented = consent; + if (consent == Consent.Yes) + { + Init(); + } + }); + } + + /// + /// Try to send the user's response to the remote consent server. + /// Returns true upon success, false otherwise. + /// + private static async Task SendAnswerToRemoteDatabase(Consent consent) + { string authTicketStr; try { - authTicketStr = GetAuthTicket(); + authTicketStr = await GetAuthTicket(); } catch (Exception e) { DebugConsole.ThrowError("Error in GameAnalyticsManager.SetConsent. Could not get a Steam authentication ticket.", e); - return; + return false; } - RestClient client = null; + if (string.IsNullOrEmpty(authTicketStr)) + { + DebugConsole.ThrowError("Error in GameAnalyticsManager.SetContent. Steam authentication ticket was empty."); + return false; + } + + IRestResponse response; try { - client = new RestClient(consentServerUrl); + var client = new RestClient(consentServerUrl); + + var request = new RestRequest(consentServerFile, Method.GET); + request.AddParameter("authticket", authTicketStr); + request.AddParameter("action", "setconsent"); + request.AddParameter("consent", consent == Consent.Yes ? 1 : 0); + request.AddParameter("request_version", RemoteRequestVersion); + + response = await client.ExecuteAsync(request, Method.GET); } catch (Exception e) { DebugConsole.ThrowError("Error while connecting to consent server", e); + return false; } - if (client == null) { return; } - var request = new RestRequest(consentServerFile, Method.GET); - request.AddParameter("authticket", authTicketStr); - request.AddParameter("action", "setconsent"); - request.AddParameter("consent", consent == Consent.Yes ? 1 : 0); + if (!CheckResponse(response)) { return false; } - var response = client.Execute(request, Method.GET); - if (CheckResponse(response)) + if (!string.IsNullOrWhiteSpace(response.Content)) { - UserConsented = consent; - if (consent == Consent.Yes) - { - Init(); - } + DebugConsole.ThrowError($"Error in GameAnalyticsManager.SetContent. Consent server reported an error: {response.Content.Trim()}"); + return false; } + return true; } static partial void CreateConsentPrompt(); @@ -146,12 +189,6 @@ namespace Barotrauma return; } - static void error(string reason, Exception exception) - { - DebugConsole.ThrowError($"Error in GameAnalyticsManager.GetConsent: {reason}", exception); - SetConsent(Consent.Error); - } - if (!SteamManager.IsInitialized) { DebugConsole.AddWarning("Error in GameAnalyticsManager.GetConsent: Could not get a Steam authentication ticket (not connected to Steam)."); @@ -159,15 +196,33 @@ namespace Barotrauma return; } + TaskPool.Add( + "GameAnalyticsConsent.RequestAnswerFromRemoteDatabase", + RequestAnswerFromRemoteDatabase(), + t => + { + if (!t.TryGetResult(out Consent consent)) { return; } + SetConsentInternal(consent, onAnswerSent: null); + }); + } + + private static async Task RequestAnswerFromRemoteDatabase() + { + static void error(string reason, Exception exception) + { + DebugConsole.ThrowError($"Error in GameAnalyticsManager.GetConsent: {reason}", exception); + SetConsent(Consent.Error); + } + string authTicketStr; try { - authTicketStr = GetAuthTicket(); + authTicketStr = await GetAuthTicket(); } catch (Exception e) { error("Could not get a Steam authentication ticket.", e); - return; + return Consent.Error; } RestClient client; @@ -178,37 +233,36 @@ namespace Barotrauma catch (Exception e) { error("Error while connecting to consent server.", e); - return; + return Consent.Error; } var request = new RestRequest(consentServerFile, Method.GET); request.AddParameter("authticket", authTicketStr); request.AddParameter("action", "getconsent"); + request.AddParameter("request_version", RemoteRequestVersion); - TaskPool.Add($"{nameof(GameAnalyticsManager)}.{nameof(InitIfConsented)}", client.ExecuteAsync(request), (t) => + IRestResponse response; + try { - if (t.Exception != null) - { - error("Error executing the request to the consent server.", t.Exception.InnerException); - return; - } + response = await client.ExecuteAsync(request); + } + catch (Exception e) + { + error("Error executing the request to the consent server.", e.GetInnermost()); + return Consent.Error; + } - if (!t.TryGetResult(out IRestResponse response)) { return; } - if (!CheckResponse(response)) - { - SetConsent(Consent.Error); - } - else if (string.IsNullOrEmpty(response.Content)) - { - SetConsent(Consent.Ask); - } - else - { - SetConsentInternal(response.Content[0] == '1' - ? Consent.Yes - : Consent.No); - } - }); + if (!CheckResponse(response)) + { + return Consent.Error; + } + if (string.IsNullOrEmpty(response.Content)) + { + return Consent.Ask; + } + return response.Content[0] == '1' + ? Consent.Yes + : Consent.No; } private static bool CheckResponse(IRestResponse response) @@ -218,24 +272,24 @@ namespace Barotrauma DebugConsole.ThrowError(TextManager.GetWithVariable("MasterServerErrorException", "[error]", response.ErrorException.ToString())); return false; } - else if (response.StatusCode != HttpStatusCode.OK) + + if (response.StatusCode == HttpStatusCode.OK) { return true; } + + switch (response.StatusCode) { - switch (response.StatusCode) - { - case HttpStatusCode.NotFound: - DebugConsole.ThrowError(TextManager.GetWithVariable("MasterServerError404", "[masterserverurl]", consentServerUrl)); - break; - case HttpStatusCode.ServiceUnavailable: - DebugConsole.ThrowError(TextManager.Get("MasterServerErrorUnavailable")); - break; - default: - DebugConsole.ThrowError(TextManager.GetWithVariables("MasterServerErrorDefault", - ("[statuscode]", response.StatusCode.ToString()), - ("[statusdescription]", response.StatusDescription))); - break; - } + case HttpStatusCode.NotFound: + DebugConsole.ThrowError(TextManager.GetWithVariable("MasterServerError404", "[masterserverurl]", consentServerUrl)); + break; + case HttpStatusCode.ServiceUnavailable: + DebugConsole.ThrowError(TextManager.Get("MasterServerErrorUnavailable")); + break; + default: + DebugConsole.ThrowError(TextManager.GetWithVariables("MasterServerErrorDefault", + ("[statuscode]", response.StatusCode.ToString()), + ("[statusdescription]", response.StatusDescription))); + break; } - return response.StatusCode == HttpStatusCode.OK; + return false; } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index 7312c3322..794567a60 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -58,9 +58,13 @@ namespace Barotrauma } } - public static void RegenerateLoot(Submarine sub, ItemContainer regeneratedContainer) + /// + /// Spawns loot in the specified container. + /// + /// Probability for an individual loot item to be skipped. I.e. a value of 1.0 means nothing spawns, 0.5 means there's about 50% of the normal amount of loot. + public static void RegenerateLoot(Submarine sub, ItemContainer regeneratedContainer, float skipItemProbability = 0.0f) { - CreateAndPlace(sub.ToEnumerable(), regeneratedContainer: regeneratedContainer); + CreateAndPlace(sub.ToEnumerable(), regeneratedContainer: regeneratedContainer, skipItemProbability); } public static Identifier DefaultStartItemSet = new Identifier("normal"); @@ -105,6 +109,7 @@ namespace Barotrauma DebugConsole.AddWarning($"Cannot find a start item with with the identifier \"{startItem.Item}\""); continue; } + if (startItem.MultiPlayerOnly && GameMain.GameSession?.GameMode is { IsSinglePlayer: true }) { continue; } for (int i = 0; i < startItem.Amount; i++) { var item = new Item(itemPrefab, initialSpawnPos.Position, sub, callOnItemLoaded: false); @@ -136,7 +141,7 @@ namespace Barotrauma } } - private static void CreateAndPlace(IEnumerable subs, ItemContainer regeneratedContainer = null) + private static void CreateAndPlace(IEnumerable subs, ItemContainer regeneratedContainer = null, float skipItemProbability = 0.0f) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { @@ -249,16 +254,16 @@ namespace Barotrauma } } - bool SpawnItems(ItemPrefab itemPrefab) + void SpawnItems(ItemPrefab itemPrefab, float skipItemProbability = 0.0f) { + if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) < skipItemProbability) { return; } if (itemPrefab == null) { string errorMsg = "Error in AutoItemPlacer.SpawnItems - itemPrefab was null.\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("AutoItemPlacer.SpawnItems:ItemNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); - return false; + return; } - bool success = false; bool isCampaign = GameMain.GameSession?.GameMode is CampaignMode; float levelDifficulty = Level.Loaded?.Difficulty ?? 0.0f; foreach (PreferredContainer preferredContainer in itemPrefab.PreferredContainers) @@ -278,11 +283,9 @@ namespace Barotrauma if (newItems.Any()) { itemsToSpawn.AddRange(newItems); - success = true; } } } - return success; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index f14e1d6f4..f7273d22c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -440,7 +440,7 @@ namespace Barotrauma if (!item.Components.All(static c => c is not Holdable { Attachable: true, Attached: true })) { return false; } if (!item.Components.All(static c => c is not Wire w || w.Connections.All(static c => c is null))) { return false; } if (!ItemAndAllContainersInteractable(item)) { return false; } - if (item.RootContainer is Item rootContainer && rootContainer.HasTag("dontsellitems")) { return false; } + if (item.RootContainer is Item rootContainer && rootContainer.HasTag(Tags.DontSellItems)) { return false; } return true; }).Distinct(); @@ -498,7 +498,7 @@ namespace Barotrauma .Distinct(); public static IEnumerable FilterCargoCrates(IEnumerable items, Func conditional = null) - => items.Where(it => it.HasTag("crate") && !it.NonInteractable && !it.NonPlayerTeamInteractable && !it.HiddenInGame && !it.Removed && (conditional == null || conditional(it))); + => items.Where(it => it.HasTag(Tags.Crate) && !it.NonInteractable && !it.NonPlayerTeamInteractable && !it.HiddenInGame && !it.Removed && (conditional == null || conditional(it))); public static IEnumerable FindReusableCargoContainers(IEnumerable subs, IEnumerable cargoRooms = null) => FilterCargoCrates(Item.ItemList, it => subs.Contains(it.Submarine) && (cargoRooms == null || cargoRooms.Contains(it.CurrentHull))) @@ -535,6 +535,12 @@ namespace Barotrauma DebugConsole.AddWarning($"CargoManager: No ItemContainer component found in {containerItem.Prefab.Identifier}!"); return null; } + if (!itemContainer.CanBeContained(item)) + { + // Can't contain the item in the crate -> let's not create it. + containerItem.Remove(); + return null; + } availableContainers.Add(itemContainer); #if SERVER if (GameMain.Server != null) @@ -607,7 +613,7 @@ namespace Barotrauma #if SERVER Entity.Spawner?.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item)); #endif - (itemContainer?.Item ?? item).CampaignInteractionType = CampaignMode.InteractionType.Cargo; + (itemContainer?.Item ?? item).AssignCampaignInteractionType(CampaignMode.InteractionType.Cargo); static void itemSpawned(PurchasedItem purchased, Item item) { Submarine sub = item.Submarine ?? item.RootContainer?.Submarine; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index a397da922..c2f3a80d4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -262,7 +262,7 @@ namespace Barotrauma while (spawnWaypoints.Any() && spawnWaypoints.Count < characterInfos.Count) { spawnWaypoints.Add(spawnWaypoints[Rand.Int(spawnWaypoints.Count)]); - } + } } if (spawnWaypoints == null || !spawnWaypoints.Any()) { @@ -306,16 +306,15 @@ namespace Barotrauma } character.LoadTalents(); - - character.GiveIdCardTags(mainSubWaypoints[i]); - character.GiveIdCardTags(spawnWaypoints[i]); + character.GiveIdCardTags(new List() { mainSubWaypoints[i], spawnWaypoints[i] }); character.Info.StartItemsGiven = true; + if (character.Info.OrderData != null) { character.Info.ApplyOrderData(); } } - + AddCharacter(character, sortCrewList: false); #if CLIENT if (IsSinglePlayer && (Character.Controlled == null || character.Info.LastControlled)) { Character.Controlled = character; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs index b08da08a6..548d30d55 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs @@ -59,6 +59,8 @@ namespace Barotrauma public LocalizedString Description { get; } public LocalizedString ShortDescription { get; } + public Identifier OpposingFaction { get; } + public class HireableCharacter { public readonly Identifier NPCSetIdentifier; @@ -150,6 +152,7 @@ namespace Barotrauma Name = element.GetAttributeString("name", null) ?? TextManager.Get($"faction.{Identifier}").Fallback("Unnamed"); Description = element.GetAttributeString("description", null) ?? TextManager.Get($"faction.{Identifier}.description").Fallback(""); ShortDescription = element.GetAttributeString("shortdescription", null) ?? TextManager.Get($"faction.{Identifier}.shortdescription").Fallback(""); + OpposingFaction = element.GetAttributeIdentifier(nameof(OpposingFaction), Identifier.Empty); List hireableCharacters = new List(); List automaticMissions = new List(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 9ff3c9aa4..fcf412f27 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -95,6 +95,8 @@ namespace Barotrauma public SubmarineInfo PendingSubmarineSwitch; public bool TransferItemsOnSubSwitch { get; set; } + public bool SwitchedSubsThisRound { get; private set; } + protected Map map; public Map Map { @@ -293,6 +295,7 @@ namespace Barotrauma PurchasedLostShuttlesInLatestSave = PurchasedLostShuttles = false; var connectedSubs = Submarine.MainSub.GetConnectedSubs(); wasDocked = Level.Loaded.StartOutpost != null && connectedSubs.Contains(Level.Loaded.StartOutpost); + SwitchedSubsThisRound = false; } public static int GetHullRepairCost() @@ -590,7 +593,7 @@ namespace Barotrauma /// protected abstract void LoadInitialLevel(); - protected abstract IEnumerable DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror, List traitorResults = null); + protected abstract IEnumerable DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror); /// /// Which type of transition between levels is currently possible (if any) @@ -623,8 +626,7 @@ namespace Barotrauma } else if (map.SelectedConnection != null) { - nextLevel = Level.Loaded.LevelData != map.SelectedConnection?.LevelData || (map.SelectedConnection.Locations[0] == Level.Loaded.EndLocation == Level.Loaded.Mirrored) ? - map.SelectedConnection.LevelData : null; + nextLevel = map.SelectedConnection.LevelData; return TransitionType.ProgressToNextEmptyLocation; } else @@ -878,6 +880,19 @@ namespace Barotrauma port.Door.IsOpen = false; } } + + foreach (Item item in Item.ItemList) + { + if (item.Submarine is not Submarine sub) { continue; } + if (!sub.Info.IsPlayer) { continue; } + if (sub.TeamID != CharacterTeamType.Team1 && sub.TeamID != CharacterTeamType.Team2) { continue; } + if (item.GetComponent() is Reactor reactor && reactor.LastAIUser != null && reactor.LastUser == reactor.LastAIUser) + { + // Reactor managed by an AI crew -> + // Turn auto temp on, so that the reactor won't be unmanaged at beginning of the next round. + reactor.AutoTemp = true; + } + } } /// @@ -896,7 +911,7 @@ namespace Barotrauma { if (c.IsOnPlayerTeam) { - c.CharacterHealth.RemoveAllAfflictions(); + c.CharacterHealth.RemoveNegativeAfflictions(); } } foreach (LocationConnection connection in Map.Connections) @@ -904,13 +919,17 @@ namespace Barotrauma connection.Difficulty = connection.Biome.AdjustedMaxDifficulty; connection.LevelData = new LevelData(connection) { - IsBeaconActive = false + IsBeaconActive = false, + ForceOutpostGenerationParams = connection.LevelData.ForceOutpostGenerationParams }; connection.LevelData.HasHuntingGrounds = connection.LevelData.OriginallyHadHuntingGrounds; } foreach (Location location in Map.Locations) { - location.LevelData = new LevelData(location, Map, location.Biome.AdjustedMaxDifficulty); + location.LevelData = new LevelData(location, Map, location.Biome.AdjustedMaxDifficulty) + { + ForceOutpostGenerationParams = location.LevelData.ForceOutpostGenerationParams + }; location.Reset(this); } Map.ClearLocationHistory(); @@ -1294,7 +1313,7 @@ namespace Barotrauma foreach (Submarine sub in subsToLeaveBehind) { GameMain.GameSession.OwnedSubmarines.RemoveAll(s => s != leavingSub.Info && s.Name == sub.Info.Name); - MapEntity.mapEntityList.RemoveAll(e => e.Submarine == sub && e is LinkedSubmarine); + MapEntity.MapEntityList.RemoveAll(e => e.Submarine == sub && e is LinkedSubmarine); LinkedSubmarine.CreateDummy(leavingSub, sub); } } @@ -1307,6 +1326,7 @@ namespace Barotrauma TransferItemsBetweenSubs(); } RefreshOwnedSubmarines(); + SwitchedSubsThisRound = true; PendingSubmarineSwitch = null; } @@ -1368,7 +1388,7 @@ namespace Barotrauma return; } // First move the cargo containers, so that we can reuse them - var cargoContainers = itemsToTransfer.Where(it => it.item.HasTag("crate")); + var cargoContainers = itemsToTransfer.Where(it => it.item.HasTag(Tags.Crate)); foreach (var (item, oldContainer) in cargoContainers) { Vector2 simPos = ConvertUnits.ToSimUnits(CargoManager.GetCargoPos(spawnHull, item.Prefab)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index 728dafe88..81a736eec 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -230,6 +230,9 @@ namespace Barotrauma Bank = new Wallet(Option.None(), subElement); break; #if SERVER + case nameof(TraitorManager): + GameMain.Server?.TraitorManager?.Load(subElement); + break; case "savedexperiencepoints": foreach (XElement savedExp in subElement.Elements()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 7c7c9e453..115d3dce2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -21,6 +21,8 @@ namespace Barotrauma public enum InfoFrameTab { Crew, Mission, MyCharacter, Traitor }; + public Version LastSaveVersion { get; set; } = GameMain.Version; + public readonly EventManager EventManager; public GameMode? GameMode; @@ -107,6 +109,10 @@ namespace Barotrauma public string? SavePath { get; set; } + public bool TraitorsEnabled => + GameMain.NetworkMember?.ServerSettings != null && + GameMain.NetworkMember.ServerSettings.TraitorProbability > 0.0f; + partial void InitProjSpecific(); private GameSession(SubmarineInfo submarineInfo) @@ -148,7 +154,8 @@ namespace Barotrauma this.SavePath = saveFile; GameMain.GameSession = this; XElement rootElement = doc.Root ?? throw new NullReferenceException("Game session XML element is invalid: document is null."); - //selectedSub.Name = doc.Root.GetAttributeString("submarine", selectedSub.Name); + + LastSaveVersion = doc.Root.GetAttributeVersion("version", GameMain.Version); foreach (var subElement in rootElement.Elements()) { @@ -300,7 +307,7 @@ namespace Barotrauma public void LoadPreviousSave() { Submarine.Unload(); - SaveUtil.LoadGame(SavePath); + SaveUtil.LoadGame(SavePath ?? ""); } /// @@ -376,7 +383,10 @@ namespace Barotrauma missionPrefab.AllowedLocationTypes.Any() && !missionPrefab.AllowedConnectionTypes.Any()) { - LocationType? locationType = LocationType.Prefabs.FirstOrDefault(lt => missionPrefab.AllowedLocationTypes.Any(m => m == lt.Identifier)); + Random rand = new MTRandom(ToolBox.StringToInt(levelSeed)); + LocationType? locationType = LocationType.Prefabs + .Where(lt => missionPrefab.AllowedLocationTypes.Any(m => m == lt.Identifier)) + .GetRandom(rand); dummyLocations = CreateDummyLocations(levelSeed, locationType); randomLevel = LevelData.CreateRandom(levelSeed, difficulty, levelGenerationParams, requireOutpost: true); break; @@ -392,7 +402,7 @@ namespace Barotrauma DateTime startTime = DateTime.Now; #endif RoundDuration = 0.0f; - AfflictionPrefab.LoadAllEffects(); + AfflictionPrefab.LoadAllEffectsAndTreatmentSuitabilities(); MirrorLevel = mirrorLevel; if (SubmarineInfo == null) @@ -572,11 +582,11 @@ namespace Barotrauma } ReadyCheck.ReadyCheckCooldown = DateTime.MinValue; - GUI.PreventPauseMenuToggle = false; - HintManager.OnRoundStarted(); + EnableEventLogNotificationIcon(enabled: false); #endif + EventManager?.EventLog?.Clear(); if (campaignMode is { DivingSuitWarningShown: false } && Level.Loaded != null && Level.Loaded.GetRealWorldDepth(0) > 4000) { @@ -605,7 +615,8 @@ namespace Barotrauma foreach (var sub in Submarine.Loaded) { - if (sub.Info.IsOutpost) + // TODO: Currently there's no need to check these on ruins, but that might change -> Could maybe just check if the body is static? + if (sub.Info.IsOutpost || sub.Info.IsBeacon || sub.Info.IsWreck) { sub.DisableObstructedWayPoints(); } @@ -759,7 +770,7 @@ namespace Barotrauma // Make sure that linked subs which are NOT docked to the main sub // (but still close enough to NOT be considered as 'left behind') // are also moved to keep their relative position to the main sub - var linkedSubs = MapEntity.mapEntityList.FindAll(me => me is LinkedSubmarine); + var linkedSubs = MapEntity.MapEntityList.FindAll(me => me is LinkedSubmarine); foreach (LinkedSubmarine ls in linkedSubs) { if (ls.Sub == null || ls.Submarine != Submarine) { continue; } @@ -845,7 +856,11 @@ namespace Barotrauma return characters.ToImmutableHashSet(); } - public void EndRound(string endMessage, List? traitorResults = null, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None) +#if SERVER + private double LastEndRoundErrorMessageTime; +#endif + + public void EndRound(string endMessage, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None, TraitorManager.TraitorResults? traitorResults = null) { RoundEnding = true; @@ -854,10 +869,10 @@ namespace Barotrauma try { + EventManager?.TriggerOnEndRoundActions(); + ImmutableHashSet crewCharacters = GetSessionCrewCharacters(CharacterType.Both); - int prevMoney = GetAmountOfMoney(crewCharacters); - foreach (Mission mission in missions) { mission.End(); @@ -898,11 +913,11 @@ namespace Barotrauma GUI.PreventPauseMenuToggle = true; - if (!(GameMode is TestGameMode) && Screen.Selected == GameMain.GameScreen && RoundSummary != null && transitionType != CampaignMode.TransitionType.End) + if (GameMode is not TestGameMode && Screen.Selected == GameMain.GameScreen && RoundSummary != null && transitionType != CampaignMode.TransitionType.End) { GUI.ClearMessages(); GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData is RoundSummary); - GUIFrame summaryFrame = RoundSummary.CreateSummaryFrame(this, endMessage, traitorResults, transitionType); + GUIFrame summaryFrame = RoundSummary.CreateSummaryFrame(this, endMessage, transitionType, traitorResults); GUIMessageBox.MessageBoxes.Add(summaryFrame); RoundSummary.ContinueButton.OnClicked = (_, __) => { GUIMessageBox.MessageBoxes.Remove(summaryFrame); return true; }; } @@ -915,6 +930,9 @@ namespace Barotrauma #endif SteamAchievementManager.OnRoundEnded(this); +#if SERVER + GameMain.Server?.TraitorManager?.EndRound(); +#endif GameMode?.End(transitionType); EventManager?.EndRound(); StatusEffect.StopAll(); @@ -924,14 +942,16 @@ namespace Barotrauma #if CLIENT bool success = CrewManager!.GetCharacters().Any(c => !c.IsDead); #else - bool success = GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead); + bool success = + GameMain.Server != null && + GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead); #endif GameAnalyticsManager.AddProgressionEvent( success ? GameAnalyticsManager.ProgressionStatus.Complete : GameAnalyticsManager.ProgressionStatus.Fail, GameMode?.Preset.Identifier.Value ?? "none", RoundDuration); string eventId = "EndRound:" + (GameMode?.Preset?.Identifier.Value ?? "none") + ":"; - LogEndRoundStats(eventId); + LogEndRoundStats(eventId, traitorResults); if (GameMode is CampaignMode campaignMode) { GameAnalyticsManager.AddDesignEvent(eventId + "MoneyEarned", GetAmountOfMoney(crewCharacters) - prevMoney); @@ -942,6 +962,19 @@ namespace Barotrauma #endif missions.Clear(); } + catch (Exception e) + { + string errorMsg = "Unknown error while ending the round."; + DebugConsole.ThrowError(errorMsg, e); + GameAnalyticsManager.AddErrorEventOnce("GameSession.EndRound:UnknownError", GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + e.StackTrace); +#if SERVER + if (Timing.TotalTime > LastEndRoundErrorMessageTime + 1.0) + { + GameMain.Server?.SendChatMessage(errorMsg + "\n" + e.StackTrace, Networking.ChatMessageType.Error); + LastEndRoundErrorMessageTime = Timing.TotalTime; + } +#endif + } finally { RoundEnding = false; @@ -949,7 +982,7 @@ namespace Barotrauma int GetAmountOfMoney(IEnumerable crew) { - if (!(GameMode is CampaignMode campaign)) { return 0; } + if (GameMode is not CampaignMode campaign) { return 0; } return GameMain.NetworkMember switch { @@ -959,7 +992,7 @@ namespace Barotrauma } } - public void LogEndRoundStats(string eventId) + public void LogEndRoundStats(string eventId, TraitorManager.TraitorResults? traitorResults = null) { GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"), RoundDuration); GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name.Value ?? "none"), RoundDuration); @@ -1003,6 +1036,12 @@ namespace Barotrauma } } + if (traitorResults.HasValue) + { + GameAnalyticsManager.AddDesignEvent($"TraitorEvent:{traitorResults.Value.TraitorEventIdentifier}:{traitorResults.Value.ObjectiveSuccessful}"); + GameAnalyticsManager.AddDesignEvent($"TraitorEvent:{traitorResults.Value.TraitorEventIdentifier}:{(traitorResults.Value.VotedCorrectTraitor ? "TraitorIdentifier" : "TraitorUnidentified")}"); + } + foreach (Character c in GetSessionCrewCharacters(CharacterType.Both)) { foreach (var itemSelectedDuration in c.ItemSelectedDurations) @@ -1134,6 +1173,7 @@ namespace Barotrauma #warning TODO: after this gets on main, replace savetime with the commented line //rootElement.Add(new XAttribute("savetime", SerializableDateTime.LocalNow)); + LastSaveVersion = GameMain.Version; rootElement.Add(new XAttribute("version", GameMain.Version)); if (Submarine?.Info != null && !Submarine.Removed && Campaign != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index b1effb055..c6522899c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -26,6 +26,8 @@ namespace Barotrauma public static readonly List AnySlot = new List() { InvSlotType.Any }; + public static bool IsHandSlotType(InvSlotType s) => s.HasFlag(InvSlotType.LeftHand) || s.HasFlag(InvSlotType.RightHand); + protected bool[] IsEquipped; /// @@ -92,7 +94,7 @@ namespace Barotrauma DebugConsole.ThrowError($"Character \"{character.SpeciesName}\" is configured to spawn with more items than it has inventory capacity for."); } #if DEBUG - else if (itemCount > capacity - 2) + else if (itemCount > capacity - 2 && !character.IsPet && capacity > 0) { DebugConsole.ThrowError( $"Character \"{character.SpeciesName}\" is configured to spawn with so many items it will have less than 2 free inventory slots. " + @@ -132,6 +134,13 @@ namespace Barotrauma DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("CharacterInventory:FailedToSpawnInitialItem", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); } + else if (!character.Enabled) + { + foreach (var heldItem in character.HeldItems) + { + if (item.body != null) { item.body.Enabled = false; } + } + } }); } } @@ -207,6 +216,10 @@ namespace Barotrauma base.RemoveItem(item); #if CLIENT CreateSlots(); + if (character == Character.Controlled) + { + character.SelectedItem?.GetComponent()?.OnViewUpdateProjSpecific(); + } #endif CharacterHUD.RecreateHudTextsIfControlling(character); //if the item was equipped and there are more items in the same stack, equip one of those items @@ -509,14 +522,20 @@ namespace Barotrauma if (character == Character.Controlled) { HintManager.OnObtainedItem(character, item); + character.SelectedItem?.GetComponent()?.OnViewUpdateProjSpecific(); } #endif CharacterHUD.RecreateHudTextsIfControlling(character); if (item.CampaignInteractionType == CampaignMode.InteractionType.Cargo) { - item.CampaignInteractionType = CampaignMode.InteractionType.None; + item.AssignCampaignInteractionType(CampaignMode.InteractionType.None); } } + protected override void CreateNetworkEvent(Range slotRange) + { + GameMain.NetworkMember?.CreateEntityEvent(character, new Character.InventoryStateEventData(slotRange)); + } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index 04a879f24..81ecc5a01 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -975,9 +975,9 @@ namespace Barotrauma.Items.Components docked = false; + Item.Submarine.RefreshOutdoorNodes(); Item.Submarine.EnableObstructedWaypoints(DockingTarget.Item.Submarine); obstructedWayPointsDisabled = false; - Item.Submarine.RefreshOutdoorNodes(); DockingTarget.Undock(); DockingTarget = null; @@ -1111,8 +1111,8 @@ namespace Barotrauma.Items.Components } if (!obstructedWayPointsDisabled && dockingState >= 0.99f) { - Item.Submarine.DisableObstructedWayPoints(DockingTarget?.Item.Submarine); Item.Submarine.RefreshOutdoorNodes(); + Item.Submarine.DisableObstructedWayPoints(DockingTarget?.Item.Submarine); obstructedWayPointsDisabled = true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index e282bbb82..41ff58290 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -41,6 +41,8 @@ namespace Barotrauma.Items.Components } private bool isStuck; + + [Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public bool IsStuck { get { return isStuck; } @@ -49,7 +51,10 @@ namespace Barotrauma.Items.Components if (isStuck == value) { return; } isStuck = value; #if SERVER - item.CreateServerEvent(this); + if (item.FullyInitialized) + { + item.CreateServerEvent(this); + } #endif } } @@ -105,7 +110,7 @@ namespace Barotrauma.Items.Components public bool CanBeWelded = true; private float stuck; - [Serialize(0.0f, IsPropertySaveable.No, description: "How badly stuck the door is (in percentages). If the percentage reaches 100, the door needs to be cut open to make it usable again.")] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How badly stuck the door is (in percentages). If the percentage reaches 100, the door needs to be cut open to make it usable again.")] public float Stuck { get { return stuck; } @@ -181,7 +186,11 @@ namespace Barotrauma.Items.Components [Serialize(false, IsPropertySaveable.No, description: "If the door has integrated buttons, it can be opened by interacting with it directly (instead of using buttons wired to it).")] public bool HasIntegratedButtons { get; private set; } - + + [ConditionallyEditable(ConditionallyEditable.ConditionType.HasIntegratedButtons), + Serialize(true, IsPropertySaveable.No, description: "If the door has integrated buttons, should clicking on it perform the default action of opening the door? Can be used in conjunction with the \"activate_out\" output to pass a signal to a circuit without toggling the door when someone tries to open/close the door.")] + public bool ToggleWhenClicked { get; private set; } + public float OpenState { get { return openState; } @@ -342,8 +351,12 @@ namespace Barotrauma.Items.Components OnFailedToOpen(); return; } + item.SendSignal("1", "activate_out"); lastUser = user; - SetState(PredictedState == null ? !isOpen : !PredictedState.Value, false, true, forcedOpen: actionType == ActionType.OnPicked); + if (ToggleWhenClicked) + { + SetState(PredictedState == null ? !isOpen : !PredictedState.Value, false, true, forcedOpen: actionType == ActionType.OnPicked); + } } public override bool Select(Character character) @@ -389,8 +402,6 @@ namespace Barotrauma.Items.Components return; } - - bool isClosing = false; if ((!IsStuck && !IsJammed) || !isOpen) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs index b039709e8..39fd2ed7f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs @@ -18,8 +18,6 @@ namespace Barotrauma.Items.Components const int MaxNodes = 100; const float MaxNodeDistance = 150.0f; - private bool waitForVoltageRecalculation; - public struct Node { public Vector2 WorldPosition; @@ -140,10 +138,6 @@ namespace Barotrauma.Items.Components if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; } if (character != null && !CharacterUsable) { return false; } - CurrPowerConsumption = powerConsumption; - Voltage = 0.0f; - - waitForVoltageRecalculation = true; charging = true; timer = Duration; IsActive = true; @@ -159,12 +153,6 @@ namespace Barotrauma.Items.Components #if CLIENT frameOffset = Rand.Int(electricitySprite.FrameCount); #endif - if (waitForVoltageRecalculation) - { - waitForVoltageRecalculation = false; - return; - } - if (timer <= 0.0f) { if (reloadTimer > 0.0f) @@ -179,40 +167,49 @@ namespace Barotrauma.Items.Components timer -= deltaTime; if (charging) { - if (GetAvailableInstantaneousBatteryPower() >= PowerConsumption) + bool hasPower = false; + if (item.Connections == null) { - List batteries = GetDirectlyConnectedBatteries(); - float neededPower = PowerConsumption; - while (neededPower > 0.0001f && batteries.Count > 0) + //no connections and can't be wired = must be powered by something like batteries + hasPower = Voltage > MinVoltage; + } + else + { + hasPower = GetAvailableInstantaneousBatteryPower() >= PowerConsumption; + } + + if (hasPower) + { + var batteries = GetDirectlyConnectedBatteries().Where(static b => !b.OutputDisabled && b.Charge > 0.0001f && b.MaxOutPut > 0.0001f); + int batteryCount = batteries.Count(); + if (batteryCount > 0) { - batteries.RemoveAll(b => b.Charge <= 0.0001f || b.MaxOutPut <= 0.0001f); - float takePower = neededPower / batteries.Count; - takePower = Math.Min(takePower, batteries.Min(b => Math.Min(b.Charge * 3600.0f, b.MaxOutPut))); - foreach (PowerContainer battery in batteries) + float neededPower = PowerConsumption; + while (neededPower > 0.0001f) { - neededPower -= takePower; - battery.Charge -= takePower / 3600.0f; - #if SERVER - if (GameMain.Server != null) { battery.Item.CreateServerEvent(battery); } - #endif + float takePower = neededPower / batteryCount; + takePower = Math.Min(takePower, batteries.Min(b => Math.Min(b.Charge * 3600.0f, b.MaxOutPut))); + foreach (PowerContainer battery in batteries) + { + neededPower -= takePower; + battery.Charge -= takePower / 3600.0f; +#if SERVER + if (GameMain.Server != null) { battery.Item.CreateServerEvent(battery); } +#endif + } } } Discharge(); - - } - else if (Voltage > MinVoltage) - { - Discharge(); } } } /// - /// Discharge coil only draws power when charging + /// Discharge coil doesn't consume grid power, directly takes from the batteries on its grid instead. /// - public override float GetCurrentPowerConsumption(Connection connection = null) + public override float GetCurrentPowerConsumption(Connection conn = null) { - return charging && IsActive ? PowerConsumption : 0; + return 0; } public override void UpdateBroken(float deltaTime, Camera cam) @@ -294,10 +291,24 @@ namespace Barotrauma.Items.Components if (!submarinesInRange.Contains(structure.Submarine)) { continue; } if (OutdoorsOnly) { - //check if the structure is within a hull - //add a small offset away from the sub's center so structures right at the edge of a hull are still valid - Vector2 offset = Vector2.Normalize(structure.WorldPosition - structure.Submarine.WorldPosition); - if (Hull.FindHull(structure.Position + offset * Submarine.GridSize, useWorldCoordinates: false) != null) { continue; } + //check if there's a hull at either side of the wall + Vector2 normal = new Vector2( + (float)-Math.Sin(structure.IsHorizontal ? -structure.BodyRotation : MathHelper.PiOver2 - structure.BodyRotation), + (float)Math.Cos(structure.IsHorizontal ? -structure.BodyRotation : MathHelper.PiOver2 - structure.BodyRotation)); + Vector2 structurePos = structure.Position; + float offsetAmount = Submarine.GridSize.X * 2; + if (structure.HasBody) + { + structurePos = ConvertUnits.ToDisplayUnits(structure.Bodies.First().Position); + offsetAmount = Math.Max( + offsetAmount, + structure.IsHorizontal ? structure.BodyHeight : structure.BodyWidth); + } + if (Hull.FindHull(structurePos + normal * offsetAmount, useWorldCoordinates: false) != null && + Hull.FindHull(structurePos - normal * offsetAmount, useWorldCoordinates: false) != null) + { + continue; + } } } @@ -586,7 +597,7 @@ namespace Barotrauma.Items.Components case "trigger_in": if (signal.value != "0") { - item.Use(1.0f, null); + item.Use(1.0f); } break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs index 2ecabc5e7..f15afb13c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs @@ -300,7 +300,7 @@ namespace Barotrauma.Items.Components var (x, y, z, w) = Parent.GrowthWeights; float[] weights = { x, y, z, w }; - value = pool.RandomElementByWeight(i => weights[i]); + value = pool.GetRandomByWeight(i => weights[i], Rand.RandSync.Unsynced); } return (TileSide) (1 << value); @@ -416,8 +416,8 @@ namespace Barotrauma.Items.Components set => health = Math.Clamp(value, 0, MaxHealth); } - public bool Decayed; - public bool FullyGrown; + public bool Decayed { get; set; } + public bool FullyGrown { get; set; } private const int maxProductDelay = 10, maxVineGrowthDelay = 10; @@ -562,7 +562,7 @@ namespace Barotrauma.Items.Components if (spawnProduct && ProducedItems.Any()) { - SpawnItem(Item, ProducedItems.RandomElementByWeight(it => it.Probability), spawnPos); + SpawnItem(Item, ProducedItems.GetRandomByWeight(it => it.Probability, Rand.RandSync.Unsynced), spawnPos); return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index 14d8699c1..bc06c7c4b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -211,6 +211,9 @@ namespace Barotrauma.Items.Components #endif public bool DisableHeadRotation { get; set; } + [Serialize(false, IsPropertySaveable.No, description: "If true, this item can't be used if the character is also holding a ranged weapon.")] + public bool DisableWhenRangedWeaponEquipped { get; set; } + [ConditionallyEditable(ConditionallyEditable.ConditionType.Attachable, MinValueFloat = 0.0f, MaxValueFloat = 0.999f, DecimalCount = 3), Serialize(0.55f, IsPropertySaveable.No, description: "Sprite depth that's used when the item is NOT attached to a wall.")] public float SpriteDepthWhenDropped { @@ -343,11 +346,9 @@ namespace Barotrauma.Items.Components DeattachFromWall(); } - if (setTransform) - { - if (Pusher != null) { Pusher.Enabled = false; } - if (item.body != null) { item.body.Enabled = true; } - } + if (Pusher != null) { Pusher.Enabled = false; } + if (item.body != null) { item.body.Enabled = true; } + IsActive = false; attachTargetCell = null; @@ -616,6 +617,7 @@ namespace Barotrauma.Items.Components { //set to submarine-relative position item.SetTransform(ConvertUnits.ToSimUnits(item.WorldPosition - attachTarget.Submarine.Position), 0.0f, false); + body.SetTransformIgnoreContacts(item.SimPosition, 0.0f); } item.Submarine = attachTarget.Submarine; } @@ -688,6 +690,7 @@ namespace Barotrauma.Items.Components public override bool Use(float deltaTime, Character character = null) { + if (UsageDisabledByRangedWeapon(character)) { return false; } if (!attachable || item.body == null) { return character == null || (character.IsKeyDown(InputType.Aim) && characterUsable); } if (character != null) { @@ -792,19 +795,27 @@ namespace Barotrauma.Items.Components Vector2 userPos = useWorldCoordinates ? user.WorldPosition : user.Position; Vector2 attachPos = userPos + mouseDiff; + //offset the position by half the size of the grid to get the item to adhere to the grid in the same way as in the sub editor + //in the sub editor, we align the top-left corner of the item with the grid + //but here the origin of the item is placed at the attach position, so we need to offset it + Vector2 offset = new Vector2( + -(item.Rect.Width / 2) % Submarine.GridSize.X, + (item.Rect.Height / 2) % Submarine.GridSize.Y); + if (user.Submarine != null) { if (Submarine.PickBody( ConvertUnits.ToSimUnits(user.Position), ConvertUnits.ToSimUnits(user.Position + mouseDiff), collisionCategory: Physics.CollisionWall) != null) { - attachPos = userPos + mouseDiff * Submarine.LastPickedFraction; + attachPos = userPos + mouseDiff * Submarine.LastPickedFraction + offset; //round down if we're placing on the right side and vice versa: ensures we don't round the position inside a wall return new Vector2( - mouseDiff.X > 0 ? (float)Math.Floor(attachPos.X / Submarine.GridSize.X) * Submarine.GridSize.X : (float)Math.Ceiling(attachPos.X / Submarine.GridSize.X) * Submarine.GridSize.X, - mouseDiff.Y > 0 ? (float)Math.Floor(attachPos.Y / Submarine.GridSize.Y) * Submarine.GridSize.X : (float)Math.Ceiling(attachPos.Y / Submarine.GridSize.Y) * Submarine.GridSize.Y); + (mouseDiff.X > 0 ? MathF.Floor(attachPos.X / Submarine.GridSize.X) : MathF.Ceiling(attachPos.X / Submarine.GridSize.X)) * Submarine.GridSize.X, + (mouseDiff.Y > 0 ? MathF.Floor(attachPos.Y / Submarine.GridSize.Y) : MathF.Ceiling(attachPos.Y / Submarine.GridSize.Y)) * Submarine.GridSize.Y) + - offset; } } else if (Level.Loaded != null) @@ -827,10 +838,9 @@ namespace Barotrauma.Items.Components } } - return - new Vector2( - MathUtils.RoundTowardsClosest(attachPos.X, Submarine.GridSize.X), - MathUtils.RoundTowardsClosest(attachPos.Y, Submarine.GridSize.Y)); + return new Vector2( + MathUtils.RoundTowardsClosest(attachPos.X + offset.X, Submarine.GridSize.X), + MathUtils.RoundTowardsClosest(attachPos.Y + offset.Y, Submarine.GridSize.Y)) - offset; } private Voronoi2.VoronoiCell GetAttachTargetCell(float maxDist) @@ -903,7 +913,7 @@ namespace Barotrauma.Items.Components { scaledHandlePos[0] = handlePos[0] * item.Scale; scaledHandlePos[1] = handlePos[1] * item.Scale; - bool aim = picker.IsKeyDown(InputType.Aim) && aimPos != Vector2.Zero && picker.CanAim; + bool aim = picker.IsKeyDown(InputType.Aim) && aimPos != Vector2.Zero && picker.CanAim && !UsageDisabledByRangedWeapon(picker); if (aim) { picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swingPos, aimPos + swingPos, aim, holdAngle, aimAngle); @@ -963,6 +973,15 @@ namespace Barotrauma.Items.Components } } + protected bool UsageDisabledByRangedWeapon(Character character) + { + if (DisableWhenRangedWeaponEquipped && character != null) + { + if (character.HeldItems.Any(it => it.GetComponent() != null)) { return true; } + } + return false; + } + public override void ReceiveSignal(Signal signal, Connection connection) { //do nothing @@ -1005,14 +1024,12 @@ namespace Barotrauma.Items.Components } else { - if (item.ParentInventory != null) + if (body != null) { - if (body != null) - { - item.body = body; - body.Enabled = false; - } - } + body.SetTransformIgnoreContacts(item.SimPosition, item.Rotation); + item.body = body; + body.Enabled = item.ParentInventory == null; + } DeattachFromWall(); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs index eb57d4ba2..c482ec981 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs @@ -37,7 +37,20 @@ namespace Barotrauma.Items.Components [Serialize("", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public string OwnerName { get; set; } - + + private string ownerNameLocalized; + [Serialize("", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public string OwnerNameLocalized + { + get { return ownerNameLocalized; } + set + { + if (value.IsNullOrWhiteSpace()) { return; } + ownerNameLocalized = value; + OwnerName = TextManager.Get(value).Fallback(value).Value; + } + } + [Serialize("", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public Identifier OwnerJobId { get; set; } @@ -67,6 +80,9 @@ namespace Barotrauma.Items.Components [Serialize("0,0", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public Vector2 OwnerSheetIndex { get; set; } + [Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public bool SpawnPointTagsGiven { get; set; } + public IdCard(Item item, ContentXElement element) : base(item, element) { } public void Initialize(WayPoint spawnPoint, Character character) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs index 353d25d9a..67c3ff90d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs @@ -135,7 +135,7 @@ namespace Barotrauma.Items.Components private void CreateTriggerBody() { System.Diagnostics.Debug.Assert(trigger == null, "LevelResource trigger already created!"); - var body = item.body ?? holdable.Body; + var body = item.body ?? holdable?.Body; if (body != null && Attached) { trigger = new PhysicsBody(body.Width, body.Height, body.Radius, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index 817f07d59..05fac61cd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -189,7 +189,7 @@ namespace Barotrauma.Items.Components impactQueue.Clear(); return; } - if (picker == null && !picker.HeldItems.Contains(item)) + if (picker == null || !picker.HeldItems.Contains(item)) { impactQueue.Clear(); IsActive = false; @@ -218,7 +218,8 @@ namespace Barotrauma.Items.Components AnimController ac = picker.AnimController; if (!hitting) { - bool aim = item.RequireAimToUse && picker.AllowInput && picker.IsKeyDown(InputType.Aim) && reloadTimer <= 0 && picker.CanAim; + bool aim = item.RequireAimToUse && picker.AllowInput && picker.IsKeyDown(InputType.Aim) && reloadTimer <= 0 && picker.CanAim && + !UsageDisabledByRangedWeapon(picker); if (aim) { UpdateSwingPos(deltaTime, out Vector2 swingPos); @@ -364,7 +365,7 @@ namespace Barotrauma.Items.Components } hitTargets.Add(targetStructure); } - else if (f2.Body.UserData is Item targetItem) + else if ((f2.Body.UserData as Item ?? f2.UserData as Item) is Item targetItem) { if (AllowHitMultiple) { @@ -410,7 +411,7 @@ namespace Barotrauma.Items.Components Limb targetLimb = target.UserData as Limb; Character targetCharacter = targetLimb?.character ?? target.UserData as Character; Structure targetStructure = target.UserData as Structure ?? targetFixture.UserData as Structure; - Item targetItem = target.UserData as Item; + Item targetItem = target.UserData as Item ?? targetFixture.UserData as Item; Entity targetEntity = targetCharacter ?? targetStructure ?? targetItem ?? target.UserData as Entity; if (Attack != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs index 03f31ed8c..0e3083684 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs @@ -73,6 +73,7 @@ namespace Barotrauma.Items.Components { //return if someone is already trying to pick the item if (pickTimer > 0.0f) { return false; } + if (PickingTime >= float.MaxValue) { return false; } if (picker == null || picker.Inventory == null) { return false; } if (!picker.Inventory.AccessibleWhenAlive && !picker.Inventory.AccessibleByOwner) { return false; } @@ -112,10 +113,16 @@ namespace Barotrauma.Items.Components } } + public virtual bool OnPicked(Character picker) + { + return OnPicked(picker, pickDroppedStack: true); + } + + public virtual bool OnPicked(Character picker, bool pickDroppedStack) { //if the item has multiple Pickable components (e.g. Holdable and Wearable, check that we don't equip it in hands when the item is worn or vice versa) - if (item.GetComponents().Count() > 0) + if (item.GetComponents().Any()) { bool alreadyEquipped = false; for (int i = 0; i < picker.Inventory.Capacity; i++) @@ -132,11 +139,13 @@ namespace Barotrauma.Items.Components } if (alreadyEquipped) { return false; } } + var droppedStack = pickDroppedStack ? item.DroppedStack.ToList() : null; if (picker.Inventory.TryPutItemWithAutoEquipCheck(item, picker, allowedSlots)) { if (!picker.HeldItems.Contains(item) && item.body != null) { item.body.Enabled = false; } this.picker = picker; + for (int i = item.linkedTo.Count - 1; i >= 0; i--) { item.linkedTo[i].RemoveLinked(item); @@ -147,14 +156,22 @@ namespace Barotrauma.Items.Components ApplyStatusEffects(ActionType.OnPicked, 1.0f, picker); #if CLIENT - if (!GameMain.Instance.LoadingScreenOpen && picker == Character.Controlled) SoundPlayer.PlayUISound(GUISoundType.PickItem); + if (!GameMain.Instance.LoadingScreenOpen && picker == Character.Controlled) { SoundPlayer.PlayUISound(GUISoundType.PickItem); } PlaySound(ActionType.OnPicked, picker); #endif + if (pickDroppedStack) + { + foreach (var droppedItem in droppedStack) + { + if (droppedItem == item) { continue; } + droppedItem.GetComponent().OnPicked(picker, pickDroppedStack: false); + } + } return true; } #if CLIENT - if (!GameMain.Instance.LoadingScreenOpen && picker == Character.Controlled) SoundPlayer.PlayUISound(GUISoundType.PickItemFail); + if (!GameMain.Instance.LoadingScreenOpen && picker == Character.Controlled) { SoundPlayer.PlayUISound(GUISoundType.PickItemFail); } #endif return false; @@ -183,12 +200,15 @@ namespace Barotrauma.Items.Components } #if CLIENT - Character.Controlled?.UpdateHUDProgressBar( - this, - item.WorldPosition, - pickTimer / requiredTime, - GUIStyle.Red, GUIStyle.Green, - !string.IsNullOrWhiteSpace(PickingMsg) ? PickingMsg : this is Door ? "progressbar.opening" : "progressbar.deattaching"); + if (requiredTime < float.MaxValue) + { + Character.Controlled?.UpdateHUDProgressBar( + this, + item.WorldPosition, + pickTimer / requiredTime, + GUIStyle.Red, GUIStyle.Green, + !string.IsNullOrWhiteSpace(PickingMsg) ? PickingMsg : this is Door ? "progressbar.opening" : "progressbar.deattaching"); + } #endif picker.AnimController.UpdateUseItem(!picker.IsClimbing, item.WorldPosition + new Vector2(0.0f, 100.0f) * ((pickTimer / 10.0f) % 0.1f)); pickTimer += CoroutineManager.DeltaTime; @@ -197,12 +217,14 @@ namespace Barotrauma.Items.Components } StopPicking(picker); - - bool isNotRemote = true; + if (!item.Removed) + { + bool isNotRemote = true; #if CLIENT - isNotRemote = !picker.IsRemotePlayer; + isNotRemote = !picker.IsRemotePlayer; #endif - if (isNotRemote) OnPicked(picker); + if (isNotRemote) { OnPicked(picker); } + } yield return CoroutineStatus.Success; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index 71d2510a9..e5dfda8a4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -608,6 +608,7 @@ namespace Barotrauma.Items.Components float closestDist = float.MaxValue; foreach (Limb limb in targetCharacter.AnimController.Limbs) { + if (limb.Removed || limb.IgnoreCollisions || limb.Hidden || limb.IsSevered) { continue; } float dist = Vector2.DistanceSquared(item.SimPosition, limb.SimPosition); if (dist < closestDist) { @@ -716,7 +717,7 @@ namespace Barotrauma.Items.Components private readonly float repairTimeOut = 5; public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { - if (!(objective.OperateTarget is Gap leak)) + if (objective.OperateTarget is not Gap leak) { Reset(); return true; @@ -806,36 +807,50 @@ namespace Barotrauma.Items.Components // Press the trigger only when the tool is approximately facing the target. Vector2 fromItemToLeak = leak.WorldPosition - item.WorldPosition; var angle = VectorExtensions.Angle(VectorExtensions.Forward(item.body.TransformedRotation), fromItemToLeak); + bool repair = true; if (angle < MathHelper.PiOver4) { if (Submarine.PickBody(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionWall, allowInsideFixture: true)?.UserData is Item i) - { - var door = i.GetComponent(); - // Hit a door, abandon so that we don't weld it shut. - return door != null && !door.CanBeTraversed; + { + if (i.GetComponent() is Door door && !door.CanBeTraversed ) + { + // Hit a door, don't repair so that we don't weld it shut. + if (door.Stuck > 90) + { + // Almost stuck -> just abandon. + return false; + } + if (door.Stuck > 50) + { + repair = false; + } + } } - // Check that we don't hit any friendlies - if (Submarine.PickBodies(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionCharacter).None(hit => + if (repair) { - if (hit.UserData is Character c) + // Check that we don't hit any friendlies + if (Submarine.PickBodies(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionCharacter).None(hit => { - if (c == character) { return false; } - return HumanAIController.IsFriendly(character, c); + if (hit.UserData is Character c) + { + if (c == character) { return false; } + return HumanAIController.IsFriendly(character, c); + } + return false; + })) + { + character.SetInput(InputType.Shoot, false, true); + Use(deltaTime, character); } - return false; - })) + } + repairTimer += deltaTime; + if (repairTimer > repairTimeOut) { - character.SetInput(InputType.Shoot, false, true); - Use(deltaTime, character); - repairTimer += deltaTime; - if (repairTimer > repairTimeOut) - { #if DEBUG - DebugConsole.NewMessage($"{character.Name}: timed out while welding a leak in {leak.FlowTargetHull.DisplayName}.", color: Color.Yellow); + DebugConsole.NewMessage($"{character.Name}: timed out while welding a leak in {leak.FlowTargetHull.DisplayName}.", color: Color.Yellow); #endif - Reset(); - return true; - } + Reset(); + return true; } } } @@ -912,7 +927,7 @@ namespace Barotrauma.Items.Components // A general purpose system could be better, but it would most likely require changes in the way we define the status effects in xml. foreach (ISerializableEntity target in currentTargets) { - if (target is not Door door) { continue; } + if (target is not Door door) { continue; } if (!door.CanBeWelded || !door.Item.IsInteractable(user)) { continue; } foreach (var propertyEffect in effect.PropertyEffects) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index b70f89e16..c73ea2f41 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -22,7 +22,7 @@ namespace Barotrauma.Items.Components /// Vector2 DrawSize { get; } - void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1); + void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null); #endif } @@ -111,7 +111,6 @@ namespace Barotrauma.Items.Components private bool drawable = true; - #warning TODO: misnomer - should be IsActiveConditionalLogicalOperator [Serialize(PropertyConditional.LogicalOperatorType.And, IsPropertySaveable.No)] public PropertyConditional.LogicalOperatorType IsActiveConditionalComparison { @@ -159,6 +158,12 @@ namespace Barotrauma.Items.Components protected set; } + [Serialize(false, IsPropertySaveable.Yes), ConditionallyEditable(ConditionallyEditable.ConditionType.OnlyByStatusEffectsAndNetwork, onlyInEditors: false)] + public bool LockGuiFramePosition { get; set; } + + [Serialize("0,0", IsPropertySaveable.Yes), ConditionallyEditable(ConditionallyEditable.ConditionType.OnlyByStatusEffectsAndNetwork, onlyInEditors: false)] + public Point GuiFrameOffset { get; set; } + [Serialize(false, IsPropertySaveable.No, description: "Can the item be selected by interacting with it.")] public bool CanBeSelected { @@ -251,6 +256,9 @@ namespace Barotrauma.Items.Components /// public float Speed => item.Speed; + public readonly record struct ItemUseInfo(Item Item, Character User); + public readonly NamedEvent OnUsed = new(); + public readonly bool InheritStatusEffects; public ItemComponent(Item item, ContentXElement element) @@ -339,6 +347,7 @@ namespace Barotrauma.Items.Components switch (subElement.Name.ToString().ToLowerInvariant()) { case "activeconditional": + case "isactiveconditional": case "isactive": IsActiveConditionals ??= new List(); IsActiveConditionals.AddRange(PropertyConditional.FromXElement(subElement)); @@ -487,7 +496,7 @@ namespace Barotrauma.Items.Components case "trigger_in": if (signal.value != "0") { - item.Use(1.0f, signal.sender); + item.Use(1.0f, user: signal.sender); } break; case "toggle": @@ -524,7 +533,7 @@ namespace Barotrauma.Items.Components } item.ParentInventory.RemoveItem(item); } - Entity.Spawner.AddItemToRemoveQueue(item); + RemoveItem(item); } else { @@ -540,12 +549,23 @@ namespace Barotrauma.Items.Components } this.Item.ParentInventory.RemoveItem(this.Item); } - Entity.Spawner.AddItemToRemoveQueue(this.Item); + RemoveItem(this.Item); } else { this.Item.Condition += transferAmount; } + static void RemoveItem(Item item) + { + if (Screen.Selected is { IsEditor: true }) + { + item?.Remove(); + } + else + { + Entity.Spawner?.AddItemToRemoveQueue(item); + } + } } else { @@ -746,7 +766,7 @@ namespace Barotrauma.Items.Components /// private bool CheckIdCardAccess(RelatedItem relatedItem, IdCard idCard) { - if (item.Submarine != null) + if (item.Submarine != null && item.Submarine != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle) { //id cards don't work in enemy subs (except on items that only require the default "idcard" tag) if (idCard.TeamID != CharacterTeamType.None && idCard.TeamID != item.Submarine.TeamID && relatedItem.Identifiers.Any(id => id != "idcard")) @@ -906,7 +926,16 @@ namespace Barotrauma.Items.Components ParseMsg(); OverrideRequiredItems(componentElement); } - +#if CLIENT + if (GuiFrame != null) + { + GuiFrame.RectTransform.ScreenSpaceOffset = GuiFrameOffset; + if (guiFrameDragHandle != null) + { + guiFrameDragHandle.Enabled = !LockGuiFramePosition; + } + } +#endif if (item.Submarine != null) { SerializableProperty.UpgradeGameVersion(this, originalElement, item.Submarine.Info.GameVersion); } } @@ -922,6 +951,11 @@ namespace Barotrauma.Items.Components public virtual void OnScaleChanged() { } + /// + /// Called when the item has an ItemContainer and the contents inside of it changed. + /// + public virtual void OnInventoryChanged() { } + public static ItemComponent Load(ContentXElement element, Item item, bool errorMessages = true) { Type type; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 74d18f5b9..e00d40f5e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -1,12 +1,12 @@ -using Microsoft.Xna.Framework; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; +using Barotrauma.Abilities; using Barotrauma.Extensions; using FarseerPhysics; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; using System.Collections.Immutable; -using Barotrauma.Abilities; +using System.Linq; +using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -14,11 +14,11 @@ namespace Barotrauma.Items.Components { readonly record struct ActiveContainedItem(Item Item, StatusEffect StatusEffect, bool ExcludeBroken, bool ExcludeFullCondition); - readonly record struct DrawableContainedItem(Item Item, bool Hide, Vector2? ItemPos, float Rotation); + readonly record struct ContainedItem(Item Item, bool Hide, Vector2? ItemPos, float Rotation); class SlotRestrictions { - public readonly int MaxStackSize; + public int MaxStackSize; public List ContainableItems; public readonly bool AutoInject; @@ -55,7 +55,7 @@ namespace Barotrauma.Items.Components private readonly List activeContainedItems = new List(); - private readonly List drawableContainedItems = new List(); + private readonly List containedItems = new List(); private List[] itemIds; @@ -128,6 +128,9 @@ namespace Barotrauma.Items.Components set; } + [Serialize(true, IsPropertySaveable.Yes, description: "When this item is equipped, and you 'quick use' (double click / equip button) another equippable item, should the game attempt to move that item inside this one?")] + public bool QuickUseMovesItemsInside { get; set; } + [Serialize(false, IsPropertySaveable.No, description: "If set to true, interacting with this item will make the character interact with the contained item(s), automatically picking them up if they can be picked up.")] public bool AutoInteractWithContained { @@ -141,17 +144,28 @@ namespace Barotrauma.Items.Components [Serialize(false, IsPropertySaveable.No)] public bool AccessOnlyWhenBroken { get; set; } + [Serialize(true, IsPropertySaveable.No)] + public bool AllowAccessWhenDropped { get; set; } + [Serialize(5, IsPropertySaveable.No, description: "How many inventory slots the inventory has per row.")] public int SlotsPerRow { get; set; } - private readonly HashSet containableRestrictions = new HashSet(); + private readonly HashSet containableRestrictions = new HashSet(); [Editable, Serialize("", IsPropertySaveable.Yes, description: "Define items (by identifiers or tags) that bots should place inside this container. If empty, no restrictions are applied.")] public string ContainableRestrictions { get { return string.Join(",", containableRestrictions); } set { - StringFormatter.ParseCommaSeparatedStringToCollection(value, containableRestrictions); + containableRestrictions.Clear(); + if (!value.IsNullOrEmpty()) + { + foreach (var str in value.Split(',')) + { + if (str.IsNullOrWhiteSpace()) { continue; } + containableRestrictions.Add(str.ToIdentifier()); + } + } } } @@ -197,7 +211,6 @@ namespace Barotrauma.Items.Components [Serialize(false, IsPropertySaveable.No)] public bool RemoveContainedItemsOnDeconstruct { get; set; } - /// /// Can be used by status effects to lock the inventory /// @@ -207,11 +220,28 @@ namespace Barotrauma.Items.Components set { Inventory.Locked = value; } } + /// + /// Can be used by status effects + /// + public int ContainedItemCount + { + get => Inventory.AllItems.Count(); + } + + /// + /// Can be used by status effects + /// + public int ContainedNonBrokenItemCount + { + get => Inventory.AllItems.Count(it => it.Condition > 0.0f); + } + private readonly ImmutableArray slotRestrictions; readonly List targets = new List(); - private Vector2 prevContainedItemPositions; + private float prevContainedItemRefreshRotation; + private Vector2 prevContainedItemRefreshPosition; private float autoInjectCooldown = 1.0f; const float AutoInjectInterval = 1.0f; @@ -330,6 +360,16 @@ namespace Barotrauma.Items.Components { slotRestrictions[i].ContainableItems = ContainableItems; } +#if CLIENT + if (element.GetChildElement("clearsubcontainerrestrictions") != null) + { + for (int i = capacity - MainContainerCapacity; i < capacity; i++) + { + slotRestrictions[i].MaxStackSize = MaxStackSize; + slotIcons[i] = null; + } + } +#endif } public int GetMaxStackSize(int slotIndex) @@ -363,12 +403,31 @@ namespace Barotrauma.Items.Components } var relatedItem = FindContainableItem(containedItem); - drawableContainedItems.RemoveAll(d => d.Item == containedItem); - drawableContainedItems.Add(new DrawableContainedItem(containedItem, - Hide: relatedItem?.Hide ?? false, - ItemPos: relatedItem?.ItemPos, - Rotation: relatedItem?.Rotation ?? 0.0f)); - drawableContainedItems.Sort((DrawableContainedItem it1, DrawableContainedItem it2) => Inventory.FindIndex(it1.Item).CompareTo(Inventory.FindIndex(it2.Item))); + var containedItemInfo = new ContainedItem(containedItem, + Hide: relatedItem?.Hide ?? false, + ItemPos: relatedItem?.ItemPos, + Rotation: relatedItem?.Rotation ?? 0.0f); + containedItems.RemoveAll(d => d.Item == containedItem); + + if (hideItems) + { + //if the items aren't visible, the draw order doesn't matter and we can skip the sorting + containedItems.Add(containedItemInfo); + } + else + { + int containedIndex = 0; + while (containedIndex < containedItems.Count) + { + if (index <= Inventory.FindIndex(containedItems[containedIndex].Item)) + { + break; + } + containedIndex++; + } + //sort drawables by their order in the inventory + containedItems.Insert(containedIndex, containedItemInfo); + } if (item.GetComponent() != null) { @@ -384,6 +443,14 @@ namespace Barotrauma.Items.Components // Set the contained items active if there's an item inserted inside the container. Enables e.g. the rifle flashlight when it's attached to the rifle (put inside of it). SetContainedActive(true); } + if (containedItem.FlippedX) + { + containedItem.FlipX(relativeToSub: false); + } + if (containedItem.FlippedY) + { + containedItem.FlipY(relativeToSub: false); + } item.SetContainedItemPositions(); CharacterHUD.RecreateHudTextsIfFocused(item, containedItem); OnContainedItemsChanged.Invoke(this); @@ -397,7 +464,8 @@ namespace Barotrauma.Items.Components public void OnItemRemoved(Item containedItem) { activeContainedItems.RemoveAll(i => i.Item == containedItem); - drawableContainedItems.RemoveAll(i => i.Item == containedItem); + containedItems.RemoveAll(i => i.Item == containedItem); + item.SetContainedItemPositions(); //deactivate if the inventory is empty IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null); CharacterHUD.RecreateHudTextsIfFocused(item, containedItem); @@ -406,12 +474,14 @@ namespace Barotrauma.Items.Components public bool CanBeContained(Item item) { + if (!AllowAccessWhenDropped && this.item.body is { Enabled: true }) { return false; } return slotRestrictions.Any(s => s.MatchesItem(item)); } public bool CanBeContained(Item item, int index) { if (index < 0 || index >= capacity) { return false; } + if (!AllowAccessWhenDropped && this.item.body is { Enabled: true }) { return false; } return slotRestrictions[index].MatchesItem(item); } @@ -463,11 +533,7 @@ namespace Barotrauma.Items.Components if (item.ParentInventory is CharacterInventory ownerInventory) { - if (Vector2.DistanceSquared(prevContainedItemPositions, item.Position) > 10.0f) - { - SetContainedItemPositions(); - prevContainedItemPositions = item.Position; - } + SetContainedItemPositionsIfNeeded(); if (AutoInject || slotRestrictions.Any(s => s.AutoInject)) { @@ -508,11 +574,12 @@ namespace Barotrauma.Items.Components } } - else if (item.body != null && - item.body.Enabled && - item.body.FarseerBody.Awake) + else if (item.body != null && item.body.Enabled) { - SetContainedItemPositions(); + if (item.body.FarseerBody.Awake) + { + SetContainedItemPositionsIfNeeded(); + } } else if (activeContainedItems.Count == 0) { @@ -553,6 +620,20 @@ namespace Barotrauma.Items.Components } } + /// + /// Set the positions of the contained items if this item has moved/rotated enough + /// + private void SetContainedItemPositionsIfNeeded() + { + if (Vector2.DistanceSquared(prevContainedItemRefreshPosition, item.Position) > 10.0f || + Math.Abs(prevContainedItemRefreshRotation - item.body?.Rotation ?? item.RotationRad) > 0.01f) + { + SetContainedItemPositions(); + prevContainedItemRefreshPosition = item.Position; + prevContainedItemRefreshRotation = item.body?.Rotation ?? item.RotationRad; + } + } + public override void UpdateBroken(float deltaTime, Camera cam) { //update when the item is broken too to get OnContaining effects to execute and contained item positions to update @@ -662,10 +743,19 @@ namespace Barotrauma.Items.Components { SetContainedActive(true); } + else + { + SetContainedActive(false); + } } private void SetContainedActive(bool active) { + if ((ContainableItems == null || !ContainableItems.Any(c => c.SetActive)) && + (AllSubContainableItems == null || !AllSubContainableItems.Any(c => c.SetActive))) + { + return; + } foreach (Item containedItem in Inventory.AllItems) { RelatedItem containableItem = FindContainableItem(containedItem); @@ -722,7 +812,7 @@ namespace Barotrauma.Items.Components case "trigger_in": if (signal.value != "0") { - item.Use(1.0f, signal.sender); + item.Use(1.0f, user: signal.sender); } break; } @@ -786,7 +876,7 @@ namespace Barotrauma.Items.Components int i = 0; Vector2 currentItemPos = transformedItemPos; - foreach (DrawableContainedItem contained in drawableContainedItems) + foreach (ContainedItem contained in containedItems) { Vector2 itemPos = currentItemPos; if (contained.ItemPos.HasValue) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index a9f4467de..caa19f47c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -62,18 +62,58 @@ namespace Barotrauma.Items.Components public IEnumerable LimbPositions { get { return limbPositions; } } - [Editable, Serialize(false, IsPropertySaveable.No, description: "When enabled, the item will continuously send out a 0/1 signal and interacting with it will flip the signal (making the item behave like a switch). When disabled, the item will simply send out 1 when interacted with.", alwaysUseInstanceValues: true)] + [Editable, Serialize(false, IsPropertySaveable.No, description: "When enabled, the item will continuously send out a signal and interacting with it will flip the signal (making the item behave like a switch). When disabled, the item will simply send out a signal when interacted with.", alwaysUseInstanceValues: true)] public bool IsToggle { get; set; } - [Editable, Serialize(false, IsPropertySaveable.No, description: "Whether the item is toggled on/off. Only valid if IsToggle is set to true.", alwaysUseInstanceValues: true)] + private string output; + [ConditionallyEditable(ConditionallyEditable.ConditionType.HasConnectionPanel, onlyInEditors: false), + Serialize("1", IsPropertySaveable.Yes, description: "The signal sent when the controller is being activated or is toggled on. If empty, no signal is sent.", alwaysUseInstanceValues: true)] + public string Output + { + get { return output; } + set + { + if (value == null || value == output) { return; } + output = value; + //reactivate if signal isn't empty (we may not have been previously sending a signal, but might now) + if (!value.IsNullOrEmpty()) { IsActive = true; } + } + } + + private string falseOutput; + [ConditionallyEditable(ConditionallyEditable.ConditionType.IsToggleableController, onlyInEditors: false), + Serialize("0", IsPropertySaveable.Yes, description: "The signal sent when the controller is toggled off. If empty, no signal is sent. Only valid if IsToggle is true.", alwaysUseInstanceValues: true)] + public string FalseOutput + { + get { return falseOutput; } + set + { + if (value == null || value == falseOutput) { return; } + falseOutput = value; + //reactivate if signal isn't empty (we may not have been previously sending a signal, but might now) + if (!value.IsNullOrEmpty()) { IsActive = true; } + } + } + + private bool state; + [ConditionallyEditable(ConditionallyEditable.ConditionType.IsToggleableController, onlyInEditors: true), + Serialize(false, IsPropertySaveable.No, description: "Whether the item is toggled on/off. Only valid if IsToggle is set to true.", alwaysUseInstanceValues: true)] public bool State { - get; - set; + get { return state; } + set + { + if (state != value) + { + state = value; + string newOutput = state ? output : falseOutput; + IsActive = !string.IsNullOrEmpty(newOutput); + } + } } [Serialize(true, IsPropertySaveable.No, description: "Should the HUD (inventory, health bar, etc) be hidden when this item is selected.")] @@ -164,10 +204,11 @@ namespace Barotrauma.Items.Components this.cam = cam; UserInCorrectPosition = false; - if (IsToggle) + string signal = IsToggle && State ? output : falseOutput; + if (item.Connections != null && IsToggle && !string.IsNullOrEmpty(signal)) { - item.SendSignal(State ? "1" : "0", "signal_out"); - item.SendSignal(State ? "1" : "0", "trigger_out"); + item.SendSignal(signal, "signal_out"); + item.SendSignal(signal, "trigger_out"); } if (user == null @@ -183,7 +224,7 @@ namespace Barotrauma.Items.Components CancelUsing(user); user = null; } - if (!IsToggle || item.Connections == null) { IsActive = false; } + if (item.Connections == null || !IsToggle || string.IsNullOrEmpty(signal)) { IsActive = false; } return; } @@ -313,9 +354,9 @@ namespace Barotrauma.Items.Components #endif } } - else + else if (!string.IsNullOrEmpty(output)) { - item.SendSignal(new Signal("1", sender: user), "trigger_out"); + item.SendSignal(new Signal(output, sender: user), "trigger_out"); } lastUsed = Timing.TotalTime; @@ -419,9 +460,9 @@ namespace Barotrauma.Items.Components #endif } } - else + else if (!string.IsNullOrEmpty(output)) { - item.SendSignal(new Signal("1", sender: picker), "signal_out"); + item.SendSignal(new Signal(output, sender: picker), "signal_out"); } #if CLIENT PlaySound(ActionType.OnUse, picker); @@ -480,6 +521,7 @@ namespace Barotrauma.Items.Components IsActive = false; CancelUsing(user); user = null; + return false; } else if (user.IsBot && !activator.IsBot) { @@ -500,8 +542,11 @@ namespace Barotrauma.Items.Components } #if SERVER item.CreateServerEvent(this); -#endif - item.SendSignal(new Signal("1", sender: user), "signal_out"); +#endif + if (!string.IsNullOrEmpty(output)) + { + item.SendSignal(new Signal(output, sender: user), "signal_out"); + } return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index f8df97352..d5a5368ee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -92,7 +92,7 @@ namespace Barotrauma.Items.Components repairable.LastActiveTime = (float)Timing.TotalTime + 10.0f; } - ApplyStatusEffects(ActionType.OnActive, deltaTime, null); + ApplyStatusEffects(ActionType.OnActive, deltaTime); progressTimer += deltaTime * Math.Min(powerConsumption <= 0.0f ? 1 : Voltage, MaxOverVoltageFactor); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs index af7668a08..0796cd686 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs @@ -24,6 +24,14 @@ namespace Barotrauma.Items.Components /// private float targetForce; + /// + /// Power demand of a marine engine is proportional with the cube of the square root of the thrusting force. + /// In practice meaning lower thrust is more effective at conserving power than it would be if the relationship between thrust and power consumption was linear. + /// Reverse exponent defined for use with overvoltage calculation: Supplying 2x power will result in 59% more force, 26% more speed, therefore 2x power. + /// + private const float ForceToPowerExponent = 3f / 2f; + private const float PowerToForceExponent = 1.0f / ForceToPowerExponent; + private float maxForce; private readonly Attack propellerDamage; @@ -130,7 +138,7 @@ namespace Barotrauma.Items.Components if (Math.Abs(Force) > 1.0f) { float voltageFactor = MinVoltage <= 0.0f ? 1.0f : Math.Min(Voltage, MaxOverVoltageFactor); - float currForce = force * voltageFactor; + float currForce = force * MathF.Pow(voltageFactor, PowerToForceExponent); float condition = item.MaxCondition <= 0.0f ? 0.0f : item.Condition / item.MaxCondition; // Broken engine makes more noise. float noise = Math.Abs(currForce) * MathHelper.Lerp(1.5f, 1f, condition); @@ -180,7 +188,7 @@ namespace Barotrauma.Items.Components return 0; } - currPowerConsumption = Math.Abs(targetForce) / 100.0f * powerConsumption; + currPowerConsumption = MathF.Pow(Math.Abs(targetForce) / 100.0f, ForceToPowerExponent) * powerConsumption; //engines consume more power when in a bad condition item.GetComponent()?.AdjustPowerConsumption(ref currPowerConsumption); return currPowerConsumption; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 71af0286b..ff6719a73 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -92,6 +92,8 @@ namespace Barotrauma.Items.Components private readonly Dictionary fabricationLimits = new Dictionary(); + public Action OnItemFabricated; + public Fabricator(Item item, ContentXElement element) : base(item, element) { @@ -128,6 +130,11 @@ namespace Barotrauma.Items.Components } if (recipeInvalid) { continue; } + if (fabricationRecipes.TryGetValue(recipe.RecipeHash, out var duplicateRecipe)) + { + DebugConsole.ThrowError($"Error in the fabrication recipe for \"{itemPrefab.Name}\". Duplicate recipe in \"{duplicateRecipe.TargetItem.Identifier}\"."); + continue; + } fabricationRecipes.Add(recipe.RecipeHash, recipe); if (recipe.FabricationLimitMax >= 0) { @@ -327,7 +334,7 @@ namespace Barotrauma.Items.Components } } - ApplyStatusEffects(ActionType.OnActive, deltaTime, null); + ApplyStatusEffects(ActionType.OnActive, deltaTime); float fabricationSpeedIncrease = 1f + tinkeringStrength * TinkeringSpeedIncrease; @@ -387,37 +394,35 @@ namespace Barotrauma.Items.Components if (GameMain.NetworkMember is null || GameMain.NetworkMember.IsServer) { - List foundAvailableItems = new List(); - foreach (FabricationRecipe.RequiredItem requiredItem in fabricatedItem.RequiredItems) + List chosenIngredients = new List(); + var suitableIngredients = GetSortedSuitableIngredients(); + + foreach (var requiredItem in fabricatedItem.RequiredItems) { - for (int usedPrefabsAmount = 0; usedPrefabsAmount < requiredItem.Amount; usedPrefabsAmount++) + for (int i = 0; i < requiredItem.Amount; i++) { - foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs) + foreach (var suitableIngredient in suitableIngredients) { - if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; } + if (!requiredItem.MatchesItem(suitableIngredient)) { continue; } + if (chosenIngredients.Contains(suitableIngredient)) { continue; } - var availableItems = availableIngredients[requiredPrefab.Identifier]; - var availableItem = availableItems.FirstOrDefault(potentialPrefab => requiredItem.IsConditionSuitable(potentialPrefab.ConditionPercentage)); - - if (availableItem == null) { continue; } - - ingredientsStolen |= availableItem.StolenDuringRound; - if (!availableItem.AllowStealing) + ingredientsStolen |= suitableIngredient.StolenDuringRound; + if (!suitableIngredient.AllowStealing) { ingredientsAllowStealing = false; } //Leave it behind with reduced condition if it has enough to stay above 0 - if (requiredItem.UseCondition && availableItem.ConditionPercentage - requiredItem.MinCondition * 100 > 0.0f) + if (requiredItem.UseCondition && suitableIngredient.ConditionPercentage - requiredItem.MinCondition * 100 > 0.0f) { - availableItem.Condition -= availableItem.Prefab.Health * requiredItem.MinCondition; + suitableIngredient.Condition -= suitableIngredient.Prefab.Health * requiredItem.MinCondition; continue; } - if (availableItem.OwnInventory != null) + if (suitableIngredient.OwnInventory != null) { - foreach (Item containedItem in availableItem.OwnInventory.AllItemsMod) + foreach (Item containedItem in suitableIngredient.OwnInventory.AllItemsMod) { - if (availableItem.GetComponent()?.RemoveContainedItemsOnDeconstruct ?? false) + if (suitableIngredient.GetComponent()?.RemoveContainedItemsOnDeconstruct ?? false) { Entity.Spawner.AddItemToRemoveQueue(containedItem); } @@ -427,15 +432,13 @@ namespace Barotrauma.Items.Components } } } - - foundAvailableItems.Add(availableItem); - availableItems.Remove(availableItem); + chosenIngredients.Add(suitableIngredient); break; } } } - var fabricationIngredients = new AbilityFabricationItemIngredients(foundAvailableItems); + var fabricationIngredients = new AbilityFabricationItemIngredients(chosenIngredients); user?.CheckTalents(AbilityEffectType.OnItemFabricatedIngredients, fabricationIngredients); foreach (Item availableItem in fabricationIngredients.Items) @@ -459,7 +462,7 @@ namespace Barotrauma.Items.Components { character.CheckTalents(AbilityEffectType.OnAllyItemFabricatedAmount, fabricationitemAmount); } - user.CheckTalents(AbilityEffectType.OnItemFabricatedAmount, fabricationitemAmount); + user.CheckTalents(AbilityEffectType.OnItemFabricatedAmount, fabricationitemAmount); quality = GetFabricatedItemQuality(fabricatedItem, user); } @@ -510,7 +513,7 @@ namespace Barotrauma.Items.Components } } - static void onItemSpawned(Item spawnedItem, Character user) + void onItemSpawned(Item spawnedItem, Character user) { if (user != null && user.TeamID != CharacterTeamType.None) { @@ -519,6 +522,7 @@ namespace Barotrauma.Items.Components wifiComponent.TeamID = user.TeamID; } } + OnItemFabricated?.Invoke(spawnedItem, user); } if (user?.Info != null && !user.Removed) { @@ -623,6 +627,11 @@ namespace Barotrauma.Items.Components } } + if (fabricableItem.HideForNonTraitors) + { + if (character is not { IsTraitor: true }) { return false; } + } + if (fabricableItem.RequiredMoney > 0) { switch (GameMain.GameSession?.GameMode) @@ -757,6 +766,19 @@ namespace Barotrauma.Items.Components itemList.AddRange(user.Inventory.AllItems); linkedInventories.Add(user.Inventory); } + foreach (Character c in Character.CharacterList) + { + //take materials from characters who've selected a linked container too + //(e.g. cabinet that's set to display alongside the fabricator UI) + if (c.SelectedItem != null && + c.Inventory != null && + linkedInventories.Contains(c.SelectedItem.OwnInventory) && + !linkedInventories.Contains(c.Inventory)) + { + itemList.AddRange(c.Inventory.AllItems); + linkedInventories.Add(c.Inventory); + } + } availableIngredients.Clear(); foreach (Item item in itemList) { @@ -765,34 +787,51 @@ namespace Barotrauma.Items.Components { availableIngredients[itemIdentifier] = new List(itemList.Count); } - //order by condition (prefer using worst-condition items) - int index = 0; - while (index < availableIngredients[itemIdentifier].Count && - compare(item, availableIngredients[itemIdentifier][index], inputContainer.Inventory) < 0) - { - index++; - } - - static int compare(Item item1, Item item2, Inventory inputInventory) - { - bool item1InInputInventory = item1.ParentInventory == inputInventory; - bool item2InInputInventory = item2.ParentInventory == inputInventory; - //prefer items in the input inventory - if (item1InInputInventory != item2InInputInventory) - { - return item1InInputInventory ? 1 : -1; - } - else - { - float condition1 = MathUtils.IsValid(item1.Condition) ? item1.Condition : 0; - float condition2 = MathUtils.IsValid(item2.Condition) ? item2.Condition : 0; - //prefer items in worse condition - return Math.Sign(condition2 - condition1); - } - } - - availableIngredients[itemIdentifier].Insert(index, item); + availableIngredients[itemIdentifier].Add(item); } + foreach (var itemId in availableIngredients.Keys) + { + availableIngredients[itemId] = SortIngredients(availableIngredients[itemId]).ToList(); + } + } + + private IEnumerable SortIngredients(IEnumerable items) + { + return items + .OrderByDescending(getIngredientContainerPriority) + .ThenBy(it => it.Prefab.DefaultPrice?.Price ?? 0) + .ThenBy(it => MathUtils.IsValid(it.Condition) ? it.Condition : 0) + .ThenByDescending(it => it.ParentInventory?.FindIndex(it) ?? 0); + + int getIngredientContainerPriority(Item item) + { + if (item.ParentInventory == InputContainer.Inventory) + { + return 3; + } + else if (item.ParentInventory is CharacterInventory) + { + return 2; + } + return 1; + } + } + + private IEnumerable GetSortedSuitableIngredients() + { + List suitableIngredients = new List(); + foreach (FabricationRecipe.RequiredItem requiredItem in fabricatedItem.RequiredItems) + { + foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs) + { + if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; } + var availableItems = availableIngredients[requiredPrefab.Identifier]; + suitableIngredients.AddRange( + availableItems.Where(potentialItem => requiredItem.IsConditionSuitable(potentialItem.ConditionPercentage))); + } + } + + return SortIngredients(suitableIngredients); } /// @@ -801,43 +840,34 @@ namespace Barotrauma.Items.Components /// private void MoveIngredientsToInputContainer(FabricationRecipe targetItem) { - //required ingredients that are already present in the input container - List usedItems = new List(); + List chosenIngredients = new List(); + var suitableIngredients = GetSortedSuitableIngredients(); - targetItem.RequiredItems.ForEach(requiredItem => { + foreach (var requiredItem in targetItem.RequiredItems) + { for (int i = 0; i < requiredItem.Amount; i++) { - foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs) + foreach (var suitableIngredient in suitableIngredients) { - if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; } + if (!requiredItem.MatchesItem(suitableIngredient)) { continue; } + if (chosenIngredients.Contains(suitableIngredient)) { continue; } - var availablePrefabs = availableIngredients[requiredPrefab.Identifier]; - var availablePrefab = availablePrefabs.FirstOrDefault(potentialPrefab => + //in another inventory, we need to move the item + if (suitableIngredient.ParentInventory != inputContainer.Inventory) { - return !usedItems.Contains(potentialPrefab) && requiredItem.IsConditionSuitable(potentialPrefab.ConditionPercentage); - }); - if (availablePrefab == null) { continue; } - - availablePrefabs.Remove(availablePrefab); - - if (availablePrefab.ParentInventory == inputContainer.Inventory) - { - //already in input container, all good - usedItems.Add(availablePrefab); - } - else //in another inventory, we need to move the item - { - if (!inputContainer.Inventory.CanBePut(availablePrefab)) + if (!inputContainer.Inventory.CanBePut(suitableIngredient)) { - var unneededItem = inputContainer.Inventory.AllItems.FirstOrDefault(it => !usedItems.Contains(it)); + var unneededItem = inputContainer.Inventory.AllItems.FirstOrDefault(it => !chosenIngredients.Contains(it)); unneededItem?.Drop(null); } - inputContainer.Inventory.TryPutItem(availablePrefab, user: null); + inputContainer.Inventory.TryPutItem(suitableIngredient, user: null); } + chosenIngredients.Add(suitableIngredient); break; } } - }); + } + RefreshAvailableIngredients(); } @@ -884,6 +914,13 @@ namespace Barotrauma.Items.Components } savedFabricatedItem = null; } + + protected override void RemoveComponentSpecific() + { + base.RemoveComponentSpecific(); + OnItemFabricated = null; + } + class AbilityFabricatorSkillGain : AbilityObject, IAbilityValue, IAbilitySkillIdentifier { public AbilityFabricatorSkillGain(Identifier skillIdentifier, float skillAmount) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs index c7d8b37e3..4b3e2e2cb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs @@ -83,7 +83,7 @@ namespace Barotrauma.Items.Components hasPower = Voltage > MinVoltage; if (hasPower) { - ApplyStatusEffects(ActionType.OnActive, deltaTime, null); + ApplyStatusEffects(ActionType.OnActive, deltaTime); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index 0401bda5e..a6c4704cc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -132,7 +132,7 @@ namespace Barotrauma.Items.Components UpdateProjSpecific(deltaTime); - ApplyStatusEffects(ActionType.OnActive, deltaTime, null); + ApplyStatusEffects(ActionType.OnActive, deltaTime); if (item.CurrentHull == null) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs index 1fdff1981..74aec6ffc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs @@ -203,6 +203,8 @@ namespace Barotrauma.Items.Components set; } + public bool MeltedDownThisRound { get; private set; } + public Reactor(Item item, ContentXElement element) : base(item, element) { @@ -282,7 +284,7 @@ namespace Barotrauma.Items.Components } prevAvailableFuel = AvailableFuel; - ApplyStatusEffects(ActionType.OnActive, deltaTime, null); + ApplyStatusEffects(ActionType.OnActive, deltaTime); //use a smoothed "correct output" instead of the actual correct output based on the load //so the player doesn't have to keep adjusting the rate impossibly fast when the load fluctuates heavily @@ -338,7 +340,7 @@ namespace Barotrauma.Items.Components { foreach (Item item in containedItems) { - if (!item.HasTag("reactorfuel")) { continue; } + if (!item.HasTag(Tags.Fuel)) { continue; } if (fissionRate > 0.0f) { bool isConnectedToFriendlyOutpost = Level.IsLoadedOutpost && @@ -648,6 +650,7 @@ namespace Barotrauma.Items.Components item.Condition = 0.0f; fireTimer = 0.0f; meltDownTimer = 0.0f; + MeltedDownThisRound = true; var containedItems = item.OwnInventory?.AllItems; if (containedItems != null) @@ -704,13 +707,13 @@ namespace Barotrauma.Items.Components var containObjective = AIContainItems(container, character, objective, itemCount: 1, equip: true, removeEmpty: true, spawnItemIfNotFound: !character.IsOnPlayerTeam, dropItemOnDeselected: true); containObjective.Completed += ReportFuelRodCount; containObjective.Abandoned += ReportFuelRodCount; - character.Speak(TextManager.Get("DialogReactorFuel").Value, null, 0.0f, "reactorfuel".ToIdentifier(), 30.0f); + character.Speak(TextManager.Get("DialogReactorFuel").Value, null, 0.0f, Tags.Fuel, 30.0f); void ReportFuelRodCount() { if (!character.IsOnPlayerTeam) { return; } if (character.Submarine != Submarine.MainSub) { return; } - int remainingFuelRods = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("reactorfuel") && i.Condition > 1); + int remainingFuelRods = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(Tags.Fuel) && i.Condition > 1); if (remainingFuelRods == 0) { character.Speak(TextManager.Get("DialogOutOfFuelRods").Value, null, 0.0f, "outoffuelrods".ToIdentifier(), 30.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index daa50559e..f2c4b506b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -300,7 +300,7 @@ namespace Barotrauma.Items.Components user = null; } - ApplyStatusEffects(ActionType.OnActive, deltaTime, null); + ApplyStatusEffects(ActionType.OnActive, deltaTime); float userSkill = 0.0f; if (user != null && controlledSub != null && @@ -508,7 +508,7 @@ namespace Barotrauma.Items.Components var closeCells = Level.Loaded.GetCells(controlledSub.WorldPosition, 4); foreach (VoronoiCell cell in closeCells) { - if (cell.DoesDamage) + if (cell.DoesDamage || cell.Body is { BodyType: BodyType.Dynamic }) { foreach (GraphEdge edge in cell.Edges) { @@ -525,7 +525,7 @@ namespace Barotrauma.Items.Components newAvoidStrength += avoid; debugDrawObstacles.Add(new ObstacleDebugInfo(edge, edge.Center, 1.0f, avoid, cell.Translation)); - if (dot > 0.0f) + if (dot > 0.0f && cell.DoesDamage) { showIceSpireWarning = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs index 45d2af075..8881ce000 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs @@ -138,6 +138,8 @@ namespace Barotrauma.Items.Components set { flipIndicator = value; } } + public bool OutputDisabled { get; private set; } + public float RechargeRatio => RechargeSpeed / MaxRechargeSpeed; public const float aiRechargeTargetRatio = 0.5f; @@ -162,19 +164,19 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - adjustedCapacity = GetCapacity(); if (item.Connections == null) { IsActive = false; return; } + adjustedCapacity = GetCapacity(); isRunning = true; float chargeRatio = charge / adjustedCapacity; if (chargeRatio > 0.0f) { - ApplyStatusEffects(ActionType.OnActive, deltaTime, null); + ApplyStatusEffects(ActionType.OnActive, deltaTime); } float loadReading = 0; @@ -242,6 +244,7 @@ namespace Barotrauma.Items.Components /// Minimum and maximum power output for the connection public override PowerRange MinMaxPowerOut(Connection connection, float load = 0) { + if (OutputDisabled) { return PowerRange.Zero; } if (connection == powerOut) { float maxOutput; @@ -275,6 +278,7 @@ namespace Barotrauma.Items.Components /// public override float GetConnectionPowerOut(Connection connection, float power, PowerRange minMaxPower, float load) { + if (OutputDisabled) { return 0; } //Only power out connection can provide power and Max poweroutput can't be negative if (connection == powerOut && minMaxPower.Max > 0) { @@ -367,22 +371,27 @@ namespace Barotrauma.Items.Components public override void ReceiveSignal(Signal signal, Connection connection) { if (connection.IsPower) { return; } - - if (connection.Name == "set_rate") + switch (connection.Name) { - if (float.TryParse(signal.value, NumberStyles.Any, CultureInfo.InvariantCulture, out float tempSpeed)) - { - if (!MathUtils.IsValid(tempSpeed)) { return; } - - float rechargeRate = MathHelper.Clamp(tempSpeed / 100.0f, 0.0f, 1.0f); - RechargeSpeed = rechargeRate * MaxRechargeSpeed; -#if CLIENT - if (rechargeSpeedSlider != null) + case "disable_output": + OutputDisabled = signal.value != "0"; + break; + case "set_rate": + if (float.TryParse(signal.value, NumberStyles.Any, CultureInfo.InvariantCulture, out float tempSpeed)) { - rechargeSpeedSlider.BarScroll = rechargeRate; - } + if (!MathUtils.IsValid(tempSpeed)) { return; } + + float rechargeRate = MathHelper.Clamp(tempSpeed / 100.0f, 0.0f, 1.0f); + RechargeSpeed = rechargeRate * MaxRechargeSpeed; +#if CLIENT + if (rechargeSpeedSlider != null) + { + rechargeSpeedSlider.BarScroll = rechargeRate; + } #endif - } + } + break; + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs index c1c82e68f..f023ff870 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs @@ -197,7 +197,7 @@ namespace Barotrauma.Items.Components isBroken = false; } - ApplyStatusEffects(ActionType.OnActive, deltaTime, null); + ApplyStatusEffects(ActionType.OnActive, deltaTime); float powerReadingOut = 0; float loadReadingOut = ExtraLoad; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs index 88db0df19..8eda11f8e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs @@ -1,6 +1,7 @@ using System; using Microsoft.Xna.Framework; using System.Collections.Generic; +using System.Linq; #if CLIENT using Barotrauma.Sounds; #endif @@ -193,13 +194,13 @@ namespace Barotrauma.Items.Components { //if the item consumes no power, ignore the voltage requirement and //apply OnActive statuseffects as long as this component is active - ApplyStatusEffects(ActionType.OnActive, deltaTime, null); + ApplyStatusEffects(ActionType.OnActive, deltaTime); return; } if (Voltage > minVoltage) { - ApplyStatusEffects(ActionType.OnActive, deltaTime, null); + ApplyStatusEffects(ActionType.OnActive, deltaTime); } #if CLIENT if (Voltage > minVoltage) @@ -666,7 +667,7 @@ namespace Barotrauma.Items.Components return conn1.IsPower && conn2.IsPower && conn1.Item.Condition > 0.0f && conn2.Item.Condition > 0.0f && - (conn1.Item.HasTag("junctionbox") || conn2.Item.HasTag("junctionbox") || conn1.Item.HasTag("dock") || conn2.Item.HasTag("dock") || conn1.IsOutput != conn2.IsOutput); + (conn1.Item.HasTag(Tags.JunctionBox) || conn2.Item.HasTag(Tags.JunctionBox) || conn1.Item.HasTag(Tags.DockingPort) || conn2.Item.HasTag(Tags.DockingPort) || conn1.IsOutput != conn2.IsOutput); } /// @@ -682,6 +683,7 @@ namespace Barotrauma.Items.Components if (!recipient.IsPower || !recipient.IsOutput) { continue; } var battery = recipient.Item?.GetComponent(); if (battery == null || battery.Item.Condition <= 0.0f) { continue; } + if (battery.OutputDisabled) { continue; } float maxOutputPerFrame = battery.MaxOutPut / 60.0f; float framesPerMinute = 3600.0f; availablePower += Math.Min(battery.Charge * framesPerMinute, maxOutputPerFrame); @@ -689,23 +691,19 @@ namespace Barotrauma.Items.Components return availablePower; } - /// - /// Returns a list of batteries directly connected to the item - /// - protected List GetDirectlyConnectedBatteries() + protected IEnumerable GetDirectlyConnectedBatteries() { - List batteries = new List(); - if (item.Connections == null || powerIn == null) { return batteries; } - foreach (Connection recipient in powerIn.Recipients) + if (item.Connections != null && powerIn != null) { - if (!recipient.IsPower || !recipient.IsOutput) { continue; } - var battery = recipient.Item?.GetComponent(); - if (battery != null) + foreach (Connection recipient in powerIn.Recipients) { - batteries.Add(battery); + if (!recipient.IsPower || !recipient.IsOutput) { continue; } + if (recipient.Item?.GetComponent() is PowerContainer battery) + { + yield return battery; + } } } - return batteries; } protected override void RemoveComponentSpecific() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index 1ad742343..b9146e660 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -56,6 +56,16 @@ namespace Barotrauma.Items.Components } } + enum StickTargetType + { + Structure, + Limb, + Item, + Submarine, + LevelWall, + Unknown + } + public const float WaterDragCoefficient = 0.1f; private readonly Queue impactQueue = new Queue(); @@ -353,7 +363,7 @@ namespace Barotrauma.Items.Components if (Item.Removed) { return; } launchPos = simPosition; //set the rotation of the projectile again because dropping the projectile resets the rotation - Item.SetTransform(simPosition, rotation + (Item.body.Dir * LaunchRotationRadians)); + Item.SetTransform(simPosition, rotation + (Item.body.Dir * LaunchRotationRadians), findNewHull: false); if (DeactivationTime > 0) { deactivationTimer = DeactivationTime; @@ -442,27 +452,49 @@ namespace Barotrauma.Items.Components { hits.Clear(); + Vector2 prevVelocity = item.body.LinearVelocity; + if (item.AiTarget != null) { item.AiTarget.SightRange = item.AiTarget.MaxSightRange; item.AiTarget.SoundRange = item.AiTarget.MaxSoundRange; } + //do not create a network event about dropping the projectile at this point, + //otherwise clients would fail to launch the correct projectile when firing the weapon + var prevInventory = item.ParentInventory; + if (prevInventory != null && GameMain.NetworkMember is { IsServer: true }) + { + //update the state of the inventory after a delay, + //in case a client failed to launch the projectile for some reason + CoroutineManager.Invoke(() => + { + if (item.Removed) { return; } + prevInventory.CreateNetworkEvent(); + }, delay: CorrectionDelay / 2); + } item.Drop(null, createNetworkEvent: false); + Item.WaterDragCoefficient = WaterDragCoefficient; launchPos = item.SimPosition; - item.body.Enabled = true; + item.body.Enabled = true; if (item.body.BodyType == BodyType.Kinematic) { item.body.LinearVelocity = impulse; } - else + else if (impulse.LengthSquared() > 0.001f) { impulse *= item.body.Mass; item.body.ApplyLinearImpulse(impulse, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.95f); } + else + { + //if no impulse is defined, maintain the projectile's original velocity (if any) + //can be used to make throwable items behave as projectiles + item.body.LinearVelocity = prevVelocity; + } item.body.FarseerBody.OnCollision += OnProjectileCollision; item.body.FarseerBody.IsBullet = true; @@ -483,7 +515,7 @@ namespace Barotrauma.Items.Components float rotation = item.body.Rotation; Vector2 simPositon = item.SimPosition; Vector2 rayStartWorld = item.WorldPosition; - item.Drop(null); + item.Drop(null, createNetworkEvent: false); Item.WaterDragCoefficient = WaterDragCoefficient; item.body.Enabled = true; @@ -666,6 +698,7 @@ namespace Barotrauma.Items.Components if (fixture.Body.UserData is VoronoiCell) { return -1; } if (fixture.Body.UserData is Entity entity && entity.Submarine != submarine) { return -1; } if (fixture.Body.UserData is Limb limb && limb.character?.Submarine != submarine) { return -1; } + if (fixture.Body == Level.Loaded?.TopBarrier || fixture.Body == Level.Loaded?.BottomBarrier) { return -1; } } // Ignore holdables that can't push -> shouldn't block @@ -891,8 +924,17 @@ namespace Barotrauma.Items.Components return false; } - Vector2 dir = item.body.LinearVelocity.LengthSquared() < 0.001f ? - contact.Manifold.LocalNormal : Vector2.Normalize(item.body.LinearVelocity); + Vector2 normalizedVel; + Vector2 dir; + if (item.body.LinearVelocity.LengthSquared() < 0.001f) + { + normalizedVel = Vector2.Zero; + dir = contact.Manifold.LocalNormal; + } + else + { + normalizedVel = dir = Vector2.Normalize(item.body.LinearVelocity); + } //do a raycast in the sub's coordinate space to see if it hit a structure var wallBody = Submarine.PickBody( @@ -901,7 +943,7 @@ namespace Barotrauma.Items.Components collisionCategory: Physics.CollisionWall); if (wallBody?.FixtureList?.First() != null && (wallBody.UserData is Structure || wallBody.UserData is Item) && //ignore the hit if it's behind the position the item was launched from, and the projectile is travelling in the opposite direction - Vector2.Dot(item.body.SimPosition - launchPos, dir) > 0) + Vector2.Dot((item.body.SimPosition + normalizedVel) - launchPos, dir) > 0) { target = wallBody.FixtureList.First(); if (hits.Contains(target.Body)) @@ -1136,7 +1178,13 @@ namespace Barotrauma.Items.Components removePending = true; item.HiddenInGame = true; item.body.FarseerBody.Enabled = false; - Entity.Spawner?.AddItemToRemoveQueue(item); + //delete with a brief delay so we don't delete the item + //before a client has managed to launch it (can happen with hitscan projectile) + CoroutineManager.Invoke(() => + { + if (item.Removed) { return; } + Entity.Spawner?.AddItemToRemoveQueue(item); + }, delay: 0.5f); } return true; @@ -1181,6 +1229,11 @@ namespace Barotrauma.Items.Components IgnoredBodies?.Clear(); } + public bool IsAttachedTo(PhysicsBody body) + { + return stickJoint != null && (stickJoint.BodyA == body?.FarseerBody || stickJoint.BodyB == body?.FarseerBody); + } + private void StickToTarget(Body targetBody, Vector2 axis) { if (stickJoint != null) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/RemoteController.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/RemoteController.cs index d457cb589..5e330bd94 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/RemoteController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/RemoteController.cs @@ -1,12 +1,11 @@ using Microsoft.Xna.Framework; -using System.Xml.Linq; namespace Barotrauma.Items.Components { partial class RemoteController : ItemComponent { [Serialize("", IsPropertySaveable.No, description: "Tag or identifier of the item that should be controlled.")] - public string Target + public Identifier Target { get; private set; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index 66e6e93e0..93721c36f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -14,7 +14,11 @@ namespace Barotrauma.Items.Components private readonly LocalizedString header; private float deteriorationTimer; - private float deteriorateAlwaysResetTimer; + public float ForceDeteriorationTimer + { + get; + private set; + } private int updateDeteriorationCounter; private const int UpdateDeteriorationInterval = 10; @@ -62,6 +66,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(60f, IsPropertySaveable.Yes, description: "How long will the item spontaneously deteriorate after being sabotaged.")] + public float SabotageDeteriorationDuration + { + get; + set; + } + [Serialize(80.0f, IsPropertySaveable.Yes, description: "The condition of the item has to be below this for it to become repairable. Percentages of max condition."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] public float RepairThreshold { @@ -83,13 +94,6 @@ namespace Barotrauma.Items.Components set; } - [Serialize(false, IsPropertySaveable.No, description: "If set to true, the deterioration timer will always run regardless if the item is being used or not.")] - public bool DeteriorateAlways - { - get; - set; - } - private float skillRequirementMultiplier; [Serialize(1.0f, IsPropertySaveable.Yes)] @@ -394,20 +398,19 @@ namespace Barotrauma.Items.Components item.SendSignal(conditionSignal, "condition_out"); + if (ForceDeteriorationTimer > 0.0f) + { + ForceDeteriorationTimer -= deltaTime; + if (ForceDeteriorationTimer <= 0.0f) + { +#if SERVER + //let the clients know the deterioration delay + item.CreateServerEvent(this); +#endif + } + } if (CurrentFixer == null) { - if (deteriorateAlwaysResetTimer > 0.0f) - { - deteriorateAlwaysResetTimer -= deltaTime; - if (deteriorateAlwaysResetTimer <= 0.0f) - { - DeteriorateAlways = false; -#if SERVER - //let the clients know the deterioration delay - item.CreateServerEvent(this); -#endif - } - } updateDeteriorationCounter++; if (updateDeteriorationCounter >= UpdateDeteriorationInterval) { @@ -534,8 +537,7 @@ namespace Barotrauma.Items.Components } deteriorationTimer = 0.0f; - deteriorateAlwaysResetTimer = item.Condition / DeteriorationSpeed; - DeteriorateAlways = true; + ForceDeteriorationTimer = SabotageDeteriorationDuration; item.Condition = item.MaxCondition * (MinSabotageCondition / 100); wasGoodCondition = false; } @@ -553,7 +555,8 @@ namespace Barotrauma.Items.Components if (item.Condition <= 0.0f) { return; } if (!ShouldDeteriorate()) { return; } - if (deteriorationTimer > 0.0f) + //forced deterioration doesn't tick down the timer for spontaneous deterioration + if (deteriorationTimer > 0.0f && ForceDeteriorationTimer <= 0.0f) { if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) { @@ -568,6 +571,7 @@ namespace Barotrauma.Items.Components if (item.ConditionPercentage > MinDeteriorationCondition) { float deteriorationSpeed = item.StatManager.GetAdjustedValue(ItemTalentStats.DetoriationSpeed, DeteriorationSpeed); + if (ForceDeteriorationTimer > 0.0f) { deteriorationSpeed = Math.Max(deteriorationSpeed, 1.0f); } item.Condition -= deteriorationSpeed * deltaTime; } } @@ -592,7 +596,7 @@ namespace Barotrauma.Items.Components if (!character.HasAbilityFlag(AbilityFlags.CanTinker)) { return false; } if (item.GetComponent() != null) { return true; } if (item.GetComponent() != null) { return true; } - if (item.HasTag("turretammosource")) { return true; } + if (item.HasTag(Tags.TurretAmmoSource)) { return true; } if (!character.HasAbilityFlag(AbilityFlags.CanTinkerFabricatorsAndDeconstructors)) { return false; } if (item.GetComponent() != null) { return true; } if (item.GetComponent() != null) { return true; } @@ -623,6 +627,8 @@ namespace Barotrauma.Items.Components private bool ShouldDeteriorate() { + if (ForceDeteriorationTimer > 0.0f) { return true; } + if (Level.IsLoadedFriendlyOutpost) { return false; } #if CLIENT if (GameMain.GameSession?.GameMode is TutorialMode) { return false; } @@ -666,13 +672,13 @@ namespace Barotrauma.Items.Components //oxygen generators don't deteriorate if they're not running if (oxyGenerator.CurrFlow > 0.1f) { return true; } } - else if (ic is Powered powered && !(powered is LightComponent)) + else if (ic is Powered powered && powered is not LightComponent) { if (powered.Voltage >= powered.MinVoltage) { return true; } } } - return DeteriorateAlways; + return false; } private float GetDeteriorationDelayMultiplier() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs index 64cc2add8..fd7fe8aca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs @@ -181,7 +181,7 @@ namespace Barotrauma.Items.Components return; } - Vector2 diff = target.WorldPosition - source.WorldPosition; + Vector2 diff = target.WorldPosition - GetSourcePos(useDrawPosition: false); float lengthSqr = diff.LengthSquared(); if (lengthSqr > MaxLength * MaxLength) { @@ -372,7 +372,40 @@ namespace Barotrauma.Items.Components } } - private PhysicsBody GetBodyToPull(ISpatialEntity target) + /// + /// Get the position the rope starts from (taking into account barrel positions if needed) + /// + /// Should the interpolated draw position be used? If not, the WorldPosition is used. + private Vector2 GetSourcePos(bool useDrawPosition = false) + { + Vector2 sourcePos = source.WorldPosition; + if (source is Item sourceItem) + { + if (useDrawPosition) + { + sourcePos = sourceItem.DrawPosition; + } + if (!sourceItem.Removed) + { + if (sourceItem.GetComponent() is { } turret) + { + sourcePos = new Vector2(sourceItem.WorldRect.X + turret.TransformedBarrelPos.X, sourceItem.WorldRect.Y - turret.TransformedBarrelPos.Y); + } + else if (sourceItem.GetComponent() is { } weapon) + { + sourcePos += ConvertUnits.ToDisplayUnits(weapon.TransformedBarrelPos); + } + } + } + else if (useDrawPosition && source is Limb sourceLimb && sourceLimb.body != null) + { + sourcePos = sourceLimb.body.DrawPosition; + } + return sourcePos; + } + + + private static PhysicsBody GetBodyToPull(ISpatialEntity target) { if (target is Item targetItem) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/BooleanOperatorComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/BooleanOperatorComponent.cs index 35b4a9d10..9f19704a7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/BooleanOperatorComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/BooleanOperatorComponent.cs @@ -57,7 +57,7 @@ namespace Barotrauma.Items.Components } } - [InGameEditable, Serialize("", IsPropertySaveable.Yes, description: "The signal sent when the condition is met (if empty, no signal is sent).", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("0", IsPropertySaveable.Yes, description: "The signal sent when the condition is met (if empty, no signal is sent).", alwaysUseInstanceValues: true)] public string FalseOutput { get { return falseOutput; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs new file mode 100644 index 000000000..f02fbebae --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs @@ -0,0 +1,684 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Xml.Linq; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; + +namespace Barotrauma.Items.Components +{ + internal sealed partial class CircuitBox : ItemComponent, IClientSerializable, IServerSerializable + { + public static readonly ImmutableHashSet UnrealiableOpcodes + = ImmutableHashSet.Create(CircuitBoxOpcode.Cursor); + + public ImmutableArray Inputs; + public ImmutableArray Outputs; + + public readonly List Components = new List(); + + public readonly List InputOutputNodes = new(); + + public readonly List Wires = new List(); + + public override bool IsActive => true; + + public Option FindInputOutputConnection(Identifier connectionName) + { + foreach (CircuitBoxInputConnection input in Inputs) + { + if (input.Name != connectionName) { continue; } + + return Option.Some(input); + } + + foreach (CircuitBoxOutputConnection output in Outputs) + { + if (output.Name != connectionName) { continue; } + + return Option.Some(output); + } + + return Option.None; + } + + public readonly ItemContainer[] containers; + + private const int ComponentContainerIndex = 0, + WireContainerIndex = 1; + + public ItemContainer? ComponentContainer + => GetContainerOrNull(ComponentContainerIndex); + + // wire container falls back to the main container if one isn't specified + public ItemContainer? WireContainer + => GetContainerOrNull(WireContainerIndex) ?? GetContainerOrNull(ComponentContainerIndex); + + public bool IsFull => ComponentContainer?.Inventory is { } inventory && inventory.IsFull(true); + + public CircuitBox(Item item, ContentXElement element) : base(item, element) + { + containers = item.GetComponents().ToArray(); + if (containers.Length < 1) + { + DebugConsole.ThrowError("Circuit box must have at least one item container to function."); + } + + InitProjSpecific(element); + + var inputBuilder = ImmutableArray.CreateBuilder(); + var outputBuilder = ImmutableArray.CreateBuilder(); + + foreach (Connection conn in Item.Connections) + { + if (conn.IsOutput) + { + outputBuilder.Add(new CircuitBoxOutputConnection(Vector2.Zero, conn, this)); + } + else + { + inputBuilder.Add(new CircuitBoxInputConnection(Vector2.Zero, conn, this)); + } + } + + Inputs = inputBuilder.ToImmutable(); + Outputs = outputBuilder.ToImmutable(); + + InputOutputNodes.Add(new CircuitBoxInputOutputNode(Inputs, new Vector2(-512, 0f), CircuitBoxInputOutputNode.Type.Input, this)); + InputOutputNodes.Add(new CircuitBoxInputOutputNode(Outputs, new Vector2(512, 0f), CircuitBoxInputOutputNode.Type.Output, this)); + + item.OnDeselect += OnDeselected; + } + + /// + /// We want to load the components after the map has loaded since we need to link up the components to their items + /// and pretty much all items have higher ID than the circuit box. + /// + private Option delayedElementToLoad; + + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) + { + base.Load(componentElement, usePrefabValues, idRemap); + if (delayedElementToLoad.IsSome()) { return; } + delayedElementToLoad = Option.Some(componentElement); + } + + public override void OnInventoryChanged() + => OnViewUpdateProjSpecific(); + + public override void Update(float deltaTime, Camera cam) + { +#if CLIENT + // When loading from the server the wires cannot be properly loaded and connected up because we might not be loaded in properly yet. + // So we need to wait until the circuit box starts updating and then we can ensure the wires are connected. + if (wasInitializedByServer) + { + foreach (var w in Wires) + { + w.EnsureWireConnected(); + } + wasInitializedByServer = false; + } +#endif + TryInitializeNodes(); + } + + public override void OnMapLoaded() + => TryInitializeNodes(); + + private void TryInitializeNodes() + { + if (!delayedElementToLoad.TryUnwrap(out var loadElement)) { return; } + LoadFromXML(loadElement); + delayedElementToLoad = Option.None; + } + + private void LoadFromXML(ContentXElement loadElement) + { + foreach (var subElement in loadElement.Elements()) + { + string elementName = subElement.Name.ToString().ToLowerInvariant(); + switch (elementName) + { + case "component" when CircuitBoxComponent.TryLoadFromXML(subElement, this).TryUnwrap(out var comp): + Components.Add(comp); + break; + case "wire" when CircuitBoxWire.TryLoadFromXML(subElement, this).TryUnwrap(out var wire): + Wires.Add(wire); + break; + case "inputnode": + LoadFor(CircuitBoxInputOutputNode.Type.Input, subElement); + break; + case "outputnode": + LoadFor(CircuitBoxInputOutputNode.Type.Output, subElement); + break; + } + } + +#if SERVER + // We need to let the clients know of the loaded data + if (needsServerInitialization) + { + CreateInitializationEvent(); + needsServerInitialization = false; + } +#endif + + void LoadFor(CircuitBoxInputOutputNode.Type type, ContentXElement subElement) + { + foreach (var node in InputOutputNodes) + { + if (node.NodeType != type) { continue; } + + node.Load(subElement); + break; + } + } + } + + public void CloneFrom(CircuitBox original, Dictionary clonedContainedItems) + { + Components.Clear(); + Wires.Clear(); + + foreach (var origComp in original.Components) + { + var newComponent = new CircuitBoxComponent(origComp.ID, clonedContainedItems[origComp.Item.ID], origComp.Position, this, origComp.UsedResource); + Components.Add(newComponent); + } + + for (int ioIndex = 0; ioIndex < original.InputOutputNodes.Count; ioIndex++) + { + var origNode = original.InputOutputNodes[ioIndex]; + var cloneNode = InputOutputNodes[ioIndex]; + + cloneNode.Position = origNode.Position; + } + + foreach (var origWire in original.Wires) + { + Option to = CircuitBoxConnectorIdentifier.FromConnection(origWire.To).FindConnection(this), + from = CircuitBoxConnectorIdentifier.FromConnection(origWire.From).FindConnection(this); + + if (!to.TryUnwrap(out var toConn) || !from.TryUnwrap(out var fromConn)) + { + DebugConsole.ThrowError($"Error while cloning item \"{Name}\" - failed to find a connection for a wire. "); + continue; + } + + var newWire = new CircuitBoxWire(this, origWire.ID, origWire.BackingWire.Select(w => clonedContainedItems[w.ID]), fromConn, toConn, origWire.UsedItemPrefab); + Wires.Add(newWire); + } + } + + public override XElement Save(XElement parentElement) + { + XElement componentElement = base.Save(parentElement); + + foreach (CircuitBoxInputOutputNode node in InputOutputNodes) + { + componentElement.Add(node.Save()); + } + + foreach (CircuitBoxComponent node in Components) + { + componentElement.Add(node.Save()); + } + + foreach (CircuitBoxWire wire in Wires) + { + componentElement.Add(wire.Save()); + } + + return componentElement; + } + + public partial void OnDeselected(Character c); + + public record struct CreatedWire(CircuitBoxConnectorIdentifier Start, CircuitBoxConnectorIdentifier End, Option Item, ushort ID); + + public bool Connect(CircuitBoxConnection one, CircuitBoxConnection two, Action onCreated, ItemPrefab selectedWirePrefab) + { + if (!VerifyConnection(one, two)) { return false; } + + ushort id = ICircuitBoxIdentifiable.FindFreeID(Wires); + switch (one.IsOutput) + { + case true when !two.IsOutput: + { + CircuitBoxConnectorIdentifier start = CircuitBoxConnectorIdentifier.FromConnection(one), + end = CircuitBoxConnectorIdentifier.FromConnection(two); + + if (IsExternalConnection(one) || IsExternalConnection(two)) + { + CreateWireWithoutItem(one, two, id, selectedWirePrefab); + onCreated(new CreatedWire(start, end, Option.None, id)); + return true; + } + + CreateWireWithItem(one, two, selectedWirePrefab, id, i => onCreated(new CreatedWire(start, end, Option.Some(i), id))); + return true; + } + case false when two.IsOutput: + { + CircuitBoxConnectorIdentifier start = CircuitBoxConnectorIdentifier.FromConnection(two), + end = CircuitBoxConnectorIdentifier.FromConnection(one); + if (IsExternalConnection(one) || IsExternalConnection(two)) + { + CreateWireWithoutItem(two, one, id, selectedWirePrefab); + onCreated(new CreatedWire(start, end, Option.None, id)); + return true; + } + + CreateWireWithItem(two, one, selectedWirePrefab, id, i => onCreated(new CreatedWire(start, end, Option.Some(i), id))); + return true; + } + } + + return false; + } + + private static bool VerifyConnection(CircuitBoxConnection one, CircuitBoxConnection two) + { + if (one.IsOutput == two.IsOutput || one == two) { return false; } + + if (one is CircuitBoxNodeConnection oneNodeConnection && + two is CircuitBoxNodeConnection twoNodeConnection) + { + if (oneNodeConnection.Component == twoNodeConnection.Component) + { + return false; + } + } + + if (one is CircuitBoxNodeConnection { HasAvailableSlots: false } || + two is CircuitBoxNodeConnection { HasAvailableSlots: false }) + { + return one is not CircuitBoxNodeConnection || two is not CircuitBoxNodeConnection; + } + + return true; + } + + private static bool IsExternalConnection(CircuitBoxConnection conn) => conn is (CircuitBoxInputConnection or CircuitBoxOutputConnection); + + private void CreateWireWithoutItem(CircuitBoxConnection one, CircuitBoxConnection two, ushort id, ItemPrefab prefab) + { + bool hasExternalConnection = false; + if (one is CircuitBoxInputConnection input) + { + hasExternalConnection = true; + input.ExternallyConnectedTo.Add(two); + } + + if (two is CircuitBoxOutputConnection output) + { + hasExternalConnection = true; + one.Connection.CircuitBoxConnections.Add(output); + } + + if (hasExternalConnection) + { + two.ExternallyConnectedFrom.Add(one); + } + + AddWireDirect(id, prefab, Option.None, one, two); + } + + private void CreateWireWithItem(CircuitBoxConnection one, CircuitBoxConnection two, ItemPrefab prefab, ushort wireId, Action onItemSpawned) + { + if (WireContainer is null) { return; } + + if (IsExternalConnection(one) || IsExternalConnection(two)) + { + DebugConsole.ThrowError("Cannot add a wire between an external connection and a component connection."); + return; + } + + SpawnItem(this, prefab, WireContainer, wire => + { + AddWireDirect(wireId, prefab, Option.Some(wire), one, two); + onItemSpawned(wire); + }); + } + + private void CreateWireWithItem(CircuitBoxConnection one, CircuitBoxConnection two, ushort wireId, Item it) + { + if (IsExternalConnection(one) || IsExternalConnection(two)) + { + DebugConsole.ThrowError("Cannot add a wire between an external connection and a component connection."); + return; + } + + AddWireDirect(wireId, it.Prefab, Option.Some(it), one, two); + } + + private void AddWireDirect(ushort id, ItemPrefab prefab, Option backingItem, CircuitBoxConnection one, CircuitBoxConnection two) + => Wires.Add(new CircuitBoxWire(this, id, backingItem, one, two, prefab)); + + private bool AddComponentInternal(ushort id, ItemPrefab prefab, ItemPrefab usedResource, Vector2 pos, Action onItemSpawned) + { + if (id is ICircuitBoxIdentifiable.NullComponentID) + { + DebugConsole.ThrowError("Unable to add component because there are no free IDs."); + return false; + } + + if (ComponentContainer?.Inventory is { } inventory && inventory.HowManyCanBePut(prefab) <= 0) + { + DebugConsole.ThrowError("Unable to add component because there is no space in the inventory."); + return false; + } + + SpawnItem(this, prefab, ComponentContainer, spawnedItem => + { + Components.Add(new CircuitBoxComponent(id, spawnedItem, pos, this, usedResource)); + onItemSpawned(spawnedItem); + }); + + OnViewUpdateProjSpecific(); + return true; + } + + // Unsafe because it doesn't perform error checking since it's data we get from the server + private void AddComponentInternalUnsafe(ushort id, Item backingItem, ItemPrefab usedResource, Vector2 pos) + { + Components.Add(new CircuitBoxComponent(id, backingItem, pos, this, usedResource)); + OnViewUpdateProjSpecific(); + } + + private static void ClearSelectionFor(ushort characterId, IReadOnlyCollection nodes) + { + foreach (var node in nodes) + { + if (node.SelectedBy != characterId) { continue; } + + node.SetSelected(Option.None); + } + } + + private void ClearAllSelectionsInternal(ushort characterId) + { + ClearSelectionFor(characterId, Components); + ClearSelectionFor(characterId, InputOutputNodes); + ClearSelectionFor(characterId, Wires); + } + + private void SelectComponentsInternal(IReadOnlyCollection ids, ushort characterId, bool overwrite) + { + if (overwrite) { ClearSelectionFor(characterId, Components); } + + if (!ids.Any()) { return; } + + foreach (CircuitBoxComponent node in Components) + { + if (!ids.Contains(node.ID)) { continue; } + + node.SetSelected(Option.Some(characterId)); + } + } + + private void UpdateSelections(ImmutableDictionary> nodeIds, + ImmutableDictionary> wireIds, + ImmutableDictionary> inputOutputs) + { + foreach (var wire in Wires) + { + if (!wireIds.TryGetValue(wire.ID, out var selectedBy)) { continue; } + + if (selectedBy.TryUnwrap(out var id)) + { + wire.IsSelected = true; + wire.SelectedBy = id; + continue; + } + + wire.IsSelected = false; + wire.SelectedBy = 0; + } + + foreach (var node in Components) + { + if (!nodeIds.TryGetValue(node.ID, out var selectedBy)) { continue; } + + node.SetSelected(selectedBy); + } + + foreach (var node in InputOutputNodes) + { + if (!inputOutputs.TryGetValue(node.NodeType, out var selectedBy)) { continue; } + + node.SetSelected(selectedBy); + } + } + + private void SelectWiresInternal(IReadOnlyCollection ids, ushort characterId, bool overwrite) + { + if (overwrite) { ClearSelectionFor(characterId, Wires); } + + foreach (CircuitBoxWire wire in Wires) + { + if (!ids.Contains(wire.ID)) { continue; } + + wire.SetSelected(Option.Some(characterId)); + } + } + + private void SelectInputOutputInternal(IReadOnlyCollection io, ushort characterId, bool overwrite) + { + if (overwrite) { ClearSelectionFor(characterId, InputOutputNodes); } + + foreach (var node in InputOutputNodes) + { + if (!io.Contains(node.NodeType)) { continue; } + + node.SetSelected(Option.Some(characterId)); + } + } + + private void RemoveComponentInternal(IReadOnlyCollection ids) + { + foreach (CircuitBoxComponent node in Components.ToImmutableArray()) + { + if (!ids.Contains(node.ID)) { continue; } + + Components.Remove(node); + node.Remove(); + + foreach (CircuitBoxWire wire in Wires.ToImmutableArray()) + { + if (node.Connectors.Contains(wire.From) || node.Connectors.Contains(wire.To)) + { + RemoveWireCollectionUnsafe(wire); + } + } + } + OnViewUpdateProjSpecific(); + } + + private void RemoveWireInternal(IReadOnlyCollection ids) + { + foreach (CircuitBoxWire wire in Wires.ToImmutableArray()) + { + if (!ids.Contains(wire.ID)) { continue; } + + RemoveWireCollectionUnsafe(wire); + } + + OnViewUpdateProjSpecific(); + } + + private void RemoveWireCollectionUnsafe(CircuitBoxWire wire) + { + foreach (CircuitBoxOutputConnection output in Outputs) + { + output.Connection.CircuitBoxConnections.Remove(wire.From); + } + + wire.From.Connection.CircuitBoxConnections.Remove(wire.To); + + if (wire.From is CircuitBoxInputConnection input) + { + input.ExternallyConnectedTo.Remove(wire.To); + } + + wire.To.ExternallyConnectedFrom.Remove(wire.From); + wire.From.ExternallyConnectedFrom.Remove(wire.To); + + wire.Remove(); + Wires.Remove(wire); + } + + private void MoveNodesInternal(IReadOnlyCollection ids, + IReadOnlyCollection ios, + Vector2 moveAmount) + { + IEnumerable nodes = Components.Where(node => ids.Contains(node.ID)); + foreach (CircuitBoxComponent node in nodes) + { + node.Position += moveAmount; + } + + + foreach (var io in InputOutputNodes) + { + if (!ios.Contains(io.NodeType)) { continue; } + io.Position += moveAmount; + } + + OnViewUpdateProjSpecific(); + } + + public override bool Select(Character character) + => item.GetComponent() is not { Attached: false } && base.Select(character); + + public partial void OnViewUpdateProjSpecific(); + + partial void InitProjSpecific(ContentXElement element); + + public override void ReceiveSignal(Signal signal, Connection connection) + { + foreach (var input in Inputs) + { + if (input.Connection != connection) { continue; } + + input.ReceiveSignal(signal); + break; + } + } + + public static bool IsRoundRunning() + => !Submarine.Unloading && GameMain.GameSession is { IsRunning: true }; + + public static Option FindCircuitBox(ushort itemId, byte componentIndex) + { + if (!IsRoundRunning() || Entity.FindEntityByID(itemId) is not Item item) { return Option.None; } + + if (componentIndex >= item.Components.Count) + { + return Option.None; + } + + ItemComponent targetComponent = item.Components[componentIndex]; + if (targetComponent is CircuitBox circuitBox) + { + return Option.Some(circuitBox); + } + + return Option.None; + } + + private ItemContainer? GetContainerOrNull(int index) => index >= 0 && index < containers.Length ? containers[index] : null; + + public void CreateRefundItemsForUsedResources(IReadOnlyCollection ids, Character? character) + { + if (!IsInGame()) { return; } + + var prefabsToCreate = Components.Where(comp => ids.Contains(comp.ID)) + .Select(static comp => comp.UsedResource) + .ToImmutableArray(); + + foreach (ItemPrefab prefab in prefabsToCreate) + { + if (character?.Inventory is null) + { + Entity.Spawner?.AddItemToSpawnQueue(prefab, item.Position, item.Submarine); + } + else + { + Entity.Spawner?.AddItemToSpawnQueue(prefab, character.Inventory); + } + } + } + + public static ImmutableArray GetSortedCircuitBoxSortedItemsFromPlayer(Character? character) + => character?.Inventory?.FindAllItems(predicate: CanItemBeAccessed, recursive: true) + .OrderBy(static i => i.Prefab.Identifier == Tags.FPGACircuit) + .ToImmutableArray() ?? ImmutableArray.Empty; + + public static bool CanItemBeAccessed(Item item) => + item.ParentInventory switch + { + ItemInventory ii => ii.Container.DrawInventory, + _ => true + }; + + public static Option GetApplicableResourcePlayerHas(ItemPrefab prefab, Character? character) + { + if (character is null) { return Option.None; } + + return GetApplicableResourcePlayerHas(prefab, GetSortedCircuitBoxSortedItemsFromPlayer(character)); + } + + public static Option GetApplicableResourcePlayerHas(ItemPrefab prefab, ImmutableArray playerItems) + { + foreach (var invItem in playerItems) + { + if (invItem.Prefab == prefab || invItem.Prefab.Identifier == Tags.FPGACircuit) + { + return Option.Some(invItem); + } + } + + return Option.None; + } + + public static void SpawnItem(CircuitBox circuitBox, ItemPrefab prefab, ItemContainer? container, Action onSpawned) + { + if (container is null) + { + throw new Exception("Circuit box has no inventory"); + } + + if (IsInGame()) + { + Entity.Spawner?.AddItemToSpawnQueue(prefab, container.Inventory, onSpawned: onSpawned); + return; + } + + Item forceSpawnedItem = new Item(prefab, Vector2.Zero, null); + container.Inventory.TryPutItem(forceSpawnedItem, null); + onSpawned(forceSpawnedItem); + } + + public static void RemoveItem(Item item) + { + if (IsInGame()) + { + Entity.Spawner?.AddItemToRemoveQueue(item); + return; + } + + item.Remove(); + } + + public static bool IsInGame() + => Screen.Selected is not { IsEditor: true }; + + public static bool IsCircuitBoxSelected(Character character) + => character.SelectedItem?.GetComponent() is not null; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs index 6f0e8a1cb..f1cc7d7ee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs @@ -23,6 +23,15 @@ namespace Barotrauma.Items.Components private readonly HashSet wires; public IReadOnlyCollection Wires => wires; + /// + /// Circuit box input and output connections that are linked to this connection. + /// + /// + /// We don't want to create a wire between the circuit boxes connection panel and the + /// connection panel of the item inside the circuit box so we use this to bridge the gap. + /// + public List CircuitBoxConnections = new(); + private bool enumeratingWires; private readonly HashSet removedWires = new HashSet(); @@ -177,6 +186,12 @@ namespace Barotrauma.Items.Components } } + /// + /// Checks if the the connection is connected to a wire or a circuit box connection + /// + public bool IsConnectedToSomething() + => wires.Count > 0 || CircuitBoxConnections.Count > 0; + public void SetRecipientsDirty() { recipientsDirty = true; @@ -304,25 +319,15 @@ namespace Barotrauma.Items.Components if (recipient.item == this.item || signal.source?.LastSentSignalRecipients.LastOrDefault() == recipient) { continue; } signal.source?.LastSentSignalRecipients.Add(recipient); - - Connection connection = recipient; - connection.LastReceivedSignal = signal; #if CLIENT wire.RegisterSignal(signal, source: this); #endif + SendSignalIntoConnection(signal, recipient); + } - foreach (ItemComponent ic in recipient.item.Components) - { - ic.ReceiveSignal(signal, connection); - } - - if (recipient.Effects != null && signal.value != "0") - { - foreach (StatusEffect effect in recipient.Effects) - { - recipient.Item.ApplyStatusEffect(effect, ActionType.OnUse, (float)Timing.Step); - } - } + foreach (CircuitBoxConnection connection in CircuitBoxConnections) + { + connection.ReceiveSignal(signal); } enumeratingWires = false; foreach (var removedWire in removedWires) @@ -331,7 +336,24 @@ namespace Barotrauma.Items.Components } removedWires.Clear(); } - + + public static void SendSignalIntoConnection(Signal signal, Connection conn) + { + conn.LastReceivedSignal = signal; + + foreach (ItemComponent ic in conn.item.Components) + { + ic.ReceiveSignal(signal, conn); + } + + if (conn.Effects == null || signal.value == "0") { return; } + + foreach (StatusEffect effect in conn.Effects) + { + conn.Item.ApplyStatusEffect(effect, ActionType.OnUse, (float)Timing.Step); + } + } + public void ClearConnections() { if (IsPower && Grid != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs index 7c9fd3a1a..82c21a973 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs @@ -9,7 +9,8 @@ namespace Barotrauma.Items.Components { partial class ConnectionPanel : ItemComponent, IServerSerializable, IClientSerializable { - public List Connections; + const int MaxConnectionCount = 256; + public readonly List Connections = new List(); private Character user; @@ -67,10 +68,13 @@ namespace Barotrauma.Items.Components public ConnectionPanel(Item item, ContentXElement element) : base(item, element) { - Connections = new List(); - foreach (var subElement in element.Elements()) { + if (Connections.Count == MaxConnectionCount) + { + DebugConsole.ThrowError($"Too many connections in the item {item.Prefab.Identifier} (> {MaxConnectionCount})."); + break; + } switch (subElement.Name.ToString()) { case "input": @@ -179,7 +183,7 @@ namespace Barotrauma.Items.Components { UpdateProjSpecific(deltaTime); - if (user == null || user.SelectedItem != item) + if (user == null || (user.SelectedItem != item && user.SelectedSecondaryItem != item)) { #if SERVER if (user != null) { item.CreateServerEvent(this); } @@ -208,6 +212,10 @@ namespace Barotrauma.Items.Components public bool CanRewire() { + if (item.Container?.GetComponent() != null) + { + return true; + } //attaching wires to items with a body is not allowed //(signal items remove their bodies when attached to a wall) if (item.body != null && item.body.BodyType == FarseerPhysics.BodyType.Dynamic) @@ -395,7 +403,7 @@ namespace Barotrauma.Items.Components #if CLIENT TriggerRewiringSound(); #endif - + msg.WriteByte((byte)Connections.Count); foreach (Connection connection in Connections) { msg.WriteVariableUInt32((uint)connection.Wires.Count); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs index eb76f17ea..2258bc176 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs @@ -299,9 +299,12 @@ namespace Barotrauma.Items.Components //make sure the clients know about the states of the checkboxes and text fields if (customInterfaceElementList.Any()) { - if (item.Submarine == null || !item.Submarine.Loading) + if (item.FullyInitialized) { - item.CreateServerEvent(this); + CoroutineManager.Invoke(() => + { + if (!item.Removed) { item.CreateServerEvent(this); } + }, delay: 0.1f); } } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs index 892a3afef..a4e549647 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs @@ -43,7 +43,7 @@ namespace Barotrauma.Items.Components } } - [InGameEditable, Serialize("", IsPropertySaveable.Yes, description: "The signal sent when the condition is met (if empty, no signal is sent).", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("0", IsPropertySaveable.Yes, description: "The signal sent when the condition is met (if empty, no signal is sent).", alwaysUseInstanceValues: true)] public string FalseOutput { get { return falseOutput; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index d2c88ab2c..f405963c9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -198,6 +198,22 @@ namespace Barotrauma.Items.Components set; } + /// + /// Returns true if the red component of the light is twice as bright as the blue and green. Can be used by StatusEffects. + /// + public bool IsRed => ColorExtensions.IsRedDominant(LightColor); + + /// + /// Returns true if the green component of the light is twice as bright as the red and blue. Can be used by StatusEffects. + /// + public bool IsGreen => ColorExtensions.IsGreenDominant(LightColor); + + /// + /// Returns true if the blue component of the light is twice as bright as the red and green. Can be used by StatusEffects. + /// + public bool IsBlue => ColorExtensions.IsBlueDominant(LightColor); + + public float TemporaryFlickerTimer; public override void Move(Vector2 amount, bool ignoreContacts = false) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs index eec77455a..b1d16e4d6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs @@ -121,7 +121,7 @@ namespace Barotrauma.Items.Components } private string falseOutput; - [InGameEditable, Serialize("", IsPropertySaveable.Yes, description: "The signal the item outputs when it has not detected movement.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("0", IsPropertySaveable.Yes, description: "The signal the item outputs when it has not detected movement.", alwaysUseInstanceValues: true)] public string FalseOutput { get { return falseOutput; } @@ -136,7 +136,7 @@ namespace Barotrauma.Items.Components } } - [Editable(DecimalCount = 3), Serialize(0.1f, IsPropertySaveable.Yes, description: "How fast the objects within the detector's range have to be moving (in m/s).", alwaysUseInstanceValues: true)] + [InGameEditable(DecimalCount = 3), Serialize(0.0f, IsPropertySaveable.Yes, description: "How fast the objects within the detector's range have to be moving (in m/s).", alwaysUseInstanceValues: true)] public float MinimumVelocity { get; @@ -254,45 +254,52 @@ namespace Barotrauma.Items.Components } } - if (Target.HasFlag(TargetType.Human) || Target.HasFlag(TargetType.Pet) || Target.HasFlag(TargetType.Monster)) + bool triggerFromHumans = Target.HasFlag(TargetType.Human); + bool triggerFromPets = Target.HasFlag(TargetType.Pet); + bool triggerFromMonsters = Target.HasFlag(TargetType.Monster); + bool hasTriggers = triggerFromHumans || triggerFromPets || triggerFromMonsters; + if (!hasTriggers) { return; } + foreach (Character c in Character.CharacterList) { - foreach (Character c in Character.CharacterList) + if (IgnoreDead && c.IsDead) { continue; } + + //ignore characters that have spawned a second or less ago + //makes it possible to detect when a spawned character moves without triggering the detector immediately as the ragdoll spawns and drops to the ground + if (c.SpawnTime > Timing.TotalTime - 1.0) { continue; } + if (c.IsHuman) { - if (IgnoreDead && c.IsDead) { continue; } - - //ignore characters that have spawned a second or less ago - //makes it possible to detect when a spawned character moves without triggering the detector immediately as the ragdoll spawns and drops to the ground - if (c.SpawnTime > Timing.TotalTime - 1.0) { continue; } - - if (c.IsHuman) - { - if (!Target.HasFlag(TargetType.Human)) { continue; } - } - else if (c.IsPet) - { - if (!Target.HasFlag(TargetType.Pet)) { continue; } - } - else - { - if (!Target.HasFlag(TargetType.Monster)) { continue; } - } - - //do a rough check based on the position of the character's collider first - //before the more accurate limb-based check - if (Math.Abs(c.WorldPosition.X - detectPos.X) > broadRangeX || Math.Abs(c.WorldPosition.Y - detectPos.Y) > broadRangeY) + if (!triggerFromHumans) { continue; } + } + else if (c.IsPet) + { + if (!triggerFromPets) { continue; } + } + else + { + // Not a human or a pet -> monster? + if (!triggerFromMonsters) { continue; } + if (CharacterParams.CompareGroup(c.Group, CharacterPrefab.HumanGroup)) { + //characters in the "human" group aren't considered monsters (even if they were something like a friendly mudraptor) continue; } + } - foreach (Limb limb in c.AnimController.Limbs) + //do a rough check based on the position of the character's collider first + //before the more accurate limb-based check + if (Math.Abs(c.WorldPosition.X - detectPos.X) > broadRangeX || Math.Abs(c.WorldPosition.Y - detectPos.Y) > broadRangeY) + { + continue; + } + + foreach (Limb limb in c.AnimController.Limbs) + { + if (limb.IsSevered) { continue; } + if (limb.LinearVelocity.LengthSquared() < MinimumVelocity * MinimumVelocity) { continue; } + if (MathUtils.CircleIntersectsRectangle(limb.WorldPosition, ConvertUnits.ToDisplayUnits(limb.body.GetMaxExtent()), detectRect)) { - if (limb.IsSevered) { continue; } - if (limb.LinearVelocity.LengthSquared() < MinimumVelocity * MinimumVelocity) { continue; } - if (MathUtils.CircleIntersectsRectangle(limb.WorldPosition, ConvertUnits.ToDisplayUnits(limb.body.GetMaxExtent()), detectRect)) - { - MotionDetected = true; - return; - } + MotionDetected = true; + return; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs index 33e8f4538..a78255abd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs @@ -50,6 +50,9 @@ namespace Barotrauma.Items.Components [InGameEditable, Serialize(false, IsPropertySaveable.Yes, description: "Should the component output a value of a capture group instead of a constant signal.", alwaysUseInstanceValues: true)] public bool UseCaptureGroup { get; set; } + [InGameEditable, Serialize(false, IsPropertySaveable.Yes, description: "Should the component output the value of a capture group even if it's empty?", alwaysUseInstanceValues: true)] + public bool OutputEmptyCaptureGroup { get; set; } + [InGameEditable, Serialize("0", IsPropertySaveable.Yes, description: "The signal this item outputs when the received signal does not match the regular expression.", alwaysUseInstanceValues: true)] public string FalseOutput { get; set; } @@ -120,6 +123,7 @@ namespace Barotrauma.Items.Components } string signalOut; + bool allowEmptyStringOutput = false; if (previousResult) { if (UseCaptureGroup) @@ -127,6 +131,7 @@ namespace Barotrauma.Items.Components if (previousGroups != null && previousGroups.TryGetValue(Output, out Group group)) { signalOut = group.Value; + allowEmptyStringOutput = OutputEmptyCaptureGroup; } else { @@ -143,13 +148,9 @@ namespace Barotrauma.Items.Components signalOut = FalseOutput; } - if (ContinuousOutput) + if (!string.IsNullOrEmpty(signalOut) || (allowEmptyStringOutput && signalOut == string.Empty)) { item.SendSignal(signalOut, "signal_out"); } + if (!ContinuousOutput) { - if (!string.IsNullOrEmpty(signalOut)) { item.SendSignal(signalOut, "signal_out"); } - } - else - { - if (!string.IsNullOrEmpty(signalOut)) { item.SendSignal(signalOut, "signal_out"); } nonContinuousOutputSent = true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs index 5696c6280..c86f30ef2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs @@ -70,7 +70,7 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Can the relay currently pass power and signals through it.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize(true, IsPropertySaveable.Yes, description: "Can the relay currently pass power and signals through it.", alwaysUseInstanceValues: true)] public bool IsOn { get @@ -139,7 +139,7 @@ namespace Barotrauma.Items.Components isBroken = false; } - ApplyStatusEffects(ActionType.OnActive, deltaTime, null); + ApplyStatusEffects(ActionType.OnActive, deltaTime); if (Voltage > OverloadVoltage && CanBeOverloaded && item.Repairables.Any()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs index 4f09c6764..6b73ebd93 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs @@ -10,11 +10,13 @@ namespace Barotrauma.Items.Components { public readonly string Text; public readonly Color Color; + public readonly bool IsWelcomeMessage; - public TerminalMessage(string text, Color color) + public TerminalMessage(string text, Color color, bool isWelcomeMessage) { Text = text; Color = color; + IsWelcomeMessage = isWelcomeMessage; } public void Deconstruct(out string text, out Color color) @@ -60,7 +62,7 @@ namespace Barotrauma.Items.Components set { if (string.IsNullOrEmpty(value)) { return; } - ShowOnDisplay(value, addToHistory: true, TextColor); + ShowOnDisplay(value, addToHistory: true, TextColor, isWelcomeMessage: false); } } @@ -113,7 +115,7 @@ namespace Barotrauma.Items.Components partial void InitProjSpecific(XElement element); - partial void ShowOnDisplay(string input, bool addToHistory, Color color); + partial void ShowOnDisplay(string input, bool addToHistory, Color color, bool isWelcomeMessage); public override void ReceiveSignal(Signal signal, Connection connection) { @@ -127,7 +129,7 @@ namespace Barotrauma.Items.Components signal.value = signal.value.Substring(0, MaxMessageLength); } string inputSignal = signal.value.Replace("\\n", "\n"); - ShowOnDisplay(inputSignal, addToHistory: true, TextColor); + ShowOnDisplay(inputSignal, addToHistory: true, TextColor, isWelcomeMessage: false); break; case "set_text_color": if (signal.value != prevColorSignal) @@ -160,7 +162,7 @@ namespace Barotrauma.Items.Components base.OnItemLoaded(); if (!DisplayedWelcomeMessage.IsNullOrEmpty() && !WelcomeMessageDisplayed) { - ShowOnDisplay(DisplayedWelcomeMessage.Value, addToHistory: !isSubEditor, TextColor); + ShowOnDisplay(DisplayedWelcomeMessage.Value, addToHistory: !isSubEditor, TextColor, isWelcomeMessage: true); DisplayedWelcomeMessage = ""; //disable welcome message if a game session is running so it doesn't reappear on successive rounds if (GameMain.GameSession != null && !isSubEditor) @@ -175,8 +177,13 @@ namespace Barotrauma.Items.Components var componentElement = base.Save(parentElement); for (int i = 0; i < messageHistory.Count; i++) { - componentElement.Add(new XAttribute("msg" + i, messageHistory[i].Text)); - componentElement.Add(new XAttribute("color" + i, messageHistory[i].Color.ToStringHex())); + var msg = messageHistory[i]; + componentElement.Add(new XAttribute("msg" + i, msg.Text)); + componentElement.Add(new XAttribute("color" + i, msg.Color.ToStringHex())); + if (msg.IsWelcomeMessage) + { + componentElement.Add(new XAttribute("welcomemessage" + i, true)); + } } return componentElement; } @@ -189,7 +196,8 @@ namespace Barotrauma.Items.Components string msg = componentElement.GetAttributeString("msg" + i, null); if (msg is null) { break; } Color color = componentElement.GetAttributeColor("color" + i, TextColor); - ShowOnDisplay(msg, addToHistory: true, color); + bool isWelcomeMessage = componentElement.GetAttributeBool("welcomemessage" + i, false); + ShowOnDisplay(msg, addToHistory: true, color, isWelcomeMessage); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs index 10fec01b0..bb61ce491 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs @@ -69,7 +69,10 @@ namespace Barotrauma.Items.Components public static int GetWaterPercentage(Hull hull) { - return hull.WaterVolume > 1.0f ? MathHelper.Clamp((int)Math.Ceiling(hull.WaterPercentage), 0, 100) : 0; + //treat less than one pixel of water as "no water" + return hull.WaterVolume / hull.Rect.Width > 1.0f ? + MathHelper.Clamp((int)Math.Ceiling(hull.WaterPercentage), 0, 100) : + 0; } public override void Update(float deltaTime, Camera cam) @@ -88,7 +91,7 @@ namespace Barotrauma.Items.Components //item in water -> we definitely want to send the True output isInWater = true; } - else if (item.CurrentHull != null && item.CurrentHull.WaterPercentage > 0.0f && item.CurrentHull.WaterVolume > 1.0f) + else if (item.CurrentHull != null && GetWaterPercentage(item.CurrentHull) > 0) { //(center of the) item in not water -> check if the water surface is below the bottom of the item's rect if (item.CurrentHull.Surface > item.Rect.Y - item.Rect.Height) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs index ebc87ccb7..a7117b73b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs @@ -89,6 +89,26 @@ namespace Barotrauma.Items.Components set; } + private float jamTimer; + public float JamTimer + { + get { return jamTimer; } + set + { + if (value > 0) + { +#if CLIENT + if (jamTimer <= 0) + { + HintManager.OnRadioJammed(Item); + } +#endif + IsActive = true; + } + jamTimer = Math.Max(0, value); + } + } + public WifiComponent(Item item, ContentXElement element) : base (item, element) { @@ -123,8 +143,12 @@ namespace Barotrauma.Items.Components } } - public bool CanTransmit() + public bool CanTransmit(bool ignoreJamming = false) { + if (!ignoreJamming) + { + if (jamTimer > 0) { return false; } + } return HasRequiredContainedItems(user: null, addMessage: false); } @@ -140,12 +164,13 @@ namespace Barotrauma.Items.Components { if (sender == null || sender.channel != channel) { return false; } if (sender.TeamID != TeamID && !AllowCrossTeamCommunication) { return false; } + if (jamTimer > 0) { return false; } //if the component is not linked to chat and has nothing connected to the output, sending a signal to it does nothing // = no point in receiving if (!LinkToChat) { - if (signalOutConnection == null || signalOutConnection.Wires.Count <= 0) + if (signalOutConnection == null || !signalOutConnection.IsConnectedToSomething()) { return false; } @@ -169,12 +194,16 @@ namespace Barotrauma.Items.Components if (sender == null || sender.channel != channel) { return false; } if (sender.TeamID != TeamID && !AllowCrossTeamCommunication) { return false; } if (Vector2.DistanceSquared(item.WorldPosition, sender.item.WorldPosition) > sender.range * sender.range) { return false; } + if (jamTimer > 0) { return false; } return HasRequiredContainedItems(user: null, addMessage: false); } + public override void Update(float deltaTime, Camera cam) { chatMsgCooldown -= deltaTime; - if (chatMsgCooldown <= 0.0f) + JamTimer -= deltaTime; + ApplyStatusEffects(ActionType.OnActive, deltaTime); + if (chatMsgCooldown <= 0.0f && JamTimer <= 0.0f) { IsActive = false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs index 39da66dbb..f96dc53e4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs @@ -110,7 +110,14 @@ namespace Barotrauma.Items.Components get; set; } - + + [Serialize(true, IsPropertySaveable.Yes, "If disabled, the wire will not be dropped when connecting. Used in circuit box to store the wires inside the box.")] + public bool DropOnConnect + { + get; + set; + } + public Wire(Item item, ContentXElement element) : base(item, element) { @@ -224,26 +231,31 @@ namespace Barotrauma.Items.Components connections[connectionIndex] = newConnection; FixNodeEnds(); - if (addNode) + if (addNode) { AddNode(newConnection, connectionIndex); } SetConnectedDirty(); - if (connections[0] != null && connections[1] != null) + if (DropOnConnect) { - foreach (ItemComponent ic in item.Components) + if (connections[0] != null && connections[1] != null) { - if (ic == this) { continue; } - ic.Drop(null); + foreach (ItemComponent ic in item.Components) + { + if (ic == this) { continue; } + + ic.Drop(null); + } + + item.Container?.RemoveContained(item); + if (item.body != null) { item.body.Enabled = false; } + + IsActive = false; + + CleanNodes(); } - item.Container?.RemoveContained(item); - if (item.body != null) { item.body.Enabled = false; } - - IsActive = false; - - CleanNodes(); } if (item.body != null) { item.Submarine = newConnection.Item.Submarine; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 02f0807b8..9c29503ad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -304,6 +304,21 @@ namespace Barotrauma.Items.Components set; } + private float _maxAngleOffset; + [Serialize(10.0f, IsPropertySaveable.No, description: "How much off the turret can be from the target for the AI to shoot. In degrees.")] + public float MaxAngleOffset + { + get => _maxAngleOffset; + private set => _maxAngleOffset = MathHelper.Clamp(value, 0f, 180f); + } + + [Serialize(1.1f, IsPropertySaveable.No, description: "How much does the AI prefer currently selected targets over new targets closer to the turret.")] + public float AICurrentTargetPriorityMultiplier + { + get; + private set; + } + [Serialize(-1, IsPropertySaveable.Yes, description: "The turret won't fire additional projectiles if the number of previously fired, still active projectiles reaches this limit. If set to -1, there is no limit to the number of projectiles.")] public int MaxActiveProjectiles { @@ -383,7 +398,7 @@ namespace Barotrauma.Items.Components } item.IsShootable = true; item.RequireAimToUse = false; - isSlowTurret = item.HasTag("slowturret"); + isSlowTurret = item.HasTag("slowturret".ToIdentifier()); InitProjSpecific(element); } @@ -474,7 +489,7 @@ namespace Barotrauma.Items.Components } } - ApplyStatusEffects(ActionType.OnActive, deltaTime, null); + ApplyStatusEffects(ActionType.OnActive, deltaTime); float previousChargeTime = currentChargeTime; @@ -594,9 +609,9 @@ namespace Barotrauma.Items.Components UpdateLightComponents(); - if (AutoOperate) + if (AutoOperate && ActiveUser == null) { - UpdateAutoOperate(deltaTime); + UpdateAutoOperate(deltaTime, ignorePower: false); } } @@ -687,7 +702,7 @@ namespace Barotrauma.Items.Components ItemContainer projectileContainer = projectiles.First().Item.Container?.GetComponent(); if (projectileContainer != null && projectileContainer.Item != item) { - projectileContainer?.Item.Use(deltaTime, null); + projectileContainer?.Item.Use(deltaTime); } } else @@ -713,7 +728,7 @@ namespace Barotrauma.Items.Components ItemContainer projectileContainer = containerItem.GetComponent(); if (projectileContainer != null) { - containerItem.Use(deltaTime, null); + containerItem.Use(deltaTime); projectiles = GetLoadedProjectiles(); if (projectiles.Any()) { return true; } } @@ -749,7 +764,7 @@ namespace Barotrauma.Items.Components { if (e is not Item linkedItem) { continue; } if (!((MapEntity)item).Prefab.IsLinkAllowed(e.Prefab)) { continue; } - if (linkedItem.GetComponent() is Repairable repairable && repairable.IsTinkering && linkedItem.HasTag("turretammosource")) + if (linkedItem.GetComponent() is Repairable repairable && repairable.IsTinkering && linkedItem.HasTag(Tags.TurretAmmoSource)) { tinkeringStrength = repairable.TinkeringStrength; } @@ -757,28 +772,29 @@ namespace Barotrauma.Items.Components if (!ignorePower) { - List batteries = GetDirectlyConnectedBatteries(); - float neededPower = GetPowerRequiredToShoot(); - - // tinkering is currently not factored into the common method as it is checked only when shooting - // but this is a minor issue that causes mostly cosmetic woes. might still be worth refactoring later - neededPower /= 1f + (tinkeringStrength * TinkeringPowerCostReduction); - - while (neededPower > 0.0001f && batteries.Count > 0) + var batteries = GetDirectlyConnectedBatteries().Where(static b => !b.OutputDisabled && b.Charge > 0.0001f && b.MaxOutPut > 0.0001f); + int batteryCount = batteries.Count(); + if (batteryCount > 0) { - batteries.RemoveAll(b => b.Charge <= 0.0001f || b.MaxOutPut <= 0.0001f); - if (!batteries.Any()) { break; } - float takePower = neededPower / batteries.Count; - takePower = Math.Min(takePower, batteries.Min(b => Math.Min(b.Charge * 3600.0f, b.MaxOutPut))); - foreach (PowerContainer battery in batteries) + float neededPower = GetPowerRequiredToShoot(); + // tinkering is currently not factored into the common method as it is checked only when shooting + // but this is a minor issue that causes mostly cosmetic woes. might still be worth refactoring later + neededPower /= 1f + (tinkeringStrength * TinkeringPowerCostReduction); + while (neededPower > 0.0001f) { - neededPower -= takePower; - battery.Charge -= takePower / 3600.0f; + float takePower = neededPower / batteryCount; + takePower = Math.Min(takePower, batteries.Min(b => Math.Min(b.Charge * 3600.0f, b.MaxOutPut))); + foreach (PowerContainer battery in batteries) + { + neededPower -= takePower; + battery.Charge -= takePower / 3600.0f; #if SERVER - battery.Item.CreateServerEvent(battery); + battery.Item.CreateServerEvent(battery); #endif + } } } + } launchedProjectile = projectiles.FirstOrDefault(); @@ -891,9 +907,21 @@ namespace Barotrauma.Items.Components } float spread = MathHelper.ToRadians(Spread) * Rand.Range(-0.5f, 0.5f); - projectile.SetTransform( - ConvertUnits.ToSimUnits(GetRelativeFiringPosition()), - -(launchRotation ?? rotation) + spread); + + Vector2 launchPos = ConvertUnits.ToSimUnits(GetRelativeFiringPosition()); + + //check if there's some other sub between the turret's origin and the launch pos, + //and if so, launch at the intersection of the turret and the sub to prevent the projectile from spawning inside the other sub + Body pickedBody = Submarine.PickBody(ConvertUnits.ToSimUnits(item.WorldPosition), launchPos, null, Physics.CollisionWall, allowInsideFixture: true, + customPredicate: (Fixture f) => + { + return f.Body.UserData is not Submarine sub || sub != item.Submarine; + }); + if (pickedBody != null) + { + launchPos = Submarine.LastPickedPosition; + } + projectile.SetTransform(launchPos, -(launchRotation ?? rotation) + spread); projectile.UpdateTransform(); projectile.Submarine = projectile.body?.Submarine; @@ -959,8 +987,15 @@ namespace Barotrauma.Items.Components private float updateTimer; private bool updatePending; - public void UpdateAutoOperate(float deltaTime, Identifier friendlyTag = default) + private float GetTargetPriorityModifier() => currentChargingState == ChargingState.WindingUp ? 10f : AICurrentTargetPriorityMultiplier; + + public void UpdateAutoOperate(float deltaTime, bool ignorePower, Identifier friendlyTag = default) { + if (!ignorePower && !HasPowerToShoot()) + { + return; + } + IsActive = true; if (friendlyTag.IsEmpty) @@ -1006,8 +1041,12 @@ namespace Barotrauma.Items.Components if (!IsValidTargetForAutoOperate(character, friendlyTag)) { continue; } float dist = Vector2.DistanceSquared(character.WorldPosition, item.WorldPosition); if (dist > closestDist) { continue; } - if (!CheckTurretAngle(character.WorldPosition)) { continue; } + if (!IsWithinAimingRadius(character.WorldPosition)) { continue; } target = character; + if (currentTarget != null && target == currentTarget) + { + priority *= GetTargetPriorityModifier(); + } closestDist = dist / priority; } } @@ -1022,8 +1061,12 @@ namespace Barotrauma.Items.Components if (dist > closestDist) { continue; } if (dist > shootDistance * shootDistance) { continue; } if (!IsTargetItemCloseEnough(targetItem, dist)) { continue; } - if (!CheckTurretAngle(targetItem.WorldPosition)) { continue; } + if (!IsWithinAimingRadius(targetItem.WorldPosition)) { continue; } target = targetItem; + if (currentTarget != null && target == currentTarget) + { + priority *= GetTargetPriorityModifier(); + } closestDist = dist / priority; } } @@ -1090,10 +1133,10 @@ namespace Barotrauma.Items.Components } } if (target == null) { return; } - + currentTarget = target; + float angle = -MathUtils.VectorToAngle(target.WorldPosition - item.WorldPosition); targetRotation = MathUtils.WrapAngleTwoPi(angle); - if (Math.Abs(targetRotation - prevTargetRotation) > 0.1f) { updatePending = true; } if (target is Hull targetHull) @@ -1106,10 +1149,8 @@ namespace Barotrauma.Items.Components } else { - if (!CheckTurretAngle(angle)) { return; } - float enemyAngle = MathUtils.VectorToAngle(target.WorldPosition - item.WorldPosition); - float turretAngle = -rotation; - if (Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) > 0.15f) { return; } + if (!IsWithinAimingRadius(angle)) { return; } + if (!IsPointingTowards(target.WorldPosition)) { return; } } Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition); Vector2 end = ConvertUnits.ToSimUnits(target.WorldPosition); @@ -1129,7 +1170,7 @@ namespace Barotrauma.Items.Components } if (shoot) { - TryLaunch(deltaTime, ignorePower: true); + TryLaunch(deltaTime, ignorePower: ignorePower); } } @@ -1149,12 +1190,12 @@ namespace Barotrauma.Items.Components bool canShoot = HasPowerToShoot(); if (!canShoot) { - List batteries = GetDirectlyConnectedBatteries(); float lowestCharge = 0.0f; PowerContainer batteryToLoad = null; - foreach (PowerContainer battery in batteries) + foreach (PowerContainer battery in GetDirectlyConnectedBatteries()) { if (!battery.Item.IsInteractable(character)) { continue; } + if (battery.OutputDisabled) { continue; } if (batteryToLoad == null || battery.Charge < lowestCharge) { batteryToLoad = battery; @@ -1328,7 +1369,11 @@ namespace Barotrauma.Items.Components { // Only check the angle to targets that are close enough to be shot at // We shouldn't check the angle when a long creature is traveling outside of the shooting range, because doing so would not allow us to shoot the limbs that might be close enough to shoot at. - if (!CheckTurretAngle(enemy.WorldPosition)) { continue; } + if (!IsWithinAimingRadius(enemy.WorldPosition)) { continue; } + } + if (currentTarget != null && enemy == currentTarget) + { + priority *= GetTargetPriorityModifier(); } targetPos = enemy.WorldPosition; closestEnemy = enemy; @@ -1344,7 +1389,11 @@ namespace Barotrauma.Items.Components if (dist > closestDistance) { continue; } if (dist > shootDistance * shootDistance) { continue; } if (!IsTargetItemCloseEnough(targetItem, dist)) { continue; } - if (!CheckTurretAngle(targetItem.WorldPosition)) { continue; } + if (!IsWithinAimingRadius(targetItem.WorldPosition)) { continue; } + if (currentTarget != null && targetItem == currentTarget) + { + priority *= GetTargetPriorityModifier(); + } targetPos = targetItem.WorldPosition; closestDistance = dist / priority; // Override the target character so that we can target the item instead. @@ -1378,7 +1427,7 @@ namespace Barotrauma.Items.Components { if (limb.IsSevered) { continue; } if (limb.Hidden) { continue; } - if (!CheckTurretAngle(limb.WorldPosition)) { continue; } + if (!IsWithinAimingRadius(limb.WorldPosition)) { continue; } float dist = Vector2.DistanceSquared(limb.WorldPosition, item.WorldPosition); if (dist < closestDist) { @@ -1416,14 +1465,14 @@ namespace Barotrauma.Items.Components Vector2 p1 = edge.Point1 + cell.Translation; Vector2 p2 = edge.Point2 + cell.Translation; Vector2 closestPoint = MathUtils.GetClosestPointOnLineSegment(p1, p2, item.WorldPosition); - if (!CheckTurretAngle(closestPoint)) + if (!IsWithinAimingRadius(closestPoint)) { // The closest point can't be targeted -> get a point directly in front of the turret Vector2 barrelDir = new Vector2((float)Math.Cos(rotation), -(float)Math.Sin(rotation)); if (MathUtils.GetLineSegmentIntersection(p1, p2, item.WorldPosition, item.WorldPosition + barrelDir * shootDistance, out Vector2 intersection)) { closestPoint = intersection; - if (!CheckTurretAngle(closestPoint)) { continue; } + if (!IsWithinAimingRadius(closestPoint)) { continue; } } else { @@ -1510,19 +1559,7 @@ namespace Barotrauma.Items.Components character.CursorPosition -= character.Submarine.Position; } - float enemyAngle = MathUtils.VectorToAngle(targetPos.Value - item.WorldPosition); - float turretAngle = -rotation; - - float maxAngleError = 0.15f; - if (MaxChargeTime > 0.0f && currentChargingState == ChargingState.WindingUp && FiringRotationSpeedModifier > 0.0f) - { - //larger margin of error if the weapon needs to be charged (-> the bot can start charging when the turret is still rotating towards the target) - maxAngleError *= 2.0f; - } - - if (Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) > maxAngleError) { return false; } - - if (canShoot) + if (IsPointingTowards(targetPos.Value)) { Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition); Vector2 end = ConvertUnits.ToSimUnits(targetPos.Value); @@ -1551,6 +1588,19 @@ namespace Barotrauma.Items.Components return false; } + private bool IsPointingTowards(Vector2 targetPos) + { + float enemyAngle = MathUtils.VectorToAngle(targetPos - item.WorldPosition); + float turretAngle = -rotation; + float maxAngleError = MathHelper.ToRadians(MaxAngleOffset); + if (MaxChargeTime > 0.0f && currentChargingState == ChargingState.WindingUp && FiringRotationSpeedModifier > 0.0f) + { + //larger margin of error if the weapon needs to be charged (-> the bot can start charging when the turret is still rotating towards the target) + maxAngleError *= 2.0f; + } + return Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) <= maxAngleError; + } + private bool IsTargetItemCloseEnough(Item target, float sqrDist) => float.IsPositiveInfinity(target.Prefab.AITurretTargetingMaxDistance) || sqrDist < MathUtils.Pow2(target.Prefab.AITurretTargetingMaxDistance); /// @@ -1669,6 +1719,7 @@ namespace Barotrauma.Items.Components customPredicate: (Fixture f) => { if (f.UserData is Item i && i.GetComponent() != null) { return false; } + if (f.UserData is Hull) { return false; } return !item.StaticFixtures.Contains(f); }); return pickedBody; @@ -1686,7 +1737,7 @@ namespace Barotrauma.Items.Components return new Vector2(item.WorldRect.X + transformedBarrelPos.X + transformedFiringOffset.X, item.WorldRect.Y - transformedBarrelPos.Y + transformedFiringOffset.Y); } - private bool CheckTurretAngle(float angle) + private bool IsWithinAimingRadius(float angle) { float midRotation = (minRotation + maxRotation) / 2.0f; while (midRotation - angle < -MathHelper.Pi) { angle -= MathHelper.TwoPi; } @@ -1694,7 +1745,7 @@ namespace Barotrauma.Items.Components return angle >= minRotation && angle <= maxRotation; } - public bool CheckTurretAngle(Vector2 target) => CheckTurretAngle(-MathUtils.VectorToAngle(target - item.WorldPosition)); + public bool IsWithinAimingRadius(Vector2 target) => IsWithinAimingRadius(-MathUtils.VectorToAngle(target - item.WorldPosition)); protected override void RemoveComponentSpecific() { @@ -1835,7 +1886,7 @@ namespace Barotrauma.Items.Components break; case "trigger_in": if (signal.value == "0") { return; } - item.Use((float)Timing.Step, sender); + item.Use((float)Timing.Step, user: sender); user = sender; ActiveUser = sender; resetActiveUserTimer = 1f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index 92e4bc74c..c94cc73e0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -7,6 +7,7 @@ using System.Xml.Linq; using Barotrauma.Items.Components; using Barotrauma.Networking; using Barotrauma.Abilities; +using System.Collections.Immutable; namespace Barotrauma { @@ -55,6 +56,11 @@ namespace Barotrauma public bool HideOtherWearables => ObscureOtherWearables == ObscuringMode.Hide; public bool AlphaClipOtherWearables => ObscureOtherWearables == ObscuringMode.AlphaClip; public bool CanBeHiddenByOtherWearables { get; private set; } + + /// + /// Tags or identifiers of items that can hide this one. + /// + public ImmutableHashSet CanBeHiddenByItem { get; private set; } public List HideWearablesOfType { get; private set; } public bool InheritLimbDepth { get; private set; } /// @@ -139,11 +145,6 @@ namespace Barotrauma case WearableType.Husk: case WearableType.Herpes: Limb = LimbType.Head; - ObscureOtherWearables = ObscuringMode.None; - InheritLimbDepth = true; - InheritScale = true; - InheritOrigin = true; - InheritSourceRect = true; break; } } @@ -177,6 +178,10 @@ namespace Barotrauma public void ParsePath(bool parseSpritePath) { +#if SERVER + // Server doesn't care about texture paths at all + return; +#endif SpritePath = UnassignedSpritePath.Value; if (_picker?.Info != null) { @@ -196,7 +201,7 @@ namespace Barotrauma } if (parseSpritePath) { - Sprite.ParseTexturePath(file: SpritePath); + Sprite?.ParseTexturePath(file: SpritePath); } } @@ -222,22 +227,23 @@ namespace Barotrauma } CanBeHiddenByOtherWearables = SourceElement.GetAttributeBool("canbehiddenbyotherwearables", true); + CanBeHiddenByItem = SourceElement.GetAttributeIdentifierArray(nameof(CanBeHiddenByItem), Array.Empty()).ToImmutableHashSet(); InheritLimbDepth = SourceElement.GetAttributeBool("inheritlimbdepth", true); var scale = SourceElement.GetAttribute("inheritscale"); if (scale != null) { - InheritScale = scale.GetAttributeBool(false); + InheritScale = scale.GetAttributeBool(Type != WearableType.Item); } else { - InheritScale = SourceElement.GetAttributeBool("inherittexturescale", false); + InheritScale = SourceElement.GetAttributeBool("inherittexturescale", Type != WearableType.Item); } IgnoreLimbScale = SourceElement.GetAttributeBool("ignorelimbscale", false); IgnoreTextureScale = SourceElement.GetAttributeBool("ignoretexturescale", false); IgnoreRagdollScale = SourceElement.GetAttributeBool("ignoreragdollscale", false); SourceElement.GetAttributeBool("inherittexturescale", false); - InheritOrigin = SourceElement.GetAttributeBool("inheritorigin", false); - InheritSourceRect = SourceElement.GetAttributeBool("inheritsourcerect", false); + InheritOrigin = SourceElement.GetAttributeBool("inheritorigin", Type != WearableType.Item); + InheritSourceRect = SourceElement.GetAttributeBool("inheritsourcerect", Type != WearableType.Item); DepthLimb = (LimbType)Enum.Parse(typeof(LimbType), SourceElement.GetAttributeString("depthlimb", "None"), true); Sound = SourceElement.GetAttributeString("sound", ""); Scale = SourceElement.GetAttributeFloat("scale", 1.0f); @@ -457,22 +463,20 @@ namespace Barotrauma.Items.Components if (!equipLimb.WearingItems.Contains(wearableSprite)) { equipLimb.WearingItems.Add(wearableSprite); - equipLimb.WearingItems.Sort((i1, i2) => { return i2.Sprite.Depth.CompareTo(i1.Sprite.Depth); }); - equipLimb.WearingItems.Sort((i1, i2) => + equipLimb.WearingItems.Sort((wearable, nextWearable) => { - if (i1?.WearableComponent == null && i2?.WearableComponent == null) - { - return 0; - } - else if (i1?.WearableComponent == null) - { - return -1; - } - else if (i2?.WearableComponent == null) - { - return 1; - } - return i1.WearableComponent.AllowedSlots.Contains(InvSlotType.OuterClothes).CompareTo(i2.WearableComponent.AllowedSlots.Contains(InvSlotType.OuterClothes)); + float depth = wearable?.Sprite?.Depth ?? 0; + float nextDepth = nextWearable?.Sprite?.Depth ?? 0; + return nextDepth.CompareTo(depth); + }); + equipLimb.WearingItems.Sort((wearable, nextWearable) => + { + var wearableComponent = wearable?.WearableComponent; + var nextWearableComponent = nextWearable?.WearableComponent; + if (wearableComponent == null && nextWearableComponent == null) { return 0; } + if (wearableComponent == null) { return -1; } + if (nextWearableComponent == null) { return 1; } + return wearableComponent.AllowedSlots.Contains(InvSlotType.OuterClothes).CompareTo(nextWearableComponent.AllowedSlots.Contains(InvSlotType.OuterClothes)); }); } #if CLIENT @@ -553,10 +557,7 @@ namespace Barotrauma.Items.Components foreach (WearableSprite wearableSprite in wearableSprites) { - if (wearableSprite != null && wearableSprite.Sprite != null) - { - wearableSprite.Sprite.Remove(); - } + wearableSprite?.Sprite?.Remove(); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index 8be00ac66..af251d6ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -8,18 +8,27 @@ using System.Linq; namespace Barotrauma { - partial class Inventory : IServerSerializable, IClientSerializable + partial class Inventory { - public const int MaxStackSize = (1 << 6) - 1; //the max value that will fit in 6 bits, i.e 63 + public const int MaxPossibleStackSize = (1 << 6) - 1; //the max value that will fit in 6 bits, i.e 63 + + public const int MaxItemsPerNetworkEvent = 128; public class ItemSlot { - private readonly List items = new List(MaxStackSize); + private readonly List items = new List(MaxPossibleStackSize); public bool HideIfEmpty; public IReadOnlyList Items => items; + private readonly Inventory inventory; + + public ItemSlot(Inventory inventory) + { + this.inventory = inventory; + } + public bool CanBePut(Item item, bool ignoreCondition = false) { if (item == null) { return false; } @@ -41,7 +50,7 @@ namespace Barotrauma } } if (items[0].Quality != item.Quality) { return false; } - if (items[0].Prefab.Identifier != item.Prefab.Identifier || items.Count + 1 > item.Prefab.MaxStackSize) + if (items[0].Prefab.Identifier != item.Prefab.Identifier || items.Count + 1 > item.Prefab.GetMaxStackSize(inventory)) { return false; } @@ -80,7 +89,7 @@ namespace Barotrauma } if (items[0].Prefab.Identifier != itemPrefab.Identifier || - items.Count + 1 > itemPrefab.MaxStackSize) + items.Count + 1 > itemPrefab.GetMaxStackSize(inventory)) { return false; } @@ -92,7 +101,7 @@ namespace Barotrauma public int HowManyCanBePut(ItemPrefab itemPrefab, int? maxStackSize = null, float? condition = null) { if (itemPrefab == null) { return 0; } - maxStackSize ??= itemPrefab.MaxStackSize; + maxStackSize ??= itemPrefab.GetMaxStackSize(inventory); if (items.Count > 0) { if (condition.HasValue) @@ -135,9 +144,9 @@ namespace Barotrauma { throw new InvalidOperationException("Tried to stack different types of items."); } - else if (items.Count + 1 > item.Prefab.MaxStackSize) + else if (items.Count + 1 > item.Prefab.GetMaxStackSize(inventory)) { - throw new InvalidOperationException("Tried to add an item to a full inventory slot (stack already full)."); + throw new InvalidOperationException($"Tried to add an item to a full inventory slot (stack already full, x{items.Count} {items.First().Prefab.Identifier})."); } } if (items.Contains(item)) { return; } @@ -229,34 +238,11 @@ namespace Barotrauma /// /// All items contained in the inventory. Stacked items are returned as individual instances. DO NOT modify the contents of the inventory while enumerating this list. /// - public IEnumerable AllItems + public virtual IEnumerable AllItems { get { - for (int i = 0; i < capacity; i++) - { - foreach (var item in slots[i].Items) - { - if (item == null) - { -#if DEBUG - DebugConsole.ThrowError($"Null item in inventory {Owner.ToString() ?? "null"}, slot {i}!"); -#endif - continue; - } - - bool duplicateFound = false; - for (int j = 0; j < i; j++) - { - if (slots[j].Items.Contains(item)) - { - duplicateFound = true; - break; - } - } - if (!duplicateFound) { yield return item; } - } - } + return GetAllItems(checkForDuplicates: this is CharacterInventory); } } @@ -292,7 +278,7 @@ namespace Barotrauma slots = new ItemSlot[capacity]; for (int i = 0; i < capacity; i++) { - slots[i] = new ItemSlot(); + slots[i] = new ItemSlot(this); } #if CLIENT @@ -315,6 +301,60 @@ namespace Barotrauma #endif } + protected IEnumerable GetAllItems(bool checkForDuplicates) + { + for (int i = 0; i < capacity; i++) + { + foreach (var item in slots[i].Items) + { + if (item == null) + { +#if DEBUG + DebugConsole.ThrowError($"Null item in inventory {Owner.ToString() ?? "null"}, slot {i}!"); +#endif + continue; + } + + if (checkForDuplicates) + { + bool duplicateFound = false; + for (int j = 0; j < i; j++) + { + if (slots[j].Items.Contains(item)) + { + duplicateFound = true; + break; + } + } + if (!duplicateFound) { yield return item; } + } + else + { + yield return item; + } + } + } + } + + private void NotifyItemComponentsOfChange() + { +#if CLIENT + if (Owner is Character character && character == Character.Controlled) + { + character.SelectedItem?.GetComponent()?.OnViewUpdateProjSpecific(); + } +#endif + + if (Owner is not Item it) { return; } + + foreach (var c in it.Components) + { + c.OnInventoryChanged(); + } + + it.ParentInventory?.NotifyItemComponentsOfChange(); + } + /// /// Is the item contained in this inventory. Does not recursively check items inside items. /// @@ -367,6 +407,13 @@ namespace Barotrauma return slots[index].Items; } + public int GetItemStackSlotIndex(Item item, int index) + { + if (index < 0 || index >= slots.Length) { return -1; } + + return slots[index].Items.IndexOf(item); + } + /// /// Find the index of the first slot the item is contained in. /// @@ -534,7 +581,7 @@ namespace Barotrauma var itemInSlot = slots[i].First(); if (itemInSlot.OwnInventory != null && !itemInSlot.OwnInventory.Contains(item) && - (itemInSlot.GetComponent()?.GetMaxStackSize(0) ?? 0) == 1 && + itemInSlot.GetComponent()?.GetMaxStackSize(0) == 1 && itemInSlot.OwnInventory.TrySwapping(0, item, user, createNetworkEvent, swapWholeStack: false)) { return true; @@ -575,7 +622,7 @@ namespace Barotrauma if (removeItem) { - item.Drop(user, setTransform: false); + item.Drop(user, setTransform: false, createNetworkEvent: createNetworkEvent && GameMain.NetworkMember is { IsServer: true }); item.ParentInventory?.RemoveItem(item); } @@ -597,7 +644,7 @@ namespace Barotrauma item.body.BodyType = FarseerPhysics.BodyType.Dynamic; item.SetTransform(item.SimPosition, rotation: 0.0f, findNewHull: false); //update to refresh the interpolated draw rotation and position (update doesn't run on disabled bodies) - item.body.Update(); + item.body.UpdateDrawPosition(interpolate: false); } #if SERVER @@ -624,6 +671,8 @@ namespace Barotrauma } } } + + NotifyItemComponentsOfChange(); } public bool IsEmpty() @@ -648,7 +697,7 @@ namespace Barotrauma { if (!slots[i].Any()) { return false; } var item = slots[i].FirstOrDefault(); - if (slots[i].Items.Count < item.Prefab.MaxStackSize) { return false; } + if (slots[i].Items.Count < item.Prefab.GetMaxStackSize(this)) { return false; } } } else @@ -878,21 +927,35 @@ namespace Barotrauma } } - public virtual void CreateNetworkEvent() + public void CreateNetworkEvent() { if (GameMain.NetworkMember == null) { return; } if (GameMain.NetworkMember.IsClient) { syncItemsDelay = 1.0f; } - if (Owner is Character character) + //split into multiple events because one might not be enough to fit all the items + List slotRanges = new List(); + int startIndex = 0; + int itemCount = 0; + for (int i = 0; i < capacity; i++) { - GameMain.NetworkMember.CreateEntityEvent(character, new Character.InventoryStateEventData()); + int count = slots[i].Items.Count; + if (itemCount + count > MaxItemsPerNetworkEvent || i == capacity - 1) + { + slotRanges.Add(new Range(startIndex, i + 1)); + startIndex = i + 1; + itemCount = 0; + } + itemCount += count; } - else if (Owner is Item item) + + foreach (var slotRange in slotRanges) { - GameMain.NetworkMember.CreateEntityEvent(item, new Item.InventoryStateEventData()); + CreateNetworkEvent(slotRange); } } + protected virtual void CreateNetworkEvent(Range slotRange) { } + public Item FindItem(Func predicate, bool recursive) { Item match = AllItems.FirstOrDefault(predicate); @@ -955,9 +1018,12 @@ namespace Barotrauma visualSlots[n].ShowBorderHighlight(Color.White, 0.1f, 0.4f); if (selectedSlot?.Inventory == this) { selectedSlot.ForceTooltipRefresh = true; } } + syncItemsDelay = 1.0f; #endif CharacterHUD.RecreateHudTextsIfFocused(item); } + + NotifyItemComponentsOfChange(); } /// @@ -989,28 +1055,48 @@ namespace Barotrauma return slots[index].Contains(item); } - public void SharedRead(IReadMessage msg, out List[] newItemIds) + public void SharedRead(IReadMessage msg, List[] receivedItemIds, out bool readyToApply) { - byte slotCount = msg.ReadByte(); - newItemIds = new List[slotCount]; - for (int i = 0; i < slotCount; i++) + byte start = msg.ReadByte(); + byte end = msg.ReadByte(); + + //if we received the first chunk of item IDs, clear the rest + //to ensure we don't have anything outdated in the list - we're about to receive the rest of the IDs next + if (start == 0) { - newItemIds[i] = new List(); - int itemCount = msg.ReadRangedInteger(0, MaxStackSize); - for (int j = 0; j < itemCount; j++) + for (int i = 0; i < capacity; i++) { - newItemIds[i].Add(msg.ReadUInt16()); + receivedItemIds[i] = null; } } + + for (int i = start; i < end; i++) + { + var newItemIds = new List(); + int itemCount = msg.ReadRangedInteger(0, MaxPossibleStackSize); + for (int j = 0; j < itemCount; j++) + { + newItemIds.Add(msg.ReadUInt16()); + } + receivedItemIds[i] = newItemIds; + } + + //if all IDs haven't been received yet (chunked into multiple events?) + //don't apply the state yet + readyToApply = !receivedItemIds.Contains(null); } - public void SharedWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) + public void SharedWrite(IWriteMessage msg, Range slotRange) { - msg.WriteByte((byte)capacity); - for (int i = 0; i < capacity; i++) + int start = slotRange.Start.Value; + int end = slotRange.End.Value; + + msg.WriteByte((byte)start); + msg.WriteByte((byte)end); + for (int i = start; i < end; i++) { - msg.WriteRangedInteger(slots[i].Items.Count, 0, MaxStackSize); - for (int j = 0; j < Math.Min(slots[i].Items.Count, MaxStackSize); j++) + msg.WriteRangedInteger(slots[i].Items.Count, 0, MaxPossibleStackSize); + for (int j = 0; j < Math.Min(slots[i].Items.Count, MaxPossibleStackSize); j++) { var item = slots[i].Items[j]; msg.WriteUInt16(item?.ID ?? (ushort)0); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index dcdedaa7d..53495afc7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -23,7 +23,7 @@ namespace Barotrauma { partial class Item : MapEntity, IDamageable, IIgnorable, ISerializableEntity, IServerPositionSync, IClientSerializable { - public static List ItemList = new List(); + public static readonly List ItemList = new List(); private static readonly HashSet dangerousItems = new HashSet(); @@ -77,17 +77,17 @@ namespace Barotrauma public CampaignMode.InteractionType CampaignInteractionType { get { return campaignInteractionType; } - set - { - if (campaignInteractionType != value) - { - campaignInteractionType = value; - AssignCampaignInteractionTypeProjSpecific(campaignInteractionType); - } - } } - partial void AssignCampaignInteractionTypeProjSpecific(CampaignMode.InteractionType interactionType); + public void AssignCampaignInteractionType(CampaignMode.InteractionType interactionType, IEnumerable targetClients = null) + { + if (campaignInteractionType == interactionType) { return; } + campaignInteractionType = interactionType; + AssignCampaignInteractionTypeProjSpecific(campaignInteractionType, targetClients); + } + + + partial void AssignCampaignInteractionTypeProjSpecific(CampaignMode.InteractionType interactionType, IEnumerable targetClients); public bool Visible = true; @@ -105,6 +105,12 @@ namespace Barotrauma private readonly List drawableComponents; private bool hasComponentsToDraw; + /// + /// Has everything in the item been loaded/instantiated/initialized (basically, can be used to check if the whole constructor/Load method has run). + /// Most commonly used to avoid creating network events when some value changes if the item is being initialized. + /// + public bool FullyInitialized { get; private set; } + public PhysicsBody body; private readonly float originalWaterDragCoefficient; private float? overrideWaterDragCoefficient; @@ -114,6 +120,21 @@ namespace Barotrauma set => overrideWaterDragCoefficient = value; } + /// + /// Can be used by StatusEffects to set the type of the body (if the item has one) + /// + public BodyType BodyType + { + get { return body?.BodyType ?? BodyType.Dynamic; } + set + { + if (body != null) + { + body.BodyType = value; + } + } + } + /// /// Removes the override value -> falls back to using the original value defined in the xml. /// @@ -125,9 +146,10 @@ namespace Barotrauma private bool transformDirty = true; + private static readonly List itemsWithPendingConditionUpdates = new List(); + private float lastSentCondition; private float sendConditionUpdateTimer; - private bool conditionUpdatePending; private float prevCondition; private float condition; @@ -207,7 +229,11 @@ namespace Barotrauma set { parentInventory = value; - if (parentInventory != null) { Container = parentInventory.Owner as Item; } + if (parentInventory != null) + { + Container = parentInventory.Owner as Item; + RemoveFromDroppedStack(allowClientExecute: false); + } #if SERVER PreviousParentInventory = value; #endif @@ -229,7 +255,6 @@ namespace Barotrauma container = value; CheckCleanable(); SetActiveSprite(); - RefreshRootContainer(); } } @@ -247,6 +272,37 @@ namespace Barotrauma set { description = value; } } + private string descriptionTag; + + [Serialize("", IsPropertySaveable.Yes, alwaysUseInstanceValues: true), ConditionallyEditable(ConditionallyEditable.ConditionType.OnlyByStatusEffectsAndNetwork)] + /// + /// Can be used to set a localized description via StatusEffects + /// + public string DescriptionTag + { + get { return descriptionTag; } + set + { + if (value == descriptionTag) { return; } + if (value.IsNullOrEmpty()) + { + descriptionTag = null; + description = null; + } + else + { + description = TextManager.Get(value).Value; + descriptionTag = value; + } + if (FullyInitialized && + SerializableProperties != null && + SerializableProperties.TryGetValue(nameof(DescriptionTag).ToIdentifier(), out SerializableProperty property)) + { + GameMain.NetworkMember?.CreateEntityEvent(this, new ChangePropertyEventData(property, this)); + } + } + } + [Editable, Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public bool NonInteractable { @@ -675,6 +731,8 @@ namespace Barotrauma set { allowStealing = value; } } + public bool IsSalvageMissionItem; + private string originalOutpost; [Serialize("", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public string OriginalOutpost @@ -875,8 +933,8 @@ namespace Barotrauma { get { return allPropertyObjects; } } - - public bool IgnoreByAI(Character character) => HasTag("ignorebyai") || OrderedToBeIgnored && character.IsOnPlayerTeam; + + public bool IgnoreByAI(Character character) => HasTag(Barotrauma.Tags.ItemIgnoredByAI) || OrderedToBeIgnored && character.IsOnPlayerTeam; public bool OrderedToBeIgnored { get; set; } public bool HasBallastFloraInHull @@ -896,6 +954,9 @@ namespace Barotrauma } } + public bool InPlayerSubmarine => Submarine?.Info is { IsPlayer: true }; + public bool InBeaconStation => Submarine?.Info is { Type: SubmarineType.BeaconStation }; + public bool IsLadder { get; } public bool IsSecondaryItem { get; } @@ -910,6 +971,11 @@ namespace Barotrauma } } + /// + /// Timing.TotalTimeUnpaused when some character was last eating the item + /// + public float LastEatenTime { get; set; } + public Action OnDeselect; public Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine, ushort id = Entity.NullEntityID, bool callOnItemLoaded = true) @@ -1157,10 +1223,15 @@ namespace Barotrauma DebugConsole.Log("Created " + Name + " (" + ID + ")"); if (Components.Any(ic => ic is Wire) && Components.All(ic => ic is Wire || ic is Holdable)) { isWire = true; } - if (HasTag("logic")) { isLogic = true; } + if (HasTag(Barotrauma.Tags.LogicItem)) { isLogic = true; } ApplyStatusEffects(ActionType.OnSpawn, 1.0f); RecalculateConditionValues(); + + if (callOnItemLoaded) + { + FullyInitialized = true; + } #if CLIENT Submarine.ForceVisibilityRecheck(); #endif @@ -1179,7 +1250,7 @@ namespace Barotrauma }; foreach (KeyValuePair property in SerializableProperties) { - if (!property.Value.Attributes.OfType().Any()) continue; + if (property.Value.Attributes.OfType().None()) { continue; } clone.SerializableProperties[property.Key].TrySetValue(clone, property.Value.GetValue(this)); } @@ -1194,9 +1265,10 @@ namespace Barotrauma for (int i = 0; i < components.Count && i < clone.components.Count; i++) { - foreach (KeyValuePair property in components[i].SerializableProperties) + //order the properties to get them to be applied in a consistent order (may matter for properties that are interconnected somehow, like IsOn/IsActive) + foreach (KeyValuePair property in components[i].SerializableProperties.OrderBy(s => s.Key)) { - if (!property.Value.Attributes.OfType().Any()) continue; + if (property.Value.Attributes.OfType().None()) { continue; } clone.components[i].SerializableProperties[property.Key].TrySetValue(clone.components[i], property.Value.GetValue(components[i])); } @@ -1219,18 +1291,48 @@ namespace Barotrauma if (FlippedX) clone.FlipX(false); if (FlippedY) clone.FlipY(false); - + foreach (ItemComponent component in clone.components) { component.OnItemLoaded(); } - foreach (Item containedItem in ContainedItems) + Dictionary clonedContainedItems = new(); + + for (int i = 0; i < components.Count && i < clone.components.Count; i++) { - var containedClone = containedItem.Clone(); - clone.ownInventory.TryPutItem(containedClone as Item, null); + ItemComponent component = components[i], + cloneComp = clone.components[i]; + + if (component is not ItemContainer origInv || + cloneComp is not ItemContainer cloneInv) + { + continue; + } + + foreach (var containedItem in origInv.Inventory.AllItems) + { + var containedClone = (Item)containedItem.Clone(); + + cloneInv.Inventory.TryPutItem(containedClone, null); + clonedContainedItems.Add(containedItem.ID, containedClone); + } } - + + for (int i = 0; i < components.Count && i < clone.components.Count; i++) + { + ItemComponent component = components[i], + cloneComp = clone.components[i]; + + if (component is not CircuitBox origBox || cloneComp is not CircuitBox cloneBox) + { + continue; + } + + cloneBox.CloneFrom(origBox, clonedContainedItems); + } + + clone.FullyInitialized = true; return clone; } @@ -1432,7 +1534,7 @@ namespace Barotrauma var pickable = GetComponent(); if (pickable != null && !pickable.IsAttached && Prefab.PreferredContainers.Any() && - (container == null || container.HasTag("allowcleanup"))) + (container == null || container.HasTag(Barotrauma.Tags.AllowCleanup))) { if (!cleanableItems.Contains(this)) { @@ -1638,12 +1740,6 @@ namespace Barotrauma tags.Add(tag); } - - public bool HasTag(string tag) - { - return HasTag(tag.ToIdentifier()); - } - public bool HasTag(Identifier tag) { if (tag == null) { return true; } @@ -1690,7 +1786,7 @@ namespace Barotrauma public bool ConditionalMatches(PropertyConditional conditional) { - if (string.IsNullOrEmpty(conditional.TargetItemComponentName)) + if (string.IsNullOrEmpty(conditional.TargetItemComponent)) { if (!conditional.Matches(this)) { return false; } } @@ -1698,7 +1794,7 @@ namespace Barotrauma { foreach (ItemComponent component in components) { - if (component.Name != conditional.TargetItemComponentName) { continue; } + if (component.Name != conditional.TargetItemComponent) { continue; } if (!conditional.Matches(component)) { return false; } } } @@ -1796,7 +1892,7 @@ namespace Barotrauma } if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs)) { - targets.AddRange(character.AnimController.Limbs.ToList()); + targets.AddRange(character.AnimController.Limbs); } if (effect.HasTargetType(StatusEffect.TargetType.Limb) && limb == null && effect.targetLimbs != null) { @@ -1811,7 +1907,7 @@ namespace Barotrauma targets.Add(limb); } - if (Container != null && effect.HasTargetType(StatusEffect.TargetType.Parent)) { targets.Add(Container); } + if (Container != null && effect.HasTargetType(StatusEffect.TargetType.Parent)) { targets.AddRange(Container.AllPropertyObjects); } effect.Apply(type, deltaTime, this, targets, worldPosition); } @@ -1897,21 +1993,20 @@ namespace Barotrauma if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { - if (Math.Abs(lastSentCondition - condition) > 1.0f) - { - conditionUpdatePending = true; - isActive = true; - } - else if (wasInFullCondition != IsFullCondition) - { - conditionUpdatePending = true; - isActive = true; - } - else if (!MathUtils.NearlyEqual(lastSentCondition, condition) && (condition <= 0.0f || condition >= MaxCondition)) + bool needsConditionUpdate = false; + if (!MathUtils.NearlyEqual(lastSentCondition, condition) && (condition <= 0.0f || condition >= MaxCondition)) { + //send the update immediately if the condition changed to max or min sendConditionUpdateTimer = 0.0f; - conditionUpdatePending = true; - isActive = true; + needsConditionUpdate = true; + } + else if (Math.Abs(lastSentCondition - condition) > 1.0f || wasInFullCondition != IsFullCondition) + { + needsConditionUpdate = true; + } + if (needsConditionUpdate && !itemsWithPendingConditionUpdates.Contains(this)) + { + itemsWithPendingConditionUpdates.Add(this); } } @@ -1967,12 +2062,16 @@ namespace Barotrauma public void SendPendingNetworkUpdates() { if (!(GameMain.NetworkMember is { IsServer: true })) { return; } - if (!conditionUpdatePending) { return; } + if (!itemsWithPendingConditionUpdates.Contains(this)) { return; } + SendPendingNetworkUpdatesInternal(); + itemsWithPendingConditionUpdates.Remove(this); + } + private void SendPendingNetworkUpdatesInternal() + { CreateStatusEvent(loadingRound: false); lastSentCondition = condition; sendConditionUpdateTimer = NetConfig.ItemConditionUpdateInterval; - conditionUpdatePending = false; } public void CreateStatusEvent(bool loadingRound) @@ -1980,21 +2079,32 @@ namespace Barotrauma GameMain.NetworkMember.CreateEntityEvent(this, new ItemStatusEventData(loadingRound)); } + public static void UpdatePendingConditionUpdates(float deltaTime) + { + if (GameMain.NetworkMember is not { IsServer: true }) { return; } + for (int i = 0; i < itemsWithPendingConditionUpdates.Count; i++) + { + var item = itemsWithPendingConditionUpdates[i]; + if (item == null || item.Removed) + { + itemsWithPendingConditionUpdates.RemoveAt(i--); + continue; + } + if (item.Submarine is { Loading: true }) { continue; } + + item.sendConditionUpdateTimer -= deltaTime; + if (item.sendConditionUpdateTimer <= 0.0f) + { + item.SendPendingNetworkUpdatesInternal(); + itemsWithPendingConditionUpdates.RemoveAt(i--); + } + } + } + private bool isActive = true; public override void Update(float deltaTime, Camera cam) { -#if SERVER - if (!(Submarine is { Loading: true })) - { - sendConditionUpdateTimer -= deltaTime; - if (conditionUpdatePending && sendConditionUpdateTimer <= 0.0f) - { - SendPendingNetworkUpdates(); - } - } -#endif - if (!isActive) { return; } if (impactQueue != null) @@ -2004,14 +2114,27 @@ namespace Barotrauma HandleCollision(impact); } } + if (isDroppedStackOwner && body != null) + { + foreach (var item in droppedStack) + { + if (item != this) + { + item.body.Enabled = false; + item.body.SetTransformIgnoreContacts(this.SimPosition, body.Rotation); + } + } + } if (aiTarget != null && aiTarget.NeedsUpdate) { aiTarget.Update(deltaTime); } + var containedEffectType = parentInventory == null ? ActionType.OnNotContained : ActionType.OnContained; + ApplyStatusEffects(ActionType.Always, deltaTime, character: (parentInventory as CharacterInventory)?.Owner as Character); - ApplyStatusEffects(parentInventory == null ? ActionType.OnNotContained : ActionType.OnContained, deltaTime, character: (parentInventory as CharacterInventory)?.Owner as Character); + ApplyStatusEffects(containedEffectType, deltaTime, character: (parentInventory as CharacterInventory)?.Owner as Character); for (int i = 0; i < updateableComponents.Count; i++) { @@ -2019,7 +2142,7 @@ namespace Barotrauma if (ic.IsActiveConditionals != null) { - if (ic.IsActiveConditionalComparison == PropertyConditional.Comparison.And) + if (ic.IsActiveConditionalComparison == PropertyConditional.LogicalOperatorType.And) { bool shouldBeActive = true; foreach (var conditional in ic.IsActiveConditionals) @@ -2140,6 +2263,7 @@ namespace Barotrauma updateableComponents.Count == 0 && (aiTarget == null || !aiTarget.NeedsUpdate) && !hasStatusEffectsOfType[(int)ActionType.Always] && + !hasStatusEffectsOfType[(int)containedEffectType] && (body == null || !body.Enabled)) { #if CLIENT @@ -2651,9 +2775,20 @@ namespace Barotrauma public bool TryInteract(Character user, bool ignoreRequiredItems = false, bool forceSelectKey = false, bool forceUseKey = false) { - if (CampaignMode.BlocksInteraction(CampaignInteractionType)) + var campaignInteractionType = CampaignInteractionType; +#if SERVER + var ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == user); + if (ownerClient != null) { - return false; + if (!campaignInteractionTypePerClient.TryGetValue(ownerClient, out campaignInteractionType)) + { + campaignInteractionType = CampaignMode.InteractionType.None; + } + } +#endif + if (CampaignMode.BlocksInteraction(campaignInteractionType)) + { + return false; } bool picked = false, selected = false; @@ -2821,9 +2956,9 @@ namespace Barotrauma return -1; } - public void Use(float deltaTime, Character character = null, Limb targetLimb = null) + public void Use(float deltaTime, Character user = null, Limb targetLimb = null, Entity useTarget = null) { - if (RequireAimToUse && (character == null || !character.IsKeyDown(InputType.Aim))) + if (RequireAimToUse && (user == null || !user.IsKeyDown(InputType.Aim))) { return; } @@ -2836,17 +2971,17 @@ namespace Barotrauma { bool isControlled = false; #if CLIENT - isControlled = character == Character.Controlled; + isControlled = user == Character.Controlled; #endif - if (!ic.HasRequiredContainedItems(character, isControlled)) { continue; } - if (ic.Use(deltaTime, character)) + if (!ic.HasRequiredContainedItems(user, isControlled)) { continue; } + if (ic.Use(deltaTime, user)) { ic.WasUsed = true; #if CLIENT - ic.PlaySound(ActionType.OnUse, character); + ic.PlaySound(ActionType.OnUse, user); #endif - ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, character, targetLimb, useTarget: character, user: character); - + ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, user, targetLimb, useTarget: useTarget, user: user); + ic.OnUsed.Invoke(new ItemComponent.ItemUseInfo(this, user)); if (ic.DeleteOnUse) { remove = true; } } } @@ -2876,7 +3011,7 @@ namespace Barotrauma #if CLIENT ic.PlaySound(ActionType.OnSecondaryUse, character); #endif - ic.ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, character: character, user: character); + ic.ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, character: character, user: character, useTarget: character); if (ic.DeleteOnUse) { remove = true; } } @@ -3009,10 +3144,10 @@ namespace Barotrauma Container = null; } - if (parentInventory != null) + if (ParentInventory != null) { - parentInventory.RemoveItem(this); - parentInventory = null; + ParentInventory.RemoveItem(this); + ParentInventory = null; } SetContainedItemPositions(); @@ -3021,6 +3156,108 @@ namespace Barotrauma #endif } + + private List droppedStack; + public IEnumerable DroppedStack => droppedStack ?? Enumerable.Empty(); + + private bool isDroppedStackOwner; + + /// + /// "Merges" the set of items so they behave as one physical object and can be picked up by clicking once. + /// The items need to be instances of the same prefab and have a physics body. + /// + public void CreateDroppedStack(IEnumerable items, bool allowClientExecute) + { + if (!allowClientExecute && GameMain.NetworkMember is { IsClient: true }) { return; } + + int itemCount = items.Count(); + + if (itemCount == 1) { return; } + + if (items.DistinctBy(it => it.Prefab).Count() > 1) + { + DebugConsole.ThrowError($"Attempted to create a dropped stack of multiple different items ({string.Join(", ", items.DistinctBy(it => it.Prefab))})\n{Environment.StackTrace}"); + return; + } + if (items.Any(it => it.body == null)) + { + DebugConsole.ThrowError($"Attempted to create a dropped stack for an item with no body ({items.First().Prefab.Identifier})\n{Environment.StackTrace}"); + return; + } + if (items.None()) + { + DebugConsole.ThrowError($"Attempted to create a dropped stack of an empty list of items.\n{Environment.StackTrace}"); + return; + } + + int maxStackSize = items.First().Prefab.MaxStackSize; + if (itemCount > maxStackSize) + { + for (int i = 0; i < MathF.Ceiling(itemCount / maxStackSize); i++) + { + int startIndex = i * maxStackSize; + items.ElementAt(startIndex).CreateDroppedStack(items.Skip(startIndex).Take(maxStackSize), allowClientExecute); + } + } + else + { + droppedStack ??= new List(); + foreach (Item item in items) + { + if (!droppedStack.Contains(item)) + { + droppedStack.Add(item); + } + } + SetDroppedStackItemStates(); +#if SERVER + if (GameMain.NetworkMember is { IsServer: true } server) + { + server.CreateEntityEvent(this, new DroppedStackEventData(droppedStack)); + } +#endif + } + } + + private void RemoveFromDroppedStack(bool allowClientExecute) + { + if (!allowClientExecute && GameMain.NetworkMember is { IsClient: true }) { return; } + if (droppedStack == null) { return; } + + body.Enabled = ParentInventory == null; + isDroppedStackOwner = false; + droppedStack.Remove(this); + SetDroppedStackItemStates(); + droppedStack = null; +#if SERVER + if (GameMain.NetworkMember is { IsServer: true } server) + { + server.CreateEntityEvent(this, new DroppedStackEventData(Enumerable.Empty())); + } +#endif + } + + private void SetDroppedStackItemStates() + { + if (droppedStack == null) { return; } + bool isFirst = true; + foreach (Item item in droppedStack) + { + item.droppedStack = droppedStack; + item.isDroppedStackOwner = isFirst; + if (item.body != null) + { + item.body.Enabled = item.body.PhysEnabled = isFirst; + if (isFirst) + { + item.isActive = true; + item.body.ResetDynamics(); + } + } + isFirst = false; + } + } + public void Equip(Character character) { if (Removed) @@ -3072,7 +3309,7 @@ namespace Barotrauma if (allProperties.Count > 1) { int propertyIndex = allProperties.FindIndex(p => p.property == property && p.obj == entity); - if (propertyIndex < -1) + if (propertyIndex < 0) { throw new Exception($"Could not find the property \"{property.Name}\" in \"{entity.Name ?? "null"}\""); } @@ -3187,6 +3424,11 @@ namespace Barotrauma propertyIndex = (int)msg.ReadVariableUInt32(); } + if (propertyIndex >= allProperties.Count || propertyIndex < 0) + { + throw new Exception($"Error in ReadPropertyChange. Property index out of bounds (index: {propertyIndex}, property count: {allProperties.Count}, in-game editable only: {inGameEditableOnly})"); + } + bool allowEditing = true; object parentObject = allProperties[propertyIndex].obj; SerializableProperty property = allProperties[propertyIndex].property; @@ -3598,7 +3840,9 @@ namespace Barotrauma if (component.Parent != null) { component.IsActive = component.Parent.IsActive; } component.OnItemLoaded(); } - + + item.FullyInitialized = true; + return item; } @@ -3811,6 +4055,7 @@ namespace Barotrauma repairableItems.Remove(this); sonarVisibleItems.Remove(this); cleanableItems.Remove(this); + RemoveFromDroppedStack(allowClientExecute: true); } partial void RemoveProjSpecific(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs index 010a675b2..fecfef994 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; using Barotrauma.Items.Components; using Barotrauma.Networking; using Microsoft.Xna.Framework; @@ -20,9 +22,10 @@ namespace Barotrauma ApplyStatusEffect = 7, Upgrade = 8, ItemStat = 9, - + DroppedStack = 10, + MinValue = 0, - MaxValue = 9 + MaxValue = 10 } public interface IEventData : NetEntityEvent.IData @@ -47,10 +50,12 @@ namespace Barotrauma { public EventType EventType => EventType.InventoryState; public readonly ItemContainer Component; + public readonly Range SlotRange; - public InventoryStateEventData(ItemContainer component) + public InventoryStateEventData(ItemContainer component, Range slotRange) { Component = component; + SlotRange = slotRange; } } @@ -62,6 +67,10 @@ namespace Barotrauma public ChangePropertyEventData(SerializableProperty serializableProperty, ISerializableEntity entity) { + if (serializableProperty.GetAttribute() == null) + { + DebugConsole.ThrowError($"Attempted to create {nameof(ChangePropertyEventData)} for the non-editable property {serializableProperty.Name}."); + } SerializableProperty = serializableProperty; Entity = entity; } @@ -94,6 +103,13 @@ namespace Barotrauma private readonly struct AssignCampaignInteractionEventData : IEventData { public EventType EventType => EventType.AssignCampaignInteraction; + + public readonly ImmutableArray TargetClients; + + public AssignCampaignInteractionEventData(IEnumerable targetClients) + { + TargetClients = (targetClients ?? Enumerable.Empty()).ToImmutableArray(); + } } public readonly struct ApplyStatusEffectEventData : IEventData diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs index a25062e49..0b8aea197 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs @@ -63,7 +63,7 @@ namespace Barotrauma if (itemPrefab == null) { return 0; } if (i < 0 || i >= slots.Length) { return 0; } if (!container.CanBeContained(itemPrefab, i)) { return 0; } - return slots[i].HowManyCanBePut(itemPrefab, maxStackSize: Math.Min(itemPrefab.MaxStackSize, container.GetMaxStackSize(i)), condition); + return slots[i].HowManyCanBePut(itemPrefab, maxStackSize: Math.Min(itemPrefab.GetMaxStackSize(this), container.GetMaxStackSize(i)), condition); } public override bool IsFull(bool takeStacksIntoAccount = false) @@ -74,7 +74,7 @@ namespace Barotrauma { if (!slots[i].Any()) { return false; } var item = slots[i].FirstOrDefault(); - if (slots[i].Items.Count < Math.Min(item.Prefab.MaxStackSize, container.GetMaxStackSize(i))) { return false; } + if (slots[i].Items.Count < Math.Min(item.Prefab.GetMaxStackSize(this), container.GetMaxStackSize(i))) { return false; } } } else @@ -133,7 +133,7 @@ namespace Barotrauma return wasPut; } - public override void CreateNetworkEvent() + protected override void CreateNetworkEvent(Range slotRange) { if (!Item.ItemList.Contains(container.Item)) { @@ -150,11 +150,17 @@ namespace Barotrauma DebugConsole.Log("Creating a network event for the item \"" + container.Item + "\" failed, ItemContainer not found in components"); return; } - + + if (slotRange.Start.Value < 0 || slotRange.End.Value > capacity) + { + DebugConsole.ThrowError($"Error when creating an inventory event: invalid slot range ({slotRange})\n" + Environment.StackTrace); + return; + } + if (GameMain.NetworkMember != null) { if (GameMain.NetworkMember.IsClient) { syncItemsDelay = 1.0f; } - GameMain.NetworkMember.CreateEntityEvent(Owner as INetSerializable, new Item.InventoryStateEventData(container)); + GameMain.NetworkMember.CreateEntityEvent(Owner as INetSerializable, new Item.InventoryStateEventData(container, slotRange)); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index af2e67a85..27e58824a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -43,9 +43,9 @@ namespace Barotrauma public readonly bool CopyCondition; //tag/identifier of the deconstructor(s) that can be used to deconstruct the item into this - public readonly string[] RequiredDeconstructor; + public readonly Identifier[] RequiredDeconstructor; //tag/identifier of other item(s) that that need to be present in the deconstructor to deconstruct the item into this - public readonly string[] RequiredOtherItem; + public readonly Identifier[] RequiredOtherItem; //text to display on the deconstructor's activate button when this output is available public readonly string ActivateButtonText; public readonly string InfoText; @@ -63,9 +63,9 @@ namespace Barotrauma OutConditionMax = element.GetAttributeFloat("outconditionmax", element.GetAttributeFloat("outcondition", 1.0f)); CopyCondition = element.GetAttributeBool("copycondition", false); Commonness = element.GetAttributeFloat("commonness", 1.0f); - RequiredDeconstructor = element.GetAttributeStringArray("requireddeconstructor", - element.Parent?.GetAttributeStringArray("requireddeconstructor", Array.Empty()) ?? Array.Empty()); - RequiredOtherItem = element.GetAttributeStringArray("requiredotheritem", Array.Empty()); + RequiredDeconstructor = element.GetAttributeIdentifierArray("requireddeconstructor", + element.Parent?.GetAttributeIdentifierArray("requireddeconstructor", Array.Empty()) ?? Array.Empty()); + RequiredOtherItem = element.GetAttributeIdentifierArray("requiredotheritem", Array.Empty()); ActivateButtonText = element.GetAttributeString("activatebuttontext", string.Empty); InfoText = element.GetAttributeString("infotext", string.Empty); InfoTextOnOtherItemMissing = element.GetAttributeString("infotextonotheritemmissing", string.Empty); @@ -88,20 +88,27 @@ namespace Barotrauma public abstract ItemPrefab FirstMatchingPrefab { get; } + public LocalizedString OverrideHeader { get; } public LocalizedString OverrideDescription { get; } - public RequiredItem(int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription) + public RequiredItem(int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription, LocalizedString overrideHeader, Identifier defaultItem) { Amount = amount; MinCondition = minCondition; MaxCondition = maxCondition; UseCondition = useCondition; + OverrideHeader = overrideHeader; OverrideDescription = overrideDescription; + DefaultItem = defaultItem; } public readonly int Amount; public readonly float MinCondition; public readonly float MaxCondition; public readonly bool UseCondition; + /// + /// Used only when there's multiple optional items. + /// + public readonly Identifier DefaultItem; public bool IsConditionSuitable(float conditionPercentage) { @@ -138,8 +145,8 @@ namespace Barotrauma return item?.Prefab.Identifier == ItemPrefabIdentifier; } - public RequiredItemByIdentifier(Identifier itemPrefab, int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription) : - base(amount, minCondition, maxCondition, useCondition, overrideDescription) + public RequiredItemByIdentifier(Identifier itemPrefab, int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription, LocalizedString overrideHeader) : + base(amount, minCondition, maxCondition, useCondition, overrideDescription, overrideHeader, defaultItem: Identifier.Empty) { ItemPrefabIdentifier = itemPrefab; using MD5 md5 = MD5.Create(); @@ -158,9 +165,26 @@ namespace Barotrauma public override UInt32 UintIdentifier { get; } - public override IEnumerable ItemPrefabs => ItemPrefab.Prefabs.Where(p => p.Tags.Contains(Tag)); + private readonly List cachedPrefabs = new List(); - public override ItemPrefab FirstMatchingPrefab => ItemPrefab.Prefabs.FirstOrDefault(p => p.Tags.Contains(Tag)); + private Md5Hash prevContentPackagesHash; + + public override IEnumerable ItemPrefabs + { + get + { + if (prevContentPackagesHash == null || + !prevContentPackagesHash.Equals(ContentPackageManager.EnabledPackages.MergedHash)) + { + cachedPrefabs.Clear(); + cachedPrefabs.AddRange(ItemPrefab.Prefabs.Where(p => p.Tags.Contains(Tag))); + prevContentPackagesHash = ContentPackageManager.EnabledPackages.MergedHash; + } + return cachedPrefabs; + } + } + + public override ItemPrefab FirstMatchingPrefab => ItemPrefabs.FirstOrDefault(); public override bool MatchesItem(Item item) { @@ -168,8 +192,8 @@ namespace Barotrauma return item.HasTag(Tag); } - public RequiredItemByTag(Identifier tag, int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription) - : base(amount, minCondition, maxCondition, useCondition, overrideDescription) + public RequiredItemByTag(Identifier tag, int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription, LocalizedString overrideHeader, Identifier defaultItem) + : base(amount, minCondition, maxCondition, useCondition, overrideDescription, overrideHeader, defaultItem) { Tag = tag; using MD5 md5 = MD5.Create(); @@ -186,7 +210,7 @@ namespace Barotrauma public ItemPrefab TargetItem => ItemPrefab.Prefabs[TargetItemPrefabIdentifier]; private readonly Lazy displayName; - public LocalizedString DisplayName + public LocalizedString DisplayName => ItemPrefab.Prefabs.ContainsKey(TargetItemPrefabIdentifier) ? displayName.Value : ""; public readonly ImmutableArray RequiredItems; public readonly ImmutableArray SuitableFabricatorIdentifiers; @@ -198,6 +222,7 @@ namespace Barotrauma public readonly uint RecipeHash; public readonly int Amount; public readonly int? Quality; + public readonly bool HideForNonTraitors; /// /// How many of this item the fabricator can create (< 0 = unlimited) @@ -230,6 +255,8 @@ namespace Barotrauma FabricationLimitMin = element.GetAttributeInt(nameof(FabricationLimitMin), limitDefault); FabricationLimitMax = element.GetAttributeInt(nameof(FabricationLimitMax), limitDefault); + HideForNonTraitors = element.GetAttributeBool(nameof(HideForNonTraitors), false); + if (element.GetAttribute(nameof(Quality)) != null) { Quality = element.GetAttributeInt(nameof(Quality), 0); @@ -266,10 +293,15 @@ namespace Barotrauma bool useCondition = subElement.GetAttributeBool("usecondition", true); int amount = subElement.GetAttributeInt("count", subElement.GetAttributeInt("amount", 1)); - LocalizedString description = string.Empty; - if (subElement.GetAttributeString("description", string.Empty) is string texTag && !texTag.IsNullOrEmpty()) + LocalizedString overrideDescription = string.Empty; + if (subElement.GetAttributeString("description", string.Empty) is string descriptionTag && !descriptionTag.IsNullOrEmpty()) { - description = TextManager.Get(texTag); + overrideDescription = TextManager.Get(descriptionTag); + } + LocalizedString overrideHeader = string.Empty; + if (subElement.GetAttributeString("header", string.Empty) is string headerTag && !headerTag.IsNullOrEmpty()) + { + overrideHeader = TextManager.Get(headerTag); } if (requiredItemIdentifier != Identifier.Empty) @@ -284,7 +316,7 @@ namespace Barotrauma amount += requiredItems[existing].Amount; requiredItems.RemoveAt(existing); } - requiredItems.Add(new RequiredItemByIdentifier(requiredItemIdentifier, amount, minCondition, maxCondition, useCondition, description)); + requiredItems.Add(new RequiredItemByIdentifier(requiredItemIdentifier, amount, minCondition, maxCondition, useCondition, overrideDescription, overrideHeader)); } else { @@ -298,7 +330,8 @@ namespace Barotrauma amount += requiredItems[existing].Amount; requiredItems.RemoveAt(existing); } - requiredItems.Add(new RequiredItemByTag(requiredItemTag, amount, minCondition, maxCondition, useCondition, description)); + Identifier defaultItem = subElement.GetAttributeIdentifier("defaultitem", Identifier.Empty); + requiredItems.Add(new RequiredItemByTag(requiredItemTag, amount, minCondition, maxCondition, useCondition, overrideDescription, overrideHeader, defaultItem)); } break; } @@ -316,12 +349,13 @@ namespace Barotrauma uint outputId = ToolBox.IdentifierToUint32Hash(TargetItemPrefabIdentifier, md5); var requiredItems = string.Join(':', RequiredItems - .Select(i => i.UintIdentifier) - .Select(i => string.Join(',', i))); + .Select(static i => $"{i.UintIdentifier}:{i.Amount}") + .Select(static i => string.Join(',', i))); + // above but include the item amount var requiredSkills = string.Join(':', RequiredSkills.Select(s => $"{s.Identifier}:{s.Level}")); - uint retVal = ToolBox.StringToUInt32Hash($"{Amount}|{outputId}|{RequiredTime}|{requiredItems}|{requiredSkills}", md5); + uint retVal = ToolBox.StringToUInt32Hash($"{Amount}|{outputId}|{RequiredTime}|{RequiresRecipe}|{requiredItems}|{requiredSkills}", md5); if (retVal == 0) { retVal = 1; } return retVal; } @@ -681,6 +715,9 @@ namespace Barotrauma [Serialize(0.0f, IsPropertySaveable.No)] public float OffsetOnSelected { get; private set; } + [Serialize(false, IsPropertySaveable.No, description: "Should the character who's selected the item grab it (hold their hand on it, the same way as they do when repairing)? Defaults to true on items that have an ItemContainer component.")] + public bool GrabWhenSelected { get; set; } + private float health; [Serialize(100.0f, IsPropertySaveable.No)] @@ -749,7 +786,7 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.No)] public bool DisableItemUsageWhenSelected { get; private set; } - [Serialize("", IsPropertySaveable.No)] + [Serialize("metalcrate", IsPropertySaveable.No)] public string CargoContainerIdentifier { get; private set; } [Serialize(false, IsPropertySaveable.No)] @@ -802,7 +839,40 @@ namespace Barotrauma public int MaxStackSize { get { return maxStackSize; } - private set { maxStackSize = MathHelper.Clamp(value, 1, Inventory.MaxStackSize); } + private set { maxStackSize = MathHelper.Clamp(value, 1, Inventory.MaxPossibleStackSize); } + } + + private int maxStackSizeCharacterInventory; + [Serialize(-1, IsPropertySaveable.No)] + public int MaxStackSizeCharacterInventory + { + get { return maxStackSizeCharacterInventory; } + private set { maxStackSizeCharacterInventory = Math.Min(value, Inventory.MaxPossibleStackSize); } + } + + private int maxStackSizeHoldableOrWearableInventory; + [Serialize(-1, IsPropertySaveable.No)] + public int MaxStackSizeHoldableOrWearableInventory + { + get { return maxStackSizeHoldableOrWearableInventory; } + private set { maxStackSizeHoldableOrWearableInventory = Math.Min(value, Inventory.MaxPossibleStackSize); } + } + + public int GetMaxStackSize(Inventory inventory) + { + if (inventory is CharacterInventory && maxStackSizeCharacterInventory > 0) + { + return maxStackSizeCharacterInventory; + } + else if (maxStackSizeHoldableOrWearableInventory > 0 && + inventory?.Owner is Item item && (item.GetComponent() != null || item.GetComponent() != null)) + { + return maxStackSizeHoldableOrWearableInventory; + } + else + { + return maxStackSize; + } } [Serialize(false, IsPropertySaveable.No)] @@ -830,13 +900,19 @@ namespace Barotrauma [Serialize(1.0f, IsPropertySaveable.No, description: "How much the bots prioritize shooting this item with slow turrets, like railguns? Defaults to 1. Not used if AITurretPriority is 0. Distance to the target affects the decision making.")] public float AISlowTurretPriority { get; private set; } - + [Serialize(float.PositiveInfinity, IsPropertySaveable.No, description: "The max distance at which the bots are allowed to target the items. Defaults to infinity.")] public float AITurretTargetingMaxDistance { get; private set; } [Serialize(false, IsPropertySaveable.Yes, description: "If enabled, taking items from this container is never considered stealing.")] public bool AllowStealingContainedItems { get; private set; } + [Serialize("255,255,255,255", IsPropertySaveable.No, description: "Used in circuit box to set the color of the nodes.")] + public Color SignalComponentColor { get; private set; } + + [Serialize(false, IsPropertySaveable.No, description: "If enabled, the player is unable to open the middle click menu when this item is selected.")] + public bool DisableCommandMenuWhenSelected { get; set; } + protected override Identifier DetermineIdentifier(XElement element) { Identifier identifier = base.DetermineIdentifier(element); @@ -1040,7 +1116,7 @@ namespace Barotrauma { DebugConsole.ThrowError( $"Error in item prefab \"{ToString()}\": " + - $"{prevRecipe.DisplayName} has the same hash as {newRecipe.DisplayName}. " + + $"{prevRecipe.TargetItemPrefabIdentifier} has the same hash as {newRecipe.TargetItemPrefabIdentifier}. " + $"This will cause issues with fabrication." ); } @@ -1172,6 +1248,11 @@ namespace Barotrauma #endif this.allowedLinks = ConfigElement.GetAttributeIdentifierArray("allowedlinks", Array.Empty()).ToImmutableHashSet(); + + GrabWhenSelected = ConfigElement.GetAttributeBool( + nameof(GrabWhenSelected), + ConfigElement.GetChildElement(nameof(ItemContainer)) != null && + ConfigElement.GetChildElement("Body") == null); } public CommonnessInfo? GetCommonnessInfo(Level level) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs index c4647ef25..4e65d9b65 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs @@ -43,21 +43,24 @@ namespace Barotrauma } /// - /// Should an empty inventory be considered valid? Can be used to, for example, make an item do something if there's a specific item, or nothing, inside it. + /// Should an empty inventory (or an empty inventory slot if is set) be considered valid? Can be used to, for example, make an item do something if there's a specific item, or nothing, inside it. /// public bool MatchOnEmpty { get; set; } /// - /// Should only an empty inventory be considered valid? Can be used to, for example, make an item do something when there's nothing inside it. + /// Should only an empty inventory (or an empty inventory slot if is set) be considered valid? Can be used to, for example, make an item do something when there's nothing inside it. /// public bool RequireEmpty { get; set; } + private bool RequireOrMatchOnEmpty => MatchOnEmpty || RequireEmpty; + /// /// Only valid for the RequiredItems of an ItemComponent. Can be used to ignore the requirement in the submarine editor, /// making it easier to for example make rewire things that require some special tool to rewire. /// public bool IgnoreInEditor { get; set; } + /// /// Identifier(s) or tag(s) of the items that are NOT considered valid. /// Can be used to, for example, exclude some specific items when using tags that apply to multiple items. @@ -103,6 +106,11 @@ namespace Barotrauma /// public int TargetSlot = -1; + /// + /// The slot type the target must be in when targeting an item contained inside a character's inventory + /// + public InvSlotType CharacterInventorySlotType; + /// /// Overrides the position defined in ItemContainer. Only valid when used in the Containable definitions of an ItemContainer. /// @@ -167,6 +175,10 @@ namespace Barotrauma { if (item.HasTag(excludedIdentifier)) { return false; } } + if (item.ParentInventory?.Owner is Character character && CharacterInventorySlotType != InvSlotType.None) + { + if (!character.HasEquippedItem(item, CharacterInventorySlotType)) { return false; } + } if (Identifiers.Contains(item.Prefab.Identifier)) { return true; } foreach (var identifier in Identifiers) { @@ -261,6 +273,8 @@ namespace Barotrauma Rotation = element.GetAttributeFloat("rotation", 0f); SetActive = element.GetAttributeBool("setactive", false); + CharacterInventorySlotType = element.GetAttributeEnum(nameof(CharacterInventorySlotType), InvSlotType.None); + if (element.GetAttribute(nameof(Hide)) != null) { Hide = element.GetAttributeBool(nameof(Hide), false); @@ -332,7 +346,7 @@ namespace Barotrauma case RelationType.Equipped: if (character == null) { return false; } var heldItems = character.HeldItems; - if ((RequireEmpty || MatchOnEmpty) && heldItems.None()) { return true; } + if (RequireOrMatchOnEmpty && heldItems.None()) { return true; } foreach (Item equippedItem in heldItems) { if (equippedItem == null) { continue; } @@ -347,7 +361,7 @@ namespace Barotrauma if (character == null) { return false; } if (character.Inventory == null) { return MatchOnEmpty || RequireEmpty; } var allItems = character.Inventory.AllItems; - if ((RequireEmpty || MatchOnEmpty) && allItems.None()) { return true; } + if (RequireOrMatchOnEmpty && allItems.None()) { return true; } foreach (Item pickedItem in allItems) { if (pickedItem == null) { continue; } @@ -370,11 +384,21 @@ namespace Barotrauma private bool CheckContained(Item parentItem) { if (parentItem.OwnInventory == null) { return false; } - bool isEmpty = parentItem.OwnInventory.IsEmpty(); - if (RequireEmpty && !isEmpty) { return false; } - if (MatchOnEmpty && isEmpty) { return true; } + + if (TargetSlot == -1 && RequireOrMatchOnEmpty) + { + bool isEmpty = parentItem.OwnInventory.IsEmpty(); + if (RequireEmpty) { return isEmpty; } + if (MatchOnEmpty && isEmpty) { return true; } + } foreach (var container in parentItem.GetComponents()) { + if (TargetSlot > -1 && RequireOrMatchOnEmpty) + { + var itemInSlot = container.Inventory.GetItemAt(TargetSlot); + if (RequireEmpty) { return itemInSlot == null; } + if (MatchOnEmpty && itemInSlot == null) { return true; } + } foreach (Item contained in container.Inventory.AllItems) { if (TargetSlot > -1 && parentItem.OwnInventory.FindIndex(contained) != TargetSlot) { continue; } @@ -389,7 +413,8 @@ namespace Barotrauma { element.Add( new XAttribute("items", JoinedIdentifiers), - new XAttribute("type", type.ToString()), + new XAttribute("type", type.ToString()), + new XAttribute("characterinventoryslottype", CharacterInventorySlotType.ToString()), new XAttribute("optional", IsOptional), new XAttribute("ignoreineditor", IgnoreInEditor), new XAttribute("excludebroken", ExcludeBroken), diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/StartItemSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/StartItemSet.cs index a8c23fb81..bafe8e15d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/StartItemSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/StartItemSet.cs @@ -9,11 +9,13 @@ namespace Barotrauma { public Identifier Item; public int Amount; - + public bool MultiPlayerOnly; + public StartItem(XElement element) { Item = element.GetAttributeIdentifier("identifier", Identifier.Empty); - Amount = element.GetAttributeInt("amount", 1); + Amount = element.GetAttributeInt(nameof(Amount), 1); + MultiPlayerOnly = element.GetAttributeBool(nameof(MultiPlayerOnly), false); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index 91797b6ee..dce56e175 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -92,6 +92,11 @@ namespace Barotrauma /// private bool flash; + /// + /// Whether a debris particle effect is created when the explosion happens. + /// + private bool debris; + /// /// Whether a underwater bubble particle effect is created when the explosion happens. /// @@ -120,12 +125,15 @@ namespace Barotrauma /// /// List of item tags that the explosion ignores when applying fire effects. /// - private readonly string[] ignoreFireEffectsForTags; + private readonly Identifier[] ignoreFireEffectsForTags; /// /// When set to true, the explosion don't deal less damage when the target is behind a solid object. /// - private readonly bool ignoreCover; + public bool IgnoreCover + { + get; set; + } /// /// How long the light source created by the explosion lasts. @@ -185,11 +193,12 @@ namespace Barotrauma this.EmpStrength = empStrength; BallastFloraDamage = ballastFloraStrength; sparks = true; + debris = true; shockwave = true; smoke = true; flames = true; underwaterBubble = true; - ignoreFireEffectsForTags = Array.Empty(); + ignoreFireEffectsForTags = Array.Empty(); } public Explosion(ContentXElement element, string parentDebugName) @@ -205,13 +214,14 @@ namespace Barotrauma flames = element.GetAttributeBool("flames", showEffects); underwaterBubble = element.GetAttributeBool("underwaterbubble", showEffects); smoke = element.GetAttributeBool("smoke", showEffects); + debris = element.GetAttributeBool("debris", false); playTinnitus = element.GetAttributeBool("playtinnitus", showEffects); applyFireEffects = element.GetAttributeBool("applyfireeffects", flames && showEffects); - ignoreFireEffectsForTags = element.GetAttributeStringArray("ignorefireeffectsfortags", Array.Empty(), convertToLowerInvariant: true); + ignoreFireEffectsForTags = element.GetAttributeIdentifierArray("ignorefireeffectsfortags", Array.Empty()); - ignoreCover = element.GetAttributeBool("ignorecover", false); + IgnoreCover = element.GetAttributeBool("ignorecover", false); OnlyInside = element.GetAttributeBool("onlyinside", false); OnlyOutside = element.GetAttributeBool("onlyoutside", false); @@ -243,6 +253,7 @@ namespace Barotrauma shockwave = false; smoke = false; flash = false; + debris = false; flames = false; underwaterBubble = false; } @@ -470,7 +481,7 @@ namespace Barotrauma float distFactor = 1.0f - dist / attack.Range; //solid obstacles between the explosion and the limb reduce the effect of the explosion - if (!ignoreCover) + if (!IgnoreCover) { distFactor *= GetObstacleDamageMultiplier(explosionPos, worldPosition, limb.SimPosition); } @@ -583,7 +594,6 @@ namespace Barotrauma } } - private static readonly List damagedStructureList = new List(); private static readonly Dictionary damagedStructures = new Dictionary(); /// /// Returns a dictionary where the keys are the structures that took damage and the values are the amount of damage taken @@ -591,37 +601,30 @@ namespace Barotrauma public static Dictionary RangedStructureDamage(Vector2 worldPosition, float worldRange, float damage, float levelWallDamage, Character attacker = null, IEnumerable ignoredSubmarines = null, bool emitWallDamageParticles = true) { float dist = 600.0f; - damagedStructureList.Clear(); - foreach (MapEntity entity in MapEntity.mapEntityList) + damagedStructures.Clear(); + foreach (Structure structure in Structure.WallList) { - if (entity is not Structure structure) { continue; } - if (ignoredSubmarines != null && entity.Submarine != null && ignoredSubmarines.Contains(entity.Submarine)) { continue; } + if (ignoredSubmarines != null && structure.Submarine != null && ignoredSubmarines.Contains(structure.Submarine)) { continue; } if (structure.HasBody && !structure.IsPlatform && Vector2.Distance(structure.WorldPosition, worldPosition) < dist * 3.0f) { - damagedStructureList.Add(structure); - } - } - - damagedStructures.Clear(); - foreach (Structure structure in damagedStructureList) - { - for (int i = 0; i < structure.SectionCount; i++) - { - float distFactor = 1.0f - (Vector2.Distance(structure.SectionPosition(i, true), worldPosition) / worldRange); - if (distFactor <= 0.0f) { continue; } - - structure.AddDamage(i, damage * distFactor, attacker, emitParticles: emitWallDamageParticles); - - if (damagedStructures.ContainsKey(structure)) + for (int i = 0; i < structure.SectionCount; i++) { - damagedStructures[structure] += damage * distFactor; - } - else - { - damagedStructures.Add(structure, damage * distFactor); + float distFactor = 1.0f - (Vector2.Distance(structure.SectionPosition(i, true), worldPosition) / worldRange); + if (distFactor <= 0.0f) { continue; } + + structure.AddDamage(i, damage * distFactor, attacker, emitParticles: emitWallDamageParticles); + + if (damagedStructures.ContainsKey(structure)) + { + damagedStructures[structure] += damage * distFactor; + } + else + { + damagedStructures.Add(structure, damage * distFactor); + } } } } @@ -717,7 +720,7 @@ namespace Barotrauma if (body.UserData is Item item) { var door = item.GetComponent(); - if (door != null && !door.IsBroken) { damageMultiplier *= 0.01f; } + if (door != null && !door.IsOpen && !door.IsBroken) { damageMultiplier *= 0.01f; } } else if (body.UserData is Structure structure) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index 77b7a1e08..4c93da0cc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -577,8 +577,8 @@ namespace Barotrauma } else { - hull1.LethalPressure = 0.0f; - hull2.LethalPressure = 0.0f; + hull1.LethalPressure -= Hull.PressureDropSpeed * deltaTime; + hull2.LethalPressure -= Hull.PressureDropSpeed * deltaTime; } } } @@ -642,7 +642,7 @@ namespace Barotrauma } else { - hull1.LethalPressure += ((Submarine != null && Submarine.AtDamageDepth) ? 100.0f : 10.0f) * deltaTime; + hull1.LethalPressure += ((Submarine != null && Submarine.AtDamageDepth) ? 100.0f : Hull.PressureBuildUpSpeed) * deltaTime; } } else @@ -657,7 +657,7 @@ namespace Barotrauma } if (hull1.WaterVolume >= hull1.Volume / Hull.MaxCompress) { - hull1.LethalPressure += ((Submarine != null && Submarine.AtDamageDepth) ? 100.0f : 10.0f) * deltaTime; + hull1.LethalPressure += ((Submarine != null && Submarine.AtDamageDepth) ? 100.0f : Hull.PressureBuildUpSpeed) * deltaTime; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 5d98d6a87..7de5bf629 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -8,6 +8,8 @@ using System.Globalization; using System.Linq; using System.Xml.Linq; using Barotrauma.MapCreatures.Behavior; +using Barotrauma.Items.Components; +using Barotrauma.Extensions; namespace Barotrauma { @@ -140,6 +142,16 @@ namespace Barotrauma get { return properties; } } + /// + /// How fast the pressure in the hull builds up when there's a gap leading outside + /// + public const float PressureBuildUpSpeed = 15.0f; + + /// + /// How fast the pressure in the hull goes back to normal when it's no longer full of water + /// + public const float PressureDropSpeed = 10.0f; + private float lethalPressure; private float surface; @@ -310,6 +322,8 @@ namespace Barotrauma } } + public bool IsAirlock { get; private set; } + private bool ForceAsWetRoom => roomName != null && ( roomName.Contains("ballast", StringComparison.OrdinalIgnoreCase) || @@ -404,6 +418,26 @@ namespace Barotrauma private bool networkUpdatePending; private float networkUpdateTimer; + /// + /// Average color of the background sections + /// + public Color AveragePaintedColor { get; private set; } + + /// + /// Returns true if the red component of the background color is twice as bright as the blue and green. Can be used by StatusEffects. + /// + public bool IsRed => ColorExtensions.IsRedDominant(AveragePaintedColor, minimumAlpha: 100); + + /// + /// Returns true if the green component of the background color is twice as bright as the red and blue. Can be used by StatusEffects. + /// + public bool IsGreen => ColorExtensions.IsGreenDominant(AveragePaintedColor, minimumAlpha: 100); + + /// + /// Returns true if the blue component of the background color is twice as bright as the red and green. Can be used by StatusEffects. + /// + public bool IsBlue => ColorExtensions.IsBlueDominant(AveragePaintedColor, minimumAlpha: 100); + public List FireSources { get; private set; } public List FakeFireSources { get; private set; } @@ -556,7 +590,9 @@ namespace Barotrauma } } Pressure = rect.Y - rect.Height + waterVolume / rect.Width; - + + DetermineIsAirlock(); + BallastFlora?.OnMapLoaded(); #if CLIENT lastAmbientLightEditTime = 0.0; @@ -980,7 +1016,7 @@ namespace Barotrauma if (waterVolume < Volume) { - LethalPressure -= 10.0f * deltaTime; + LethalPressure -= PressureDropSpeed * deltaTime; if (WaterVolume <= 0.0f) { #if CLIENT @@ -1264,7 +1300,7 @@ namespace Barotrauma { if (g.ConnectedDoor != null && !HullList.Any(h => h.ConnectedGaps.Contains(g) && h != this)) return true; } - List structures = mapEntityList.FindAll(me => me is Structure && me.Rect.Intersects(Rect)); + List structures = MapEntityList.FindAll(me => me is Structure && me.Rect.Intersects(Rect)); return structures.Any(st => !(st as Structure).CastShadow); } return false; @@ -1280,7 +1316,7 @@ namespace Barotrauma if (item.GetComponent() != null) roomItems.Add("engine"); if (item.GetComponent() != null) roomItems.Add("steering"); if (item.GetComponent() != null) roomItems.Add("sonar"); - if (item.HasTag("ballast")) roomItems.Add("ballast"); + if (item.HasTag(Tags.Ballast)) roomItems.Add("ballast"); } if (roomItems.Contains("reactor")) @@ -1297,7 +1333,7 @@ namespace Barotrauma if (moduleFlags != null && moduleFlags.Any() && (Submarine.Info.Type == SubmarineType.OutpostModule || Submarine.Info.Type == SubmarineType.Outpost)) { - if (moduleFlags.Contains("airlock".ToIdentifier()) && + if (moduleFlags.Contains(Tags.Airlock) && ConnectedGaps.Any(g => !g.IsRoomToRoom && g.ConnectedDoor != null)) { return "RoomName.Airlock"; @@ -1334,23 +1370,26 @@ namespace Barotrauma /// /// Is this hull or any of the items inside it tagged as "airlock"? /// - public bool IsTaggedAirlock() + private void DetermineIsAirlock() { if (RoomName != null && RoomName.Contains("airlock", StringComparison.OrdinalIgnoreCase)) { - return true; + IsAirlock = true; + return; } else { + var airlockTag = "airlock".ToIdentifier(); foreach (Item item in Item.ItemList) { - if (item.CurrentHull != this && item.HasTag("airlock")) + if (item.CurrentHull != this && item.HasTag(airlockTag)) { - return true; + IsAirlock = true; + return; } } } - return false; + IsAirlock = false; } /// @@ -1489,6 +1528,7 @@ namespace Barotrauma #endif sectionUpdated = true; } + RefreshAveragePaintedColor(); } if (sectionUpdated && GameMain.NetworkMember != null && requiresUpdate) @@ -1501,6 +1541,17 @@ namespace Barotrauma } } + private void RefreshAveragePaintedColor() + { + Vector4 avgColor = Vector4.Zero; + foreach (var anySection in BackgroundSections) + { + avgColor += anySection.Color.ToVector4(); + } + avgColor /= BackgroundSections.Count; + AveragePaintedColor = new Color(avgColor); + } + public void SetSectionColorOrStrength(BackgroundSection section, Color? color, float? strength) { if (color != null) @@ -1619,6 +1670,7 @@ namespace Barotrauma } } } + hull.RefreshAveragePaintedColor(); SerializableProperty.DeserializeProperties(hull, element); if (element.GetAttribute("oxygen") == null) { hull.Oxygen = hull.Volume; } @@ -1687,7 +1739,7 @@ namespace Barotrauma public override string ToString() { - return $"{base.ToString()} ({Name ?? "unnamed"})"; + return $"{base.ToString()} ({DisplayName ?? "unnamed"}, {(Submarine?.Info?.Name ?? "no sub")})"; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 720f13874..a3fa754af 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -167,7 +167,7 @@ namespace Barotrauma public Point StartPos, EndPos; - public bool DisplayOnSonar; + public readonly HashSet MissionsToDisplayOnSonar = new HashSet(); public readonly CaveGenerationParams CaveGenerationParams; @@ -334,13 +334,15 @@ namespace Barotrauma LevelGenParams, Size, GenStart, - TunnelGen, + TunnelGen1, + TunnelGen2, AbyssGen, CaveGen, VoronoiGen, VoronoiGen2, VoronoiGen3, Ruins, + Outposts, FloatingIce, LevelBodies, IceSpires, @@ -512,7 +514,6 @@ namespace Barotrauma GenerateEqualityCheckValue(LevelGenStage.GenStart); SetEqualityCheckValue(LevelGenStage.LevelGenParams, unchecked((int)GenerationParams.UintIdentifier)); SetEqualityCheckValue(LevelGenStage.Size, borders.Width ^ borders.Height << 16); - GenerateEqualityCheckValue(LevelGenStage.TunnelGen); LevelObjectManager = new LevelObjectManager(); @@ -574,7 +575,7 @@ namespace Barotrauma (int)MathHelper.Lerp(borders.Bottom - Math.Max(minMainPathWidth, ExitDistance * 1.5f), borders.Y + minMainPathWidth, GenerationParams.EndPosition.Y)); endExitPosition = new Point(endPosition.X, borders.Bottom); - GenerateEqualityCheckValue(LevelGenStage.TunnelGen); + GenerateEqualityCheckValue(LevelGenStage.TunnelGen1); //---------------------------------------------------------------------------------- //generate the initial nodes for the main path and smaller tunnels @@ -670,15 +671,15 @@ namespace Barotrauma CalculateTunnelDistanceField(null); GenerateSeaFloorPositions(); - GenerateEqualityCheckValue(LevelGenStage.AbyssGen); + GenerateEqualityCheckValue(LevelGenStage.TunnelGen2); GenerateAbyssArea(); - GenerateEqualityCheckValue(LevelGenStage.CaveGen); + GenerateEqualityCheckValue(LevelGenStage.AbyssGen); GenerateCaves(mainPath); - GenerateEqualityCheckValue(LevelGenStage.VoronoiGen); + GenerateEqualityCheckValue(LevelGenStage.CaveGen); //---------------------------------------------------------------------------------- //generate voronoi sites @@ -784,7 +785,7 @@ namespace Barotrauma } } - GenerateEqualityCheckValue(LevelGenStage.VoronoiGen2); + GenerateEqualityCheckValue(LevelGenStage.VoronoiGen); //---------------------------------------------------------------------------------- // construct the voronoi graph and cells @@ -895,7 +896,7 @@ namespace Barotrauma startPosition.X = (int)pathCells[0].Site.Coord.X; startExitPosition.X = startPosition.X; - GenerateEqualityCheckValue(LevelGenStage.VoronoiGen3); + GenerateEqualityCheckValue(LevelGenStage.VoronoiGen2); //---------------------------------------------------------------------------------- // remove unnecessary cells and create some holes at the bottom of the level @@ -933,8 +934,12 @@ namespace Barotrauma } List ruinPositions = new List(); - int ruinCount = GenerationParams.RuinCount; - if (GameMain.GameSession?.GameMode?.Missions.Any(m => m.Prefab.RequireRuin) ?? false) + int ruinCount = GenerationParams.UseRandomRuinCount() + ? Rand.Range(GenerationParams.MinRuinCount, GenerationParams.MaxRuinCount + 1, Rand.RandSync.ServerAndClient) + : GenerationParams.RuinCount; + + bool hasRuinMissions = GameMain.GameSession?.GameMode?.Missions.Any(m => m.Prefab.RequireRuin) ?? false; + if (hasRuinMissions) { ruinCount = Math.Max(ruinCount, 1); } @@ -1131,7 +1136,7 @@ namespace Barotrauma } } - GenerateEqualityCheckValue(LevelGenStage.Ruins); + GenerateEqualityCheckValue(LevelGenStage.VoronoiGen3); //---------------------------------------------------------------------------------- // create some ruins @@ -1141,10 +1146,10 @@ namespace Barotrauma for (int i = 0; i < ruinPositions.Count; i++) { Rand.SetSyncedSeed(ToolBox.StringToInt(Seed) + i); - GenerateRuin(ruinPositions[i], mirror); + GenerateRuin(ruinPositions[i], mirror, hasRuinMissions); } - GenerateEqualityCheckValue(LevelGenStage.FloatingIce); + GenerateEqualityCheckValue(LevelGenStage.Ruins); //---------------------------------------------------------------------------------- // create floating ice chunks @@ -1176,7 +1181,7 @@ namespace Barotrauma } } - GenerateEqualityCheckValue(LevelGenStage.LevelBodies); + GenerateEqualityCheckValue(LevelGenStage.FloatingIce); //---------------------------------------------------------------------------------- // generate the bodies and rendered triangles of the cells @@ -1281,7 +1286,7 @@ namespace Barotrauma } #endif - GenerateEqualityCheckValue(LevelGenStage.IceSpires); + GenerateEqualityCheckValue(LevelGenStage.LevelBodies); //---------------------------------------------------------------------------------- // create ice spires @@ -1294,6 +1299,8 @@ namespace Barotrauma if (spire != null) { ExtraWalls.Add(spire); }; } + GenerateEqualityCheckValue(LevelGenStage.IceSpires); + //---------------------------------------------------------------------------------- // connect side paths and cave branches to their parents //---------------------------------------------------------------------------------- @@ -1316,7 +1323,7 @@ namespace Barotrauma CreateOutposts(); - GenerateEqualityCheckValue(LevelGenStage.TopAndBottom); + GenerateEqualityCheckValue(LevelGenStage.Outposts); //---------------------------------------------------------------------------------- // top barrier & sea floor @@ -1325,7 +1332,8 @@ namespace Barotrauma TopBarrier = GameMain.World.CreateEdge( ConvertUnits.ToSimUnits(new Vector2(borders.X, 0)), ConvertUnits.ToSimUnits(new Vector2(borders.Right, 0))); - + //for debugging purposes + TopBarrier.UserData = "topbarrier"; TopBarrier.SetTransform(ConvertUnits.ToSimUnits(new Vector2(0.0f, borders.Height)), 0.0f); TopBarrier.BodyType = BodyType.Static; TopBarrier.CollisionCategories = Physics.CollisionLevel; @@ -1358,15 +1366,23 @@ namespace Barotrauma CreateWrecks(); CreateBeaconStation(); - GenerateEqualityCheckValue(LevelGenStage.PlaceLevelObjects); + GenerateEqualityCheckValue(LevelGenStage.TopAndBottom); LevelObjectManager.PlaceObjects(this, GenerationParams.LevelObjectAmount); - GenerateEqualityCheckValue(LevelGenStage.GenerateItems); + GenerateEqualityCheckValue(LevelGenStage.PlaceLevelObjects); GenerateItems(); - GenerateEqualityCheckValue(LevelGenStage.Finish); + foreach (Submarine sub in Submarine.Loaded) + { + if (sub.Info.IsOutpost) + { + OutpostGenerator.PowerUpOutpost(sub); + } + } + + GenerateEqualityCheckValue(LevelGenStage.GenerateItems); #if CLIENT backgroundCreatureManager.SpawnCreatures(this, GenerationParams.BackgroundCreatureAmount); @@ -1384,7 +1400,7 @@ namespace Barotrauma } //initialize MapEntities that aren't in any sub (e.g. items inside ruins) - MapEntity.MapLoaded(MapEntity.mapEntityList.FindAll(me => me.Submarine == null), false); + MapEntity.MapLoaded(MapEntity.MapEntityList.FindAll(me => me.Submarine == null), false); Debug.WriteLine("Generatelevel: " + sw2.ElapsedMilliseconds + " ms"); sw2.Restart(); @@ -1409,6 +1425,7 @@ namespace Barotrauma GameMain.Server.EntityEventManager.Clear(); #endif + GenerateEqualityCheckValue(LevelGenStage.Finish); //assign an ID to make entity events work //ID = FindFreeID(); Generating = false; @@ -1949,7 +1966,8 @@ namespace Barotrauma BottomBarrier = GameMain.World.CreateEdge( ConvertUnits.ToSimUnits(new Vector2(borders.X, 0)), ConvertUnits.ToSimUnits(new Vector2(borders.Right, 0))); - + //for debugging purposes + BottomBarrier.UserData = "bottombarrier"; BottomBarrier.SetTransform(ConvertUnits.ToSimUnits(new Vector2(0.0f, BottomPos)), 0.0f); BottomBarrier.BodyType = BodyType.Static; BottomBarrier.CollisionCategories = Physics.CollisionLevel; @@ -2055,23 +2073,56 @@ namespace Barotrauma } } - private void GenerateRuin(Point ruinPos, bool mirror) + private void GenerateRuin(Point ruinPos, bool mirror, bool requireMissionReadyRuin) { - var ruinGenerationParams = RuinGenerationParams.RuinParams.GetRandom(Rand.RandSync.ServerAndClient); + float GetWeight(RuinGenerationParams p) + { + if (p.PreferredDifficulty == -1) { return 100; } + float diff = Math.Abs(Difficulty - p.PreferredDifficulty) / 100; + float weight = MathUtils.Pow(1 - diff, 10); + return Math.Max(weight, 0); + } + IEnumerable possibleRuinGenerationParams = RuinGenerationParams.RuinParams; + if (requireMissionReadyRuin) + { + possibleRuinGenerationParams = possibleRuinGenerationParams.Where(p => p.IsMissionReady); + } + if (possibleRuinGenerationParams.Multiple()) + { + // Sort by weight and choose from the closest 25% of the candidates. + // Prevents choosing from the "wrong" end, which would otherwise be possible (yet rare), because we use a weighted random for the pick. + possibleRuinGenerationParams = possibleRuinGenerationParams + /* the prefabs aren't in a consistent order, so we need to sort them first to ensure the clients and server choose the same one */ + .OrderByDescending(p => p.UintIdentifier) + .OrderByDescending(GetWeight) + .Take((int)Math.Max(Math.Round(possibleRuinGenerationParams.Count() / 4f), 1)); + } + var selectedRuinGenerationParams = possibleRuinGenerationParams.GetRandomByWeight(GetWeight, randSync: Rand.RandSync.ServerAndClient); + if (selectedRuinGenerationParams == null) + { + DebugConsole.ThrowError("Failed to generate alien ruins. Could not find any RuinGenerationParameters!"); + return; + } + DebugConsole.NewMessage($"Creating alien ruins using {selectedRuinGenerationParams.Identifier} (preferred difficulty: {selectedRuinGenerationParams.PreferredDifficulty}, current difficulty {Difficulty})", color: Color.Yellow); LocationType locationType = StartLocation?.Type; if (locationType == null) { locationType = LocationType.Prefabs.GetRandom(Rand.RandSync.ServerAndClient); - if (ruinGenerationParams.AllowedLocationTypes.Any()) + if (selectedRuinGenerationParams.AllowedLocationTypes.Any()) { locationType = LocationType.Prefabs.Where(lt => - ruinGenerationParams.AllowedLocationTypes.Any(allowedType => + selectedRuinGenerationParams.AllowedLocationTypes.Any(allowedType => allowedType == "any" || lt.Identifier == allowedType)).GetRandom(Rand.RandSync.ServerAndClient); } } - var ruin = new Ruin(this, ruinGenerationParams, locationType, ruinPos, mirror); + var ruin = new Ruin(this, selectedRuinGenerationParams, locationType, ruinPos, mirror); + if (ruin.Submarine != null) + { + SetLinkedSubCrushDepth(ruin.Submarine); + } + Ruins.Add(ruin); var tooClose = GetTooCloseCells(ruinPos.ToVector2(), Math.Max(ruin.Area.Width, ruin.Area.Height) * 4); @@ -3671,7 +3722,7 @@ namespace Barotrauma } tempSW.Stop(); Debug.WriteLine($"Sub {sub.Info.Name} loaded in { tempSW.ElapsedMilliseconds} (ms)"); - sub.SetPosition(spawnPoint); + sub.SetPosition(spawnPoint, forceUndockFromStaticSubmarines: false); wreckPositions.Add(sub, positions); blockedRects.Add(sub, rects); return sub; @@ -3985,11 +4036,13 @@ namespace Barotrauma SubmarineInfo outpostInfo; Submarine outpost = null; - if (i == 0 && preSelectedStartOutpost == null || i == 1 && preSelectedEndOutpost == null) + + SubmarineInfo preSelectedOutpost = isStart ? preSelectedStartOutpost : preSelectedEndOutpost; + if (preSelectedOutpost == null) { if (LevelData.OutpostGenerationParamsExist) { - Location location = i == 0 ? StartLocation : EndLocation; + Location location = isStart ? StartLocation : EndLocation; OutpostGenerationParams outpostGenerationParams = null; if (LevelData.ForceOutpostGenerationParams != null) { @@ -4027,7 +4080,7 @@ namespace Barotrauma foreach (string categoryToHide in locationType.HideEntitySubcategories) { - foreach (MapEntity entityToHide in MapEntity.mapEntityList.Where(me => me.Submarine == outpost && (me.Prefab?.HasSubCategory(categoryToHide) ?? false))) + foreach (MapEntity entityToHide in MapEntity.MapEntityList.Where(me => me.Submarine == outpost && (me.Prefab?.HasSubCategory(categoryToHide) ?? false))) { entityToHide.HiddenInGame = true; } @@ -4048,7 +4101,7 @@ namespace Barotrauma else { DebugConsole.NewMessage($"Loading a pre-selected outpost for the {(isStart ? "start" : "end")} of the level..."); - outpostInfo = (i == 0) ? preSelectedStartOutpost : preSelectedEndOutpost; + outpostInfo = preSelectedOutpost; outpostInfo.Type = SubmarineType.Outpost; outpost = new Submarine(outpostInfo); } @@ -4132,6 +4185,7 @@ namespace Barotrauma } outpost.SetPosition(spawnPos, forceUndockFromStaticSubmarines: false); + SetLinkedSubCrushDepth(outpost); foreach (WayPoint wp in WayPoint.WayPointList) { @@ -4178,7 +4232,7 @@ namespace Barotrauma ContentFile contentFile = null; if (!string.IsNullOrEmpty(GenerationParams.ForceBeaconStation)) { - contentFile = beaconStationFiles.FirstOrDefault(f => f.Path == GenerationParams.ForceBeaconStation); + contentFile = beaconStationFiles.OrderBy(b => b.UintIdentifier).FirstOrDefault(f => f.Path == GenerationParams.ForceBeaconStation); if (contentFile == null) { DebugConsole.ThrowError($"Failed to find the beacon station \"{GenerationParams.ForceBeaconStation}\". Using a random one instead..."); @@ -4266,77 +4320,75 @@ namespace Barotrauma beaconSonar.Item.CreateServerEvent(beaconSonar); #endif } - else + else if (GameMain.NetworkMember is not { IsClient: true }) { - if (!(GameMain.NetworkMember?.IsClient ?? false)) + bool allowDisconnectedWires = true; + bool allowDamagedWalls = true; + if (BeaconStation?.Info?.BeaconStationInfo is BeaconStationInfo info) { - bool allowDisconnectedWires = true; - bool allowDamagedWalls = true; - if (BeaconStation?.Info?.BeaconStationInfo is BeaconStationInfo info) - { - allowDisconnectedWires = info.AllowDisconnectedWires; - allowDamagedWalls = info.AllowDamagedWalls; - } + allowDisconnectedWires = info.AllowDisconnectedWires; + allowDamagedWalls = info.AllowDamagedWalls; + } - //remove wires - float removeWireMinDifficulty = 20.0f; - float removeWireProbability = MathUtils.InverseLerp(removeWireMinDifficulty, 100.0f, LevelData.Difficulty) * 0.5f; - if (removeWireProbability > 0.0f && allowDisconnectedWires) + //remove wires + float removeWireMinDifficulty = 20.0f; + float removeWireProbability = MathUtils.InverseLerp(removeWireMinDifficulty, 100.0f, LevelData.Difficulty) * 0.5f; + if (removeWireProbability > 0.0f && allowDisconnectedWires) + { + foreach (Item item in beaconItems.Where(it => it.GetComponent() != null).ToList()) { - foreach (Item item in beaconItems.Where(it => it.GetComponent() != null).ToList()) + if (item.NonInteractable || item.InvulnerableToDamage) { continue; } + Wire wire = item.GetComponent(); + if (wire.Locked) { continue; } + if (wire.Connections[0] != null && (wire.Connections[0].Item.NonInteractable || wire.Connections[0].Item.GetComponent().Locked)) { - if (item.NonInteractable || item.InvulnerableToDamage) { continue; } - Wire wire = item.GetComponent(); - if (wire.Locked) { continue; } - if (wire.Connections[0] != null && (wire.Connections[0].Item.NonInteractable || wire.Connections[0].Item.GetComponent().Locked)) + continue; + } + if (wire.Connections[1] != null && (wire.Connections[1].Item.NonInteractable || wire.Connections[1].Item.GetComponent().Locked)) + { + continue; + } + if (Rand.Range(0f, 1.0f, Rand.RandSync.Unsynced) < removeWireProbability) + { + foreach (Connection connection in wire.Connections) { - continue; - } - if (wire.Connections[1] != null && (wire.Connections[1].Item.NonInteractable || wire.Connections[1].Item.GetComponent().Locked)) - { - continue; - } - if (Rand.Range(0f, 1.0f, Rand.RandSync.Unsynced) < removeWireProbability) - { - foreach (Connection connection in wire.Connections) + if (connection != null) { - if (connection != null) - { - connection.ConnectionPanel.DisconnectedWires.Add(wire); - wire.RemoveConnection(connection.Item); + connection.ConnectionPanel.DisconnectedWires.Add(wire); + wire.RemoveConnection(connection.Item); #if SERVER - connection.ConnectionPanel.Item.CreateServerEvent(connection.ConnectionPanel); - wire.CreateNetworkEvent(); + connection.ConnectionPanel.Item.CreateServerEvent(connection.ConnectionPanel); + wire.CreateNetworkEvent(); #endif - } } } } } + } - if (allowDamagedWalls) + if (allowDamagedWalls) + { + //break powered items + foreach (Item item in beaconItems.Where(it => it.Components.Any(c => c is Powered) && it.Components.Any(c => c is Repairable))) { - //break powered items - foreach (Item item in beaconItems.Where(it => it.Components.Any(c => c is Powered) && it.Components.Any(c => c is Repairable))) + if (item.NonInteractable || item.InvulnerableToDamage) { continue; } + if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.5f) { - if (item.NonInteractable || item.InvulnerableToDamage) { continue; } - if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.5f) - { - item.Condition *= Rand.Range(0.6f, 0.8f, Rand.RandSync.Unsynced); - } + item.Condition *= Rand.Range(0.6f, 0.8f, Rand.RandSync.Unsynced); } - //poke holes in the walls - foreach (Structure structure in Structure.WallList.Where(s => s.Submarine == BeaconStation)) + } + //poke holes in the walls + foreach (Structure structure in Structure.WallList.Where(s => s.Submarine == BeaconStation)) + { + if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.25f) { - if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.25f) - { - int sectionIndex = Rand.Range(0, structure.SectionCount - 1, Rand.RandSync.Unsynced); - structure.AddDamage(sectionIndex, Rand.Range(structure.MaxHealth * 0.2f, structure.MaxHealth, Rand.RandSync.Unsynced)); - } + int sectionIndex = Rand.Range(0, structure.SectionCount - 1, Rand.RandSync.Unsynced); + structure.AddDamage(sectionIndex, Rand.Range(structure.MaxHealth * 0.2f, structure.MaxHealth, Rand.RandSync.Unsynced)); } } } } + SetLinkedSubCrushDepth(BeaconStation); } public bool CheckBeaconActive() @@ -4345,7 +4397,15 @@ namespace Barotrauma return beaconSonar.Voltage > beaconSonar.MinVoltage && beaconSonar.CurrentMode == Sonar.Mode.Active; } - private bool IsModeStartOutpostCompatible() + private void SetLinkedSubCrushDepth(Submarine parentSub) + { + foreach (var connectedSub in parentSub.GetConnectedSubs()) + { + connectedSub.RealWorldCrushDepth = Math.Max(connectedSub.RealWorldCrushDepth, GetRealWorldDepth(0) + 1000); + } + } + + private static bool IsModeStartOutpostCompatible() { #if CLIENT return GameMain.GameSession?.GameMode is CampaignMode || GameMain.GameSession?.GameMode is TutorialMode || GameMain.GameSession?.GameMode is TestGameMode; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index b7a5aea58..77c3a7cf0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -178,7 +178,7 @@ namespace Barotrauma if (eventSet is null) { continue; } int count = childElement.GetAttributeInt("count", 0); if (count < 1) { continue; } - FinishedEvents.Add(eventSet, count); + FinishedEvents.TryAdd(eventSet, count); } static EventSet FindSetRecursive(EventSet parentSet, Identifier setIdentifier) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs index 1a1210460..66e83c546 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs @@ -502,9 +502,19 @@ namespace Barotrauma } } - [Serialize(1, IsPropertySaveable.Yes, description: "The number of alien ruins in the level."), Editable(MinValueInt = 0, MaxValueInt = 10)] + public bool UseRandomRuinCount() => MinRuinCount >= 0 && MaxRuinCount > 0; + + public int GetMaxRuinCount() => UseRandomRuinCount() ? MaxRuinCount : RuinCount; + + [Serialize(1, IsPropertySaveable.Yes, description: "The number of alien ruins in the level. Ignored, if both MinRuinCount and MaxRuinCount are defined."), Editable(MinValueInt = 0, MaxValueInt = 10)] public int RuinCount { get; set; } + [Serialize(0, IsPropertySaveable.Yes, description: "The minimum number of alien ruins in the level."), Editable(MinValueInt = 0, MaxValueInt = 10)] + public int MinRuinCount { get; set; } + + [Serialize(0, IsPropertySaveable.Yes, description: "The maximum number of alien ruins in the level."), Editable(MinValueInt = 0, MaxValueInt = 10)] + public int MaxRuinCount { get; set; } + // TODO: Move the wreck parameters under a separate class? #region Wreck parameters [Serialize(1, IsPropertySaveable.Yes, description: "The minimum number of wrecks in the level. Note that this value cannot be higher than the amount of wreck prefabs (subs)."), Editable(MinValueInt = 0, MaxValueInt = 10)] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs index 1bec94d04..462235bed 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -20,6 +20,8 @@ namespace Barotrauma private List updateableObjects; private List[,] objectGrid; + const float ParallaxStrength = 0.0001f; + public float GlobalForceDecreaseTimer { get; @@ -237,7 +239,6 @@ namespace Barotrauma PlaceObject(prefab, spawnPosition, level, cave); if (amount > prefab.MaxCount && objects.Count > prefab.MaxCount) { - bool maxReached = false; int objectCount = 0; for (int j = 0; j < objects.Count; j++) { @@ -406,11 +407,11 @@ namespace Barotrauma } } - float minX = spriteCorners.Min(c => c.X) - newObject.Position.Z / 10000.0f; - float maxX = spriteCorners.Max(c => c.X) + newObject.Position.Z / 10000.0f; + float minX = spriteCorners.Min(c => c.X) - newObject.Position.Z * ParallaxStrength; + float maxX = spriteCorners.Max(c => c.X) + newObject.Position.Z * ParallaxStrength; - float minY = spriteCorners.Min(c => c.Y) - newObject.Position.Z / 10000.0f - level.BottomPos; - float maxY = spriteCorners.Max(c => c.Y) + newObject.Position.Z / 10000.0f - level.BottomPos; + float minY = spriteCorners.Min(c => c.Y) - newObject.Position.Z * ParallaxStrength - level.BottomPos; + float maxY = spriteCorners.Max(c => c.Y) + newObject.Position.Z * ParallaxStrength - level.BottomPos; if (newObject.Triggers != null) { @@ -465,7 +466,7 @@ namespace Barotrauma for (int y = yStart; y <= yEnd; y++) { var list = objectGrid[x, y]; - if (objectGrid[x, y] == null) { list = objectGrid[x, y] = new List(); } + if (list == null) { objectGrid[x, y] = list = new List(); } //insertion sort in ascending order (= prefer rendering objects in front) int drawOrderIndex = 0; @@ -478,9 +479,9 @@ namespace Barotrauma } } - public static Microsoft.Xna.Framework.Point GetGridIndices(Vector2 worldPosition) + public static Point GetGridIndices(Vector2 worldPosition) { - return new Microsoft.Xna.Framework.Point( + return new Point( (int)Math.Floor(worldPosition.X / GridSize), (int)Math.Floor((worldPosition.Y - Level.Loaded.BottomPos) / GridSize)); } @@ -521,7 +522,7 @@ namespace Barotrauma return objectsInRange; } - private static List GetAvailableSpawnPositions(IEnumerable cells, LevelObjectPrefab.SpawnPosType spawnPosType, bool checkFlags = true) + private static List GetAvailableSpawnPositions(IEnumerable cells, LevelObjectPrefab.SpawnPosType spawnPosType) { List spawnPosTypes = new List(4); List availableSpawnPositions = new List(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs index 60d7ce783..1aadaa354 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs @@ -366,6 +366,7 @@ namespace Barotrauma private void LoadElements(LevelObjectPrefabsFile file, ContentXElement element, int parentTriggerIndex) { int propertyOverrideCount = 0; + //load sprites first, OverrideProperties may need them (defaulting to the default sprite if no override is defined) foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -384,6 +385,12 @@ namespace Barotrauma case "deformablesprite": DeformableSprite = new DeformableSprite(subElement, lazyLoad: true); break; + } + } + foreach (var subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { case "overridecommonness": Identifier levelType = subElement.GetAttributeIdentifier("leveltype", Identifier.Empty); if (!OverrideCommonness.ContainsKey(levelType)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs index 501c9d163..a0b834335 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs @@ -27,6 +27,9 @@ namespace Barotrauma.RuinGeneration public override string Name => "RuinGenerationParams"; + [Serialize(true, IsPropertySaveable.Yes, description: "Are these params designed to be used for alien ruins targeted by missions. If false, the params are ignored when there's any missions targeting ruins."), Editable] + public bool IsMissionReady { get; set; } + public RuinGenerationParams(ContentXElement element, RuinConfigFile file) : base(element, file) { } public static void SaveAll() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs index 72504a631..4d4d7ab3b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs @@ -44,7 +44,7 @@ namespace Barotrauma.RuinGeneration //prevent the ruin from extending above the level "ceiling" position.Y = Math.Min(level.Size.Y - (Submarine.Borders.Height / 2) - 100, position.Y); - Submarine.SetPosition(position.ToVector2()); + Submarine.SetPosition(position.ToVector2(), forceUndockFromStaticSubmarines: false); if (mirror) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs index 58ae53c65..1cae8abfe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs @@ -398,7 +398,8 @@ namespace Barotrauma } } - if (GameMain.GameSession?.GameMode is CampaignMode campaign && campaign.PurchasedLostShuttles) + if (GameMain.GameSession?.GameMode is CampaignMode campaign && + (campaign.PurchasedLostShuttles || campaign.PurchasedLostShuttlesInLatestSave)) { foreach (Structure wall in Structure.WallList) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 296df273b..03ebcb980 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -69,9 +69,11 @@ namespace Barotrauma public int LocationTypeChangeCooldown; /// - /// Is some mission blocking this location from changing its type? + /// Is some mission blocking this location from changing its type, or have location type changes been forcibly disabled on the location? /// - public bool LocationTypeChangesBlocked => availableMissions.Any(m => m.Prefab.BlockLocationTypeChanges); + public bool LocationTypeChangesBlocked => DisallowLocationTypeChanges || availableMissions.Any(m => m.Prefab.BlockLocationTypeChanges); + + public bool DisallowLocationTypeChanges; public string BaseName { get => baseName; } @@ -1146,6 +1148,7 @@ namespace Barotrauma foreach (Item item in items) { if (takenItems.Any(it => it.Matches(item) && it.OriginalID == item.ID)) { continue; } + if (item.IsSalvageMissionItem) { continue; } if (item.OriginalModuleIndex < 0) { DebugConsole.ThrowError("Tried to register a non-outpost item as being taken from the outpost."); @@ -1417,7 +1420,7 @@ namespace Barotrauma public void Reset(CampaignMode campaign) { - if (Type != OriginalType) + if (Type != OriginalType && !DisallowLocationTypeChanges) { ChangeType(campaign, OriginalType); PendingLocationTypeChange = null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs index f2211a472..475fd10e0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs @@ -41,6 +41,8 @@ namespace Barotrauma public bool IsEnterable { get; private set; } + public bool AllowAsBiomeGate { get; private set; } + /// /// Can this location type be used in the random, non-campaign levels that don't take place in any specific zone /// @@ -120,6 +122,7 @@ namespace Barotrauma UsePortraitInRandomLoadingScreens = element.GetAttributeBool(nameof(UsePortraitInRandomLoadingScreens), true); HasOutpost = element.GetAttributeBool("hasoutpost", true); IsEnterable = element.GetAttributeBool("isenterable", HasOutpost); + AllowAsBiomeGate = element.GetAttributeBool(nameof(AllowAsBiomeGate), true); AllowInRandomLevels = element.GetAttributeBool(nameof(AllowInRandomLevels), true); ShowSonarMarker = element.GetAttributeBool("showsonarmarker", true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index b3c614074..bbb70a021 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -614,13 +614,20 @@ namespace Barotrauma Connections[i].Locations[0].MapPosition.X < Connections[i].Locations[1].MapPosition.X ? Connections[i].Locations[0] : Connections[i].Locations[1]; - if (!leftMostLocation.Type.HasOutpost || leftMostLocation.Type.Identifier == "abandoned") + if (!AllowAsBiomeGate(leftMostLocation.Type)) { leftMostLocation.ChangeType( campaign, - LocationType.Prefabs.OrderBy(lt => lt.Identifier).First(lt => lt.HasOutpost && lt.Identifier != "abandoned"), + LocationType.Prefabs.OrderBy(lt => lt.Identifier).First(lt => AllowAsBiomeGate(lt)), createStores: false); } + static bool AllowAsBiomeGate(LocationType lt) + { + //checking for "abandoned" is not strictly necessary here because it's now configured to not be allowed as a biome gate + //but might be better to keep it for backwards compatibility (previously we relied only on that check) + return lt.HasOutpost && lt.Identifier != "abandoned" && lt.AllowAsBiomeGate; + } + leftMostLocation.IsGateBetweenBiomes = true; Connections[i].Locked = true; @@ -784,6 +791,28 @@ namespace Barotrauma System.Diagnostics.Debug.Assert(Connections.All(c => c.Biome != null)); } + private Location GetPreviousToEndLocation() + { + Location previousToEndLocation = null; + foreach (Location location in Locations) + { + if (!location.Biome.IsEndBiome && (previousToEndLocation == null || location.MapPosition.X > previousToEndLocation.MapPosition.X)) + { + previousToEndLocation = location; + } + } + return previousToEndLocation; + } + + private void ForceLocationTypeToNone(CampaignMode campaign, Location location) + { + if (LocationType.Prefabs.TryGet("none", out LocationType locationType)) + { + location.ChangeType(campaign, locationType, createStores: false); + } + location.DisallowLocationTypeChanges = true; + } + private void CreateEndLocation(CampaignMode campaign) { float zoneWidth = Width / generationParams.DifficultyZones; @@ -800,15 +829,7 @@ namespace Barotrauma } } - Location previousToEndLocation = null; - foreach (Location location in Locations) - { - if (!location.Biome.IsEndBiome && (previousToEndLocation == null || location.MapPosition.X > previousToEndLocation.MapPosition.X)) - { - previousToEndLocation = location; - } - } - + var previousToEndLocation = GetPreviousToEndLocation(); if (endLocation == null || previousToEndLocation == null) { return; } endLocations = new List() { endLocation }; @@ -833,10 +854,7 @@ namespace Barotrauma } } - if (LocationType.Prefabs.TryGet("none", out LocationType locationType)) - { - previousToEndLocation.ChangeType(campaign, locationType, createStores: false); - } + ForceLocationTypeToNone(campaign, previousToEndLocation); //remove all locations from the end biome except the end location for (int i = Locations.Count - 1; i >= 0; i--) @@ -1555,6 +1573,7 @@ namespace Barotrauma if (index < 0) { return null; } return Locations[index]; } + } void Discover(Location location) @@ -1604,6 +1623,12 @@ namespace Barotrauma SelectLocation(CurrentLocation.Connections[0].OtherLocation(CurrentLocation)); } } + + var previousToEndLocation = GetPreviousToEndLocation(); + if (previousToEndLocation != null) + { + ForceLocationTypeToNone(campaign, previousToEndLocation); + } } public void Save(XElement element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs index 1e1e136c2..a510fd2f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs @@ -110,7 +110,13 @@ namespace Barotrauma return; } - radiationAffliction ??= new Affliction(AfflictionPrefab.RadiationSickness, Params.RadiationDamageAmount); + if (radiationAffliction == null) + { + float radiationStrengthChange = AfflictionPrefab.RadiationSickness.Effects.FirstOrDefault()?.StrengthChange ?? 0.0f; + radiationAffliction = new Affliction( + AfflictionPrefab.RadiationSickness, + (Params.RadiationDamageAmount - radiationStrengthChange) * Params.RadiationDamageDelay); + } radiationTimer = Params.RadiationDamageDelay; @@ -120,11 +126,9 @@ namespace Barotrauma if (IsEntityRadiated(character)) { - foreach (Limb limb in character.AnimController.Limbs) - { - AttackResult attackResult = limb.AddDamage(limb.SimPosition, radiationAffliction.ToEnumerable(), playSound: false); - character.CharacterHealth.ApplyDamage(limb, attackResult); - } + var limb = character.AnimController.MainLimb; + AttackResult attackResult = limb.AddDamage(limb.SimPosition, radiationAffliction.ToEnumerable(), playSound: false); + character.CharacterHealth.ApplyDamage(limb, attackResult); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index 09fcedaf3..39f35865a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -12,7 +12,7 @@ namespace Barotrauma { abstract partial class MapEntity : Entity, ISpatialEntity { - public static List mapEntityList = new List(); + public readonly static List MapEntityList = new List(); public readonly MapEntityPrefab Prefab; @@ -82,8 +82,8 @@ namespace Barotrauma { get { return isHighlighted || ExternalHighlight; } set - { - if (value != IsHighlighted) + { + if (value != isHighlighted) { isHighlighted = value; CheckIsHighlighted(); @@ -531,32 +531,32 @@ namespace Barotrauma { if (Sprite == null) { - mapEntityList.Add(this); + MapEntityList.Add(this); return; } int i = 0; - while (i < mapEntityList.Count) + while (i < MapEntityList.Count) { i++; - if (mapEntityList[i - 1]?.Prefab == Prefab) + if (MapEntityList[i - 1]?.Prefab == Prefab) { - mapEntityList.Insert(i, this); + MapEntityList.Insert(i, this); return; } } #if CLIENT i = 0; - while (i < mapEntityList.Count) + while (i < MapEntityList.Count) { i++; - Sprite existingSprite = mapEntityList[i - 1].Sprite; + Sprite existingSprite = MapEntityList[i - 1].Sprite; if (existingSprite == null) { continue; } if (existingSprite.Texture == this.Sprite.Texture) { break; } } #endif - mapEntityList.Insert(i, this); + MapEntityList.Insert(i, this); } /// @@ -566,7 +566,7 @@ namespace Barotrauma { base.Remove(); - mapEntityList.Remove(this); + MapEntityList.Remove(this); if (aiTarget != null) aiTarget.Remove(); } @@ -575,7 +575,7 @@ namespace Barotrauma { base.Remove(); - mapEntityList.Remove(this); + MapEntityList.Remove(this); #if CLIENT Submarine.ForceRemoveFromVisibleEntities(this); @@ -638,6 +638,7 @@ namespace Barotrauma sw.Restart(); #endif Powered.UpdatePower(deltaTime); + Item.UpdatePendingConditionUpdates(deltaTime); foreach (Item item in Item.ItemList) { item.Update(deltaTime, cam); @@ -689,6 +690,20 @@ namespace Barotrauma { IdRemap idRemap = new IdRemap(parentElement, idOffset); + bool containsHiddenContainers = false; + bool hiddenContainerCreated = false; + MTRandom hiddenContainerRNG = new MTRandom(ToolBox.StringToInt(submarine.Info.Name)); + foreach (var element in parentElement.Elements()) + { + if (element.NameAsIdentifier() != "Item") { continue; } + var tags = element.GetAttributeIdentifierArray("tags", Array.Empty()); + if (tags.Contains(Tags.HiddenItemContainer)) + { + containsHiddenContainers = true; + break; + } + } + List entities = new List(); foreach (var element in parentElement.Elements()) { @@ -713,10 +728,11 @@ namespace Barotrauma continue; } + Identifier identifier = element.GetAttributeIdentifier("identifier", ""); + Identifier replacementIdentifier = Identifier.Empty; if (t == typeof(Structure)) { string name = element.Attribute("name").Value; - Identifier identifier = element.GetAttributeIdentifier("identifier", ""); StructurePrefab structurePrefab = Structure.FindPrefab(name, identifier); if (structurePrefab == null) { @@ -727,6 +743,20 @@ namespace Barotrauma } } } + else if (t == typeof(Item) && !containsHiddenContainers && identifier == "vent" && + submarine.Info.Type == SubmarineType.Player && !submarine.Info.HasTag(SubmarineTag.Shuttle)) + { + if (!hiddenContainerCreated) + { + DebugConsole.AddWarning($"There are no hidden containers such as loose vents or loose panels in the submarine \"{submarine.Info.Name}\". Certain traitor events require these to function properly. Converting one of the vents to a loose vent..."); + } + if (!hiddenContainerCreated || hiddenContainerRNG.NextDouble() < 0.2) + { + replacementIdentifier = "loosevent".ToIdentifier(); + containsHiddenContainers = true; + hiddenContainerCreated = true; + } + } try { @@ -741,7 +771,12 @@ namespace Barotrauma } else { - object newEntity = loadMethod.Invoke(t, new object[] { element.FromPackage(null), submarine, idRemap }); + var newElement = element.FromPackage(null); + if (!replacementIdentifier.IsEmpty) + { + newElement.SetAttributeValue("identifier", replacementIdentifier.ToString()); + } + object newEntity = loadMethod.Invoke(t, new object[] { newElement, submarine, idRemap }); if (newEntity != null) { entities.Add((MapEntity)newEntity); @@ -774,9 +809,9 @@ namespace Barotrauma for (int i = 0; i < entities.Count; i++) { if (entities[i].mapLoadedCalled || entities[i].Removed) { continue; } - if (entities[i] is LinkedSubmarine) + if (entities[i] is LinkedSubmarine sub) { - linkedSubs.Add((LinkedSubmarine)entities[i]); + linkedSubs.Add(sub); continue; } @@ -795,6 +830,35 @@ namespace Barotrauma { linkedSub.OnMapLoaded(); } + + CreateDroppedStacks(entities); + } + + private static void CreateDroppedStacks(List entities) + { + const float MaxDist = 10.0f; + List itemsInStack = new List(); + for (int i = 0; i < entities.Count; i++) + { + if (entities[i] is not Item item1 || item1.Prefab.MaxStackSize <= 1 || item1.body is not { Enabled: true }) { continue; } + itemsInStack.Clear(); + itemsInStack.Add(item1); + for (int j = i + 1; j < entities.Count; j++) + { + if (entities[j] is not Item item2) { continue; } + if (item1.Prefab != item2.Prefab) { continue; } + if (item2.body is not { Enabled: true }) { continue; } + if (item2.DroppedStack.Any()) { continue; } + if (Math.Abs(item1.Position.X - item2.Position.X) > MaxDist) { continue; } + if (Math.Abs(item1.Position.Y - item2.Position.Y) > MaxDist) { continue; } + itemsInStack.Add(item2); + } + if (itemsInStack.Count > 1) + { + item1.CreateDroppedStack(itemsInStack, allowClientExecute: true); + DebugConsole.Log($"Merged x{itemsInStack.Count} of {item1.Name} into a dropped stack."); + } + } } public static void InitializeLoadedLinks(IEnumerable entities) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs index 2c9624004..ab5a6f72b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs @@ -158,11 +158,22 @@ namespace Barotrauma { if (name.IsNullOrEmpty()) { throw new ArgumentException($"{nameof(name)} must not be null or empty"); } - return Find(prefab => - prefab.Name.Equals(name, StringComparison.OrdinalIgnoreCase) || - prefab.OriginalName.Equals(name, StringComparison.OrdinalIgnoreCase) || - (prefab.Aliases != null && - prefab.Aliases.Any(a => a.Equals(name, StringComparison.OrdinalIgnoreCase)))); + var matches = + List.Where(prefab => + prefab.Name.Equals(name, StringComparison.OrdinalIgnoreCase) || + prefab.OriginalName.Equals(name, StringComparison.OrdinalIgnoreCase) || + (prefab.Aliases != null && + prefab.Aliases.Any(a => a.Equals(name, StringComparison.OrdinalIgnoreCase)))); + //if there's multiple matches, prefer ones that aren't hidden in menus and base items + //(hidden ones and variants are often e.g. some kind of special ones used in an event or versions unlockable with talents) + if (matches.Count() > 1) + { + var bestMatch = + matches.FirstOrDefault(prefab => !prefab.HideInMenus) ?? + matches.FirstOrDefault(prefab => prefab is ItemPrefab ip && ip.VariantOf.IsEmpty); + if (bestMatch != null) { return bestMatch; } + } + return matches.FirstOrDefault(); } public static MapEntityPrefab FindByIdentifier(Identifier identifier) @@ -210,6 +221,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.No)] public bool HideInMenus { get; protected set; } + [Serialize(false, IsPropertySaveable.No)] + public bool HideInEditors { get; protected set; } + [Serialize("", IsPropertySaveable.No)] public string Subcategory { get; protected set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs index 35a758882..b55acd04e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs @@ -31,6 +31,12 @@ namespace Barotrauma set; } + [Serialize(-1, IsPropertySaveable.Yes, description: "The closer to the current level difficulty this value is, the higher the probability of choosing these generation params are. Defaults to -1, which means we use the current difficulty."), Editable(MinValueInt = 1, MaxValueInt = 50)] + public int PreferredDifficulty + { + get; + set; + } [Serialize(10, IsPropertySaveable.Yes, description: "Total number of modules in the outpost."), Editable(MinValueInt = 1, MaxValueInt = 50)] public int TotalModuleCount diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 5f410a025..5c0da41f4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Security.Cryptography; namespace Barotrauma { @@ -130,7 +129,7 @@ namespace Barotrauma //remove linked subs too if (otherSub.Submarine == sub) { connectedSubs.Add(otherSub); } } - List entities = MapEntity.mapEntityList.FindAll(e => connectedSubs.Contains(e.Submarine)); + List entities = MapEntity.MapEntityList.FindAll(e => connectedSubs.Contains(e.Submarine)); entities.ForEach(e => e.Remove()); foreach (Submarine otherSub in connectedSubs) { @@ -201,7 +200,14 @@ namespace Barotrauma selectedModules.Add(new PlacedModule(initialModule, null, OutpostModuleInfo.GapPosition.None)); selectedModules.Last().FulfilledModuleTypes.Add(initialModuleFlag); - AppendToModule(selectedModules.Last(), outpostModules.ToList(), pendingModuleFlags, selectedModules, locationType, allowExtendBelowInitialModule: generationParams is RuinGeneration.RuinGenerationParams); + + AppendToModule( + selectedModules.Last(), outpostModules.ToList(), pendingModuleFlags, + selectedModules, + locationType, + allowExtendBelowInitialModule: generationParams is RuinGeneration.RuinGenerationParams, + allowDifferentLocationType: remainingTries == 1); + if (pendingModuleFlags.Any(flag => flag != "none")) { if (!allowInvalidOutpost) @@ -442,7 +448,6 @@ namespace Barotrauma } } AlignLadders(selectedModules, entities); - PowerUpOutpost(entities.SelectMany(e => e.Value)); if (generationParams.MaxWaterPercentage > 0.0f) { foreach (var entity in allEntities) @@ -533,67 +538,67 @@ namespace Barotrauma /// Attaches additional modules to all the available gaps of the given module, /// and continues recursively through the attached modules until all the pending module types have been placed. /// - /// The module to attach to - /// Which modules we can choose from - /// Which types of modules we still need in the outpost + /// The module to attach to. + /// Which modules we can choose from. + /// Which types of modules we still need in the outpost. /// The modules we've already selected to be used in the outpost. + /// The type of the location we're generating the outpost for. + /// If we fail to append to the current module, should we try replacing it with something else and see if we can append to it then? + /// Is the module allowed to be placed further down than the initial module (usually the airlock module)? + /// If we fail to find a module suitable for the location type, should we use a module that's meant for a different location type instead? private static bool AppendToModule(PlacedModule currentModule, List availableModules, List pendingModuleFlags, List selectedModules, LocationType locationType, - bool retry = true, - bool allowExtendBelowInitialModule = false) + bool tryReplacingCurrentModule = true, + bool allowExtendBelowInitialModule = false, + bool allowDifferentLocationType = false) { if (pendingModuleFlags.Count == 0) { return true; } List placedModules = new List(); - for (int i = 0; i < 2; i++) + foreach (OutpostModuleInfo.GapPosition gapPosition in GapPositions.Randomize(Rand.RandSync.ServerAndClient)) { - //try placing a module meant for this location type first, and if that fails, try choosing whatever fits - bool allowDifferentLocationType = i > 0; - foreach (OutpostModuleInfo.GapPosition gapPosition in GapPositions.Randomize(Rand.RandSync.ServerAndClient)) + if (currentModule.UsedGapPositions.HasFlag(gapPosition)) { continue; } + if (DisallowBelowAirlock(allowExtendBelowInitialModule, gapPosition, currentModule)) { continue; } + + PlacedModule newModule = null; + //try appending to the current module if possible + if (currentModule.Info.OutpostModuleInfo.GapPositions.HasFlag(gapPosition)) { - if (currentModule.UsedGapPositions.HasFlag(gapPosition)) { continue; } - if (DisallowBelowAirlock(allowExtendBelowInitialModule, gapPosition, currentModule)) { continue; } - - PlacedModule newModule = null; - //try appending to the current module if possible - if (currentModule.Info.OutpostModuleInfo.GapPositions.HasFlag(gapPosition)) - { - newModule = AppendModule(currentModule, GetOpposingGapPosition(gapPosition), availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType); - } - - if (newModule != null) - { - placedModules.Add(newModule); - } - else - { - //couldn't append to current module, try one of the other placed modules - foreach (PlacedModule otherModule in selectedModules) - { - if (otherModule == currentModule) { continue; } - foreach (OutpostModuleInfo.GapPosition otherGapPosition in - GapPositions.Where(g => !otherModule.UsedGapPositions.HasFlag(g) && otherModule.Info.OutpostModuleInfo.GapPositions.HasFlag(g))) - { - if (DisallowBelowAirlock(allowExtendBelowInitialModule, otherGapPosition, otherModule)) { continue; } - newModule = AppendModule(otherModule, GetOpposingGapPosition(otherGapPosition), availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType); - if (newModule != null) - { - placedModules.Add(newModule); - break; - } - } - if (newModule != null) { break; } - } - } - if (pendingModuleFlags.Count == 0) { return true; } + newModule = AppendModule(currentModule, GetOpposingGapPosition(gapPosition), availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType); } - } + + if (newModule != null) + { + placedModules.Add(newModule); + } + else + { + //couldn't append to current module, try one of the other placed modules + foreach (PlacedModule otherModule in selectedModules) + { + if (otherModule == currentModule) { continue; } + foreach (OutpostModuleInfo.GapPosition otherGapPosition in + GapPositions.Where(g => !otherModule.UsedGapPositions.HasFlag(g) && otherModule.Info.OutpostModuleInfo.GapPositions.HasFlag(g))) + { + if (DisallowBelowAirlock(allowExtendBelowInitialModule, otherGapPosition, otherModule)) { continue; } + newModule = AppendModule(otherModule, GetOpposingGapPosition(otherGapPosition), availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType); + if (newModule != null) + { + placedModules.Add(newModule); + break; + } + } + if (newModule != null) { break; } + } + } + if (pendingModuleFlags.Count == 0) { return true; } + } //couldn't place a module anywhere, we're probably fucked! - if (placedModules.Count == 0 && retry && currentModule.PreviousModule != null && !selectedModules.Any(m => m != currentModule && m.PreviousModule == currentModule)) + if (placedModules.Count == 0 && tryReplacingCurrentModule && currentModule.PreviousModule != null && !selectedModules.Any(m => m != currentModule && m.PreviousModule == currentModule)) { //try to replace the previously placed module with something else that we can append to for (int i = 0; i < 10; i++) @@ -607,7 +612,7 @@ namespace Barotrauma currentModule = AppendModule(currentModule.PreviousModule, currentModule.ThisGapPosition, availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType: true); assertAllPreviousModulesPresent(); if (currentModule == null) { break; } - if (AppendToModule(currentModule, availableModules, pendingModuleFlags, selectedModules, locationType, retry: false, allowExtendBelowInitialModule: allowExtendBelowInitialModule)) + if (AppendToModule(currentModule, availableModules, pendingModuleFlags, selectedModules, locationType, tryReplacingCurrentModule: false, allowExtendBelowInitialModule, allowDifferentLocationType)) { assertAllPreviousModulesPresent(); return true; @@ -618,7 +623,7 @@ namespace Barotrauma foreach (PlacedModule placedModule in placedModules) { - AppendToModule(placedModule, availableModules, pendingModuleFlags, selectedModules, locationType, allowExtendBelowInitialModule: allowExtendBelowInitialModule); + AppendToModule(placedModule, availableModules, pendingModuleFlags, selectedModules, locationType, tryReplacingCurrentModule: true, allowExtendBelowInitialModule, allowDifferentLocationType); } return placedModules.Count > 0; @@ -929,10 +934,8 @@ namespace Barotrauma if (!suitableModules.Any()) { - if (allowDifferentLocationType) + if (allowDifferentLocationType && modulesWithCorrectFlags.Any()) { - if (modulesWithCorrectFlags.Any()) - DebugConsole.NewMessage($"Could not find a suitable module for the location type {locationType}. Module flag: {moduleFlag}.", Color.Orange); return ToolBox.SelectWeightedRandom(modulesWithCorrectFlags.ToList(), modulesWithCorrectFlags.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient); } @@ -1353,44 +1356,38 @@ namespace Barotrauma if (startWaypoint.WorldPosition.X > endWaypoint.WorldPosition.X) { - var temp = startWaypoint; - startWaypoint = endWaypoint; - endWaypoint = temp; + (endWaypoint, startWaypoint) = (startWaypoint, endWaypoint); } if (hallwayLength > 100 && isHorizontal) { + //if the hallway is longer than 100 pixels, generate some waypoints inside it + //for vertical hallways this isn't necessarily, it's done as a part of the ladder generation in AlignLadders WayPoint prevWayPoint = startWaypoint; + WayPoint firstWayPoint = null; for (float x = leftHull.Rect.Right + 50; x < rightHull.Rect.X - 50; x += 100.0f) { var newWayPoint = new WayPoint(new Vector2(x, hullBounds.Y + 110.0f), SpawnType.Path, sub); + firstWayPoint ??= newWayPoint; prevWayPoint.linkedTo.Add(newWayPoint); newWayPoint.linkedTo.Add(prevWayPoint); prevWayPoint = newWayPoint; } + if (firstWayPoint != null) + { + firstWayPoint.linkedTo.Add(startWaypoint); + startWaypoint.linkedTo.Add(firstWayPoint); + } if (prevWayPoint != null) { prevWayPoint.linkedTo.Add(endWaypoint); endWaypoint.linkedTo.Add(prevWayPoint); } } - - WayPoint closestWaypoint = null; - float closestDistSqr = 30.0f * 30.0f; - foreach (WayPoint waypoint in WayPoint.WayPointList) + else { - if (waypoint == startWaypoint) { continue; } - float dist = Vector2.DistanceSquared(waypoint.WorldPosition, startWaypoint.WorldPosition); - if (dist < closestDistSqr) - { - closestWaypoint = waypoint; - closestDistSqr = dist; - } - } - if (closestWaypoint != null) - { - startWaypoint.linkedTo.Add(closestWaypoint); - closestWaypoint.linkedTo.Add(startWaypoint); + startWaypoint.linkedTo.Add(endWaypoint); + endWaypoint.linkedTo.Add(startWaypoint); } } } @@ -1444,7 +1441,7 @@ namespace Barotrauma { foreach (MapEntity me in entities[module]) { - if (!(me is Gap gap)) { continue; } + if (me is not Gap gap) { continue; } var door = gap.ConnectedDoor; if (door != null && !door.UseBetweenOutpostModules) { continue; } if (placedModules.Any(m => m.PreviousGap == gap || m.ThisGap == gap)) @@ -1524,15 +1521,38 @@ namespace Barotrauma static void RemoveLinkedEntity(MapEntity linked) { - if (linked is Item linkedItem && linkedItem.Connections != null) + if (linked is Item linkedItem) { - foreach (Connection connection in linkedItem.Connections) + if (linkedItem.Connections != null) { - foreach (Wire w in connection.Wires.ToArray()) + foreach (Connection connection in linkedItem.Connections) { - w?.Item.Remove(); + foreach (Wire w in connection.Wires.ToArray()) + { + w?.Item.Remove(); + } } } + //if we end up removing a ladder, remove its waypoints too + if (linkedItem.GetComponent() is Ladder ladder) + { + var ladderWaypoints = WayPoint.WayPointList.FindAll(wp => wp.Ladders == ladder); + foreach (var ladderWaypoint in ladderWaypoints) + { + //got through all waypoints linked to the ladder waypoints, and link them together + //so we don't end up breaking up any paths by removing the ladder waypoints + for (int i = 0; i < ladderWaypoint.linkedTo.Count; i++) + { + if (ladderWaypoint.linkedTo[i] is not WayPoint waypoint1 || waypoint1.Ladders == ladder) { continue; } + for (int j = i + 1; j < ladderWaypoint.linkedTo.Count; j++) + { + if (ladderWaypoint.linkedTo[j] is not WayPoint waypoint2 || waypoint2.Ladders == ladder) { continue; } + waypoint1.ConnectTo(waypoint2); + } + } + } + ladderWaypoints.ForEach(wp => wp.Remove()); + } } linked.Remove(); } @@ -1610,11 +1630,15 @@ namespace Barotrauma } } - private static void PowerUpOutpost(IEnumerable entities) + public static void PowerUpOutpost(Submarine sub) { - foreach (Entity e in entities) + //create a copy of the list, because EntitySpawner may not exist yet we're generating the level, + //which can cause items to be removed/instantiated directly + var entities = MapEntity.MapEntityList.Where(me => me.Submarine == sub).ToList(); + + foreach (MapEntity e in entities) { - if (!(e is Item item)) { continue; } + if (e is not Item item) { continue; } var reactor = item.GetComponent(); if (reactor != null) { @@ -1626,9 +1650,9 @@ namespace Barotrauma for (int i = 0; i < 600; i++) { Powered.UpdatePower((float)Timing.Step); - foreach (Entity e in entities) + foreach (MapEntity e in entities) { - if (!(e is Item item) || item.GetComponent() == null) { continue; } + if (e is not Item item || item.GetComponent() == null) { continue; } item.Update((float)Timing.Step, GameMain.GameScreen.Cam); } } @@ -1683,7 +1707,7 @@ namespace Barotrauma gotoTarget = outpost.GetHulls(true).GetRandom(Rand.RandSync.ServerAndClient); } characterInfo.TeamID = CharacterTeamType.FriendlyNPC; - var npc = Character.Create(CharacterPrefab.HumanSpeciesName, SpawnAction.OffsetSpawnPos(gotoTarget.WorldPosition, 100.0f), ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true); + var npc = Character.Create(characterInfo.SpeciesName, SpawnAction.OffsetSpawnPos(gotoTarget.WorldPosition, 100.0f), ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true); npc.AnimController.FindHull(gotoTarget.WorldPosition, setSubmarine: true); npc.TeamID = CharacterTeamType.FriendlyNPC; npc.HumanPrefab = humanPrefab; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index 535ade368..0569e112d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -81,8 +81,6 @@ namespace Barotrauma get { return base.Prefab.Sprite; } } - public bool IsExteriorWall { get; private set; } = true; - public bool IsPlatform { get { return Prefab.Platform; } @@ -406,8 +404,6 @@ namespace Barotrauma } } - CheckIsExteriorWall(); - #if CLIENT convexHulls?.ForEach(x => x.Move(amount)); @@ -700,41 +696,15 @@ namespace Barotrauma } } - public void CheckIsExteriorWall() + private static Vector2[] CalculateExtremes(Rectangle sectionRect) { - if (!HasBody) - { - IsExteriorWall = false; - return; - } + Vector2[] corners = new Vector2[4]; + corners[0] = new Vector2(sectionRect.X, sectionRect.Y - sectionRect.Height); + corners[1] = new Vector2(sectionRect.X, sectionRect.Y); + corners[2] = new Vector2(sectionRect.Right, sectionRect.Y); + corners[3] = new Vector2(sectionRect.Right, sectionRect.Y - sectionRect.Height); - Vector2 point1 = WorldPosition + BodyOffset * Scale; - //point1 = MathUtils.RotatePointAroundTarget(WorldPosition, point1, BodyRotation); - Vector2 point2 = point1; - - Vector2 normal = new Vector2( - (float)-Math.Sin(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation), - (float)Math.Cos(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation)); - - float thickness = IsHorizontal ? - (BodyHeight > 0 ? BodyHeight : rect.Height) : - (BodyWidth > 0 ? BodyWidth : rect.Width); - - point1 += normal * (thickness / 2 + 16); - point2 -= normal * (thickness / 2 + 16); - - IsExteriorWall = - Hull.FindHullUnoptimized(point1, null, useWorldCoordinates: true) == null || - Hull.FindHullUnoptimized(point2, null, useWorldCoordinates: true) == null; -#if CLIENT - if (convexHulls != null) - { - foreach (ConvexHull ch in convexHulls) - { - ch.IsExteriorWall = IsExteriorWall; - } - } -#endif + return corners; } /// @@ -742,9 +712,9 @@ namespace Barotrauma /// public static Structure GetAttachTarget(Vector2 worldPosition) { - foreach (MapEntity mapEntity in mapEntityList) + foreach (MapEntity mapEntity in MapEntityList) { - if (mapEntity is not Structure structure) { continue; } + if (!(mapEntity is Structure structure)) { continue; } if (!structure.Prefab.AllowAttachItems) { continue; } if (structure.Bodies != null && structure.Bodies.Count > 0) { continue; } Rectangle worldRect = mapEntity.WorldRect; @@ -1273,7 +1243,7 @@ namespace Barotrauma UpdateSections(); } - private void CreateWallDamageExplosion(Gap gap, Character attacker) + private static void CreateWallDamageExplosion(Gap gap, Character attacker) { const float explosionRange = 750.0f; float explosionStrength = gap.Open; @@ -1294,20 +1264,23 @@ namespace Barotrauma if (explosionOnBroken == null) { - explosionOnBroken = new Explosion(explosionRange * gap.Open, force: 10.0f, damage: 0.0f, structureDamage: 0.0f, itemDamage: 0.0f); + explosionOnBroken = new Explosion(explosionRange, force: 10.0f, damage: 0.0f, structureDamage: 0.0f, itemDamage: 0.0f); if (AfflictionPrefab.Prefabs.TryGet("lacerations".ToIdentifier(), out AfflictionPrefab lacerations)) { - explosionOnBroken.Attack.Afflictions.Add(lacerations.Instantiate(50.0f), null); + explosionOnBroken.Attack.Afflictions.Add(lacerations.Instantiate(3.0f), null); } else { - explosionOnBroken.Attack.Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(5.0f), null); + explosionOnBroken.Attack.Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(3.0f), null); } + explosionOnBroken.IgnoreCover = true; explosionOnBroken.OnlyInside = true; explosionOnBroken.DisableParticles(); } + explosionOnBroken.Attack.Range = explosionRange * gap.Open; explosionOnBroken.Attack.DamageMultiplier = explosionStrength; + explosionOnBroken.Attack.Stun = MathHelper.Clamp(explosionStrength, 0.5f, 1.0f); explosionOnBroken?.Explode(gap.WorldPosition, damageSource: null, attacker: attacker); #if CLIENT if (linkedHull != null) @@ -1669,7 +1642,6 @@ namespace Barotrauma { SetDamage(i, Sections[i].damage, createNetworkEvent: false, createExplosionEffect: false); } - CheckIsExteriorWall(); } public virtual void Reset() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index 57d2d0424..fad619258 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -361,11 +361,11 @@ namespace Barotrauma List pumps = new List(); List allItems = GetItems(true); - bool anyHasTag = allItems.Any(i => i.HasTag("ballast")); + bool anyHasTag = allItems.Any(i => i.HasTag(Tags.Ballast)); foreach (Item item in allItems) { - if ((!anyHasTag || item.HasTag("ballast")) && item.GetComponent() is { } pump) + if ((!anyHasTag || item.HasTag(Tags.Ballast)) && item.GetComponent() is { } pump) { pumps.Add(pump); } @@ -624,11 +624,18 @@ namespace Barotrauma //math/physics stuff ---------------------------------------------------- - public static Vector2 VectorToWorldGrid(Vector2 position) + public static Vector2 VectorToWorldGrid(Vector2 position, bool round = false) { - position.X = (float)Math.Floor(position.X / GridSize.X) * GridSize.X; - position.Y = (float)Math.Ceiling(position.Y / GridSize.Y) * GridSize.Y; - + if (round) + { + position.X = MathF.Round(position.X / GridSize.X) * GridSize.X; + position.Y = MathF.Round(position.Y / GridSize.Y) * GridSize.Y; + } + else + { + position.X = MathF.Floor(position.X / GridSize.X) * GridSize.X; + position.Y = MathF.Ceiling(position.Y / GridSize.Y) * GridSize.Y; + } return position; } @@ -636,7 +643,7 @@ namespace Barotrauma { List entities = onlyHulls ? Hull.HullList.FindAll(h => h.Submarine == this).Cast().ToList() : - MapEntity.mapEntityList.FindAll(me => me.Submarine == this); + MapEntity.MapEntityList.FindAll(me => me.Submarine == this); //ignore items whose body is disabled (wires, items inside cabinets) entities.RemoveAll(e => @@ -693,6 +700,22 @@ namespace Barotrauma return new Rectangle((int)pos.X, (int)pos.Y, (int)size.X, (int)size.Y); } + public static RectangleF AbsRectF(Vector2 pos, Vector2 size) + { + if (size.X < 0.0f) + { + pos.X += size.X; + size.X = -size.X; + } + if (size.Y < 0.0f) + { + pos.Y += size.Y; + size.Y = -size.Y; + } + + return new RectangleF(pos.X, pos.Y, size.X, size.Y); + } + public static bool RectContains(Rectangle rect, Vector2 pos, bool inclusive = false) { if (inclusive) @@ -721,6 +744,18 @@ namespace Barotrauma } } + public static bool RectsOverlap(RectangleF rect1, RectangleF rect2, bool inclusive = true) + { + if (inclusive) + { + return !(rect1.X > rect2.X + rect2.Width || rect1.X + rect1.Width < rect2.X || + rect1.Y < rect2.Y - rect2.Height || rect1.Y - rect1.Height > rect2.Y); + } + + return !(rect1.X >= rect2.X + rect2.Width || rect1.X + rect1.Width <= rect2.X || + rect1.Y <= rect2.Y - rect2.Height || rect1.Y - rect1.Height >= rect2.Y); + } + public static Body PickBody(Vector2 rayStart, Vector2 rayEnd, IEnumerable ignoredBodies = null, Category? collisionCategory = null, bool ignoreSensors = true, Predicate customPredicate = null, bool allowInsideFixture = false) { if (Vector2.DistanceSquared(rayStart, rayEnd) < 0.0001f) @@ -965,7 +1000,7 @@ namespace Barotrauma Item.UpdateHulls(); List bodyItems = Item.ItemList.FindAll(it => it.Submarine == this && it.body != null); - List subEntities = MapEntity.mapEntityList.FindAll(me => me.Submarine == this); + List subEntities = MapEntity.MapEntityList.FindAll(me => me.Submarine == this); foreach (MapEntity e in subEntities) { @@ -1047,7 +1082,7 @@ namespace Barotrauma public void EnableFactionSpecificEntities(Identifier factionIdentifier) { - foreach (MapEntity me in MapEntity.mapEntityList) + foreach (MapEntity me in MapEntity.MapEntityList) { if (string.IsNullOrEmpty(me.Layer) || me.Submarine != this) { continue; } @@ -1202,7 +1237,7 @@ namespace Barotrauma if (item.Submarine != this) { continue; } var pump = item.GetComponent(); if (pump == null || item.CurrentHull == null) { continue; } - if (!item.HasTag("ballast") && !item.CurrentHull.RoomName.Contains("ballast", StringComparison.OrdinalIgnoreCase)) { continue; } + if (!item.HasTag(Tags.Ballast) && !item.CurrentHull.RoomName.Contains("ballast", StringComparison.OrdinalIgnoreCase)) { continue; } pump.FlowPercentage = 0.0f; ballastHulls.Add(item.CurrentHull); } @@ -1326,7 +1361,7 @@ namespace Barotrauma foreach (Item item in Item.ItemList.ToList()) { if (!connectedSubs.Contains(item.Submarine)) { continue; } - if (!item.HasTag("cargocontainer")) { continue; } + if (!item.HasTag(Tags.CargoContainer)) { continue; } if (item.NonInteractable || item.HiddenInGame) { continue; } var itemContainer = item.GetComponent(); if (itemContainer == null) { continue; } @@ -1358,22 +1393,38 @@ namespace Barotrauma } /// - /// Finds the sub whose borders contain the position + /// Finds the sub whose borders contain the position. Note that this method uses the "actual" position of the sub outside the level: + /// only use this if the position is in a submarine's local coordinate space! /// - public static Submarine FindContaining(Vector2 position) + public static Submarine FindContainingInLocalCoordinates(Vector2 position, float inflate = 500.0f) { foreach (Submarine sub in Loaded) { Rectangle subBorders = sub.Borders; - subBorders.Location += MathUtils.ToPoint(sub.HiddenSubPosition) - new Microsoft.Xna.Framework.Point(0, sub.Borders.Height); - - subBorders.Inflate(500.0f, 500.0f); - - if (subBorders.Contains(position)) return sub; + subBorders.Location += MathUtils.ToPoint(sub.HiddenSubPosition) - new Point(0, sub.Borders.Height); + subBorders.Inflate(inflate, inflate); + if (subBorders.Contains(position)) { return sub; } } return null; } + + /// + /// Finds the sub whose world borders contain the position. + /// + public static Submarine FindContaining(Vector2 worldPosition, float inflate = 500.0f) + { + foreach (Submarine sub in Loaded) + { + Rectangle worldBorders = sub.Borders; + worldBorders.Location += sub.WorldPosition.ToPoint() - new Point(0, sub.Borders.Height); + worldBorders.Inflate(inflate, inflate); + if (worldBorders.Contains(worldPosition)) { return sub; } + } + return null; + } + + public static Rectangle GetBorders(XElement submarineElement) { Vector4 bounds = Vector4.Zero; @@ -1430,7 +1481,7 @@ namespace Barotrauma HiddenSubPosition = new Vector2( //1st sub on the left side, 2nd on the right, etc - HiddenSubPosition.X * (i % 2 == 0 ? 1 : -1), + -HiddenSubPosition.X, HiddenSubPosition.Y + sub.Borders.Height + 5000.0f); } @@ -1479,7 +1530,7 @@ namespace Barotrauma center.X -= center.X % GridSize.X; center.Y -= center.Y % GridSize.Y; - RepositionEntities(-center, MapEntity.mapEntityList.Where(me => me.Submarine == this)); + RepositionEntities(-center, MapEntity.MapEntityList.Where(me => me.Submarine == this)); } subBody = new SubmarineBody(this, showErrorMessages); @@ -1497,7 +1548,7 @@ namespace Barotrauma !GameMain.NetworkMember.ServerSettings.DestructibleOutposts && !(info.OutpostGenerationParams?.AlwaysDestructible ?? false); - foreach (MapEntity me in MapEntity.mapEntityList) + foreach (MapEntity me in MapEntity.MapEntityList) { if (me.Submarine != this) { continue; } if (me is Item item) @@ -1554,16 +1605,16 @@ namespace Barotrauma } entityGrid = Hull.GenerateEntityGrid(this); - for (int i = 0; i < MapEntity.mapEntityList.Count; i++) + for (int i = 0; i < MapEntity.MapEntityList.Count; i++) { - if (MapEntity.mapEntityList[i].Submarine != this) { continue; } - MapEntity.mapEntityList[i].Move(HiddenSubPosition, ignoreContacts: true); + if (MapEntity.MapEntityList[i].Submarine != this) { continue; } + MapEntity.MapEntityList[i].Move(HiddenSubPosition, ignoreContacts: true); } Loading = false; MapEntity.MapLoaded(newEntities, true); - foreach (MapEntity me in MapEntity.mapEntityList) + foreach (MapEntity me in MapEntity.MapEntityList) { if (me.Submarine != this) { continue; } if (me is LinkedSubmarine linkedSub) @@ -1653,7 +1704,7 @@ namespace Barotrauma public bool CheckFuel() { - float fuel = GetItems(true).Where(i => i.HasTag("reactorfuel")).Sum(i => i.Condition); + float fuel = GetItems(true).Where(i => i.HasTag(Tags.Fuel)).Sum(i => i.Condition); Info.LowFuel = fuel < 200; return !Info.LowFuel; } @@ -1681,7 +1732,7 @@ namespace Barotrauma element.Add(new XAttribute("dimensions", XMLExtensions.Vector2ToString(dimensions.Size.ToVector2()))); var cargoContainers = GetCargoContainers(); int cargoCapacity = cargoContainers.Sum(c => c.container.Capacity); - foreach (MapEntity me in MapEntity.mapEntityList) + foreach (MapEntity me in MapEntity.MapEntityList) { if (me is LinkedSubmarine linkedSub && linkedSub.Submarine == this) { @@ -1702,13 +1753,12 @@ namespace Barotrauma foreach (Item item in Item.ItemList) { - if (item.PendingItemSwap?.SwappableItem?.ConnectedItemsToSwap == null) { continue; } - foreach (var (requiredTag, swapTo) in item.PendingItemSwap.SwappableItem.ConnectedItemsToSwap) + if (item.PendingItemSwap?.SwappableItem?.ConnectedItemsToSwap is not { } connectedItemsToSwap) { continue; } + foreach (var (requiredTag, swapTo) in connectedItemsToSwap) { List itemsToSwap = new List(); itemsToSwap.AddRange(item.linkedTo.Where(lt => (lt as Item)?.HasTag(requiredTag) ?? false).Cast()); - var connectionPanel = item.GetComponent(); - if (connectionPanel != null) + if (item.GetComponent() is ConnectionPanel connectionPanel) { foreach (Connection c in connectionPanel.Connections) { @@ -1730,13 +1780,13 @@ namespace Barotrauma foreach (Item itemToSwap in itemsToSwap) { itemToSwap.PurchasedNewSwap = item.PurchasedNewSwap; - if (itemPrefab != itemToSwap.Prefab) { itemToSwap.PendingItemSwap = itemPrefab; } + if (itemPrefab != itemToSwap.Prefab) { itemToSwap.PendingItemSwap = itemPrefab; } } } } Dictionary savedEntities = new Dictionary(); - foreach (MapEntity e in MapEntity.mapEntityList.OrderBy(e => e.ID)) + foreach (MapEntity e in MapEntity.MapEntityList.OrderBy(e => e.ID)) { if (!e.ShouldBeSaved) { continue; } @@ -1980,25 +2030,33 @@ namespace Barotrauma { var connectedWp = connection.Waypoint; if (connectedWp.IsObstructed || connectedWp.Ladders != null) { continue; } - Vector2 start = ConvertUnits.ToSimUnits(wp.WorldPosition) - otherSub.SimPosition; - Vector2 end = ConvertUnits.ToSimUnits(connectedWp.WorldPosition) - otherSub.SimPosition; - var body = PickBody(start, end, null, Physics.CollisionWall, allowInsideFixture: true); - if (body != null) + bool isObstructed = wp.CurrentHull is Hull h && h.Submarine != this; + if (!isObstructed) { - if (body.UserData is Structure wall && !wall.IsPlatform || body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) + Vector2 start = ConvertUnits.ToSimUnits(wp.WorldPosition) - otherSub.SimPosition; + Vector2 end = ConvertUnits.ToSimUnits(connectedWp.WorldPosition) - otherSub.SimPosition; + var body = PickBody(start, end, null, Physics.CollisionWall, allowInsideFixture: true); + if (body != null) { - connectedWp.IsObstructed = true; - wp.IsObstructed = true; - if (!obstructedNodes.TryGetValue(otherSub, out HashSet nodes)) + if (body.UserData is Structure wall && !wall.IsPlatform || body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { - nodes = new HashSet(); - obstructedNodes.Add(otherSub, nodes); + isObstructed = true; } - nodes.Add(node); - nodes.Add(connection); - break; } } + if (isObstructed) + { + connectedWp.IsObstructed = true; + wp.IsObstructed = true; + if (!obstructedNodes.TryGetValue(otherSub, out HashSet nodes)) + { + nodes = new HashSet(); + obstructedNodes.Add(otherSub, nodes); + } + nodes.Add(node); + nodes.Add(connection); + break; + } } } } @@ -2054,5 +2112,47 @@ namespace Barotrauma } return selectedContainer; } + + public static Vector2 GetRelativeSimPosition(ISpatialEntity from, ISpatialEntity to, Vector2? targetWorldPos = null) + { + return targetWorldPos.HasValue ? + GetRelativeSimPositionFromWorldPosition(targetWorldPos.Value, from.Submarine, to.Submarine) : + GetRelativeSimPosition(to.SimPosition, from.Submarine, to.Submarine); + } + + public static Vector2 GetRelativeSimPositionFromWorldPosition(Vector2 targetWorldPos, Submarine fromSub, Submarine toSub) + { + Vector2 worldPos = targetWorldPos; + if (toSub != null) + { + worldPos -= toSub.Position; + } + return GetRelativeSimPosition(ConvertUnits.ToSimUnits(worldPos), fromSub, toSub); + } + + public static Vector2 GetRelativeSimPosition(Vector2 targetSimPos, Submarine fromSub, Submarine toSub) + { + Vector2 targetPos = targetSimPos; + if (fromSub == null && toSub != null) + { + // outside and targeting inside + targetPos += toSub.SimPosition; + } + else if (fromSub != null && toSub == null) + { + // inside and targeting outside + targetPos -= fromSub.SimPosition; + } + else if (fromSub != toSub) + { + if (fromSub != null && toSub != null) + { + // both inside, but in different subs + Vector2 diff = fromSub.SimPosition - toSub.SimPosition; + targetPos -= diff; + } + } + return targetPos; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index 064687270..e0d060c04 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -163,7 +163,7 @@ namespace Barotrauma { farseerBody.BodyType = BodyType.Static; } - foreach (var mapEntity in MapEntity.mapEntityList) + foreach (var mapEntity in MapEntity.MapEntityList) { if (mapEntity.Submarine != submarine || mapEntity is not Structure wall) { continue; } @@ -521,20 +521,25 @@ namespace Barotrauma { if (Submarine.LockY) { return Vector2.Zero; } + //calculate the buoyancy for all connected subs + //doing it separately for each connected sub means e.g. a flooded drone barely + //affects the buoyancy of the main sub even if there was as much water in the + //drone as the whole ballast volume of the sub + var connectedSubs = submarine.GetConnectedSubs(); float waterVolume = 0.0f; float volume = 0.0f; + float totalMass = connectedSubs.Sum(s => s.SubBody.Body.Mass); foreach (Hull hull in Hull.HullList) { - if (hull.Submarine != submarine) { continue; } - + if (hull.Submarine == null || !connectedSubs.Contains(hull.Submarine)) { continue; } + if (hull.Submarine.PhysicsBody is not { BodyType: BodyType.Dynamic }) { continue; } waterVolume += hull.WaterVolume; - volume += hull.Volume; + volume += hull.Volume; } - float waterPercentage = volume <= 0.0f ? 0.0f : waterVolume / volume; - + float waterPercentage = volume <= 0.0f ? 0.0f : waterVolume / volume; float buoyancy = NeutralBallastPercentage - waterPercentage; - + float massRatio = Body.Mass / totalMass; if (buoyancy > 0.0f) { buoyancy *= 2.0f; @@ -543,13 +548,11 @@ namespace Barotrauma { buoyancy = Math.Max(buoyancy, -0.5f); } - if (forceUpwardsTimer > 0.0f) { buoyancy = MathHelper.Lerp(buoyancy, 0.1f, forceUpwardsTimer / ForceUpwardsDelay); } - - return new Vector2(0.0f, buoyancy * Body.Mass * 10.0f); + return new Vector2(0.0f, buoyancy * Body.Mass * 10.0f) * massRatio; } public void ApplyForce(Vector2 force) @@ -1009,7 +1012,8 @@ namespace Barotrauma //stun for up to 2 second if the impact equal or higher to the maximum impact if (impact >= MaxCollisionImpact) { - c.AddDamage(impactPos, AfflictionPrefab.ImpactDamage.Instantiate(3.0f).ToEnumerable(), stun: Math.Min(impulse.Length() * 0.2f, 2.0f), playSound: true); + float impactDamage = c.AnimController.GetImpactDamage(impact); + c.AddDamage(impactPos, AfflictionPrefab.ImpactDamage.Instantiate(impactDamage).ToEnumerable(), stun: Math.Min(impulse.Length() * 0.2f, 2.0f), playSound: true); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index 12ea2fef8..a8a0551db 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -562,7 +562,7 @@ namespace Barotrauma removals.ForEach(wp => wp.Remove()); removals.Clear(); // Stairs - foreach (MapEntity mapEntity in mapEntityList.ToList()) + foreach (MapEntity mapEntity in MapEntityList.ToList()) { if (!(mapEntity is Structure structure)) { continue; } if (structure.StairDirection == Direction.None) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs index 704d3f9db..e232ae070 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs @@ -282,12 +282,12 @@ namespace Barotrauma.Networking return length; } - public static bool CanUseRadio(Character sender) + public static bool CanUseRadio(Character sender, bool ignoreJamming = false) { - return CanUseRadio(sender, out _); + return CanUseRadio(sender, out _, ignoreJamming); } - public static bool CanUseRadio(Character sender, out WifiComponent radio) + public static bool CanUseRadio(Character sender, out WifiComponent radio, bool ignoreJamming = false) { radio = null; if (sender?.Inventory == null || sender.Removed) { return false; } @@ -295,7 +295,7 @@ namespace Barotrauma.Networking foreach (Item item in sender.Inventory.AllItems) { var wifiComponent = item.GetComponent(); - if (wifiComponent == null || !wifiComponent.LinkToChat || !wifiComponent.CanTransmit() || !sender.HasEquippedItem(item)) { continue; } + if (wifiComponent == null || !wifiComponent.LinkToChat || !wifiComponent.CanTransmit(ignoreJamming) || !sender.HasEquippedItem(item)) { continue; } if (radio == null || wifiComponent.Range > radio.Range) { radio = wifiComponent; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs index e88558001..b3b47676e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs @@ -80,7 +80,7 @@ namespace Barotrauma.Networking { if (value != null) { - DebugConsole.NewMessage(value.Name, Microsoft.Xna.Framework.Color.Yellow); + DebugConsole.NewMessage(value.Name, Color.Yellow); } } character = value; @@ -90,12 +90,6 @@ namespace Barotrauma.Networking UsingFreeCam = false; #if CLIENT GameMain.GameSession?.CrewManager?.SetPlayerVoiceIconState(this, muted, mutedLocally); - - if (character == GameMain.Client.Character && GameMain.Client.SpawnAsTraitor) - { - character.IsTraitor = true; - character.TraitorCurrentObjective = GameMain.Client.TraitorFirstObjective; - } #endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs index 12be53f66..a5067d7c6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs @@ -194,8 +194,7 @@ namespace Barotrauma } } - private readonly Queue spawnQueue; - private readonly Queue removeQueue; + private readonly Queue> spawnOrRemoveQueue; public abstract class SpawnOrRemove : NetEntityEvent.IData { @@ -255,8 +254,7 @@ namespace Barotrauma public EntitySpawner() : base(null, Entity.EntitySpawnerID) { - spawnQueue = new Queue(); - removeQueue = new Queue(); + spawnOrRemoveQueue = new Queue>(); } public override string ToString() @@ -274,7 +272,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue1:ItemPrefabNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } - spawnQueue.Enqueue(new ItemSpawnInfo(itemPrefab, worldPosition, onSpawned, condition, quality)); + spawnOrRemoveQueue.Enqueue(new ItemSpawnInfo(itemPrefab, worldPosition, onSpawned, condition, quality)); } public void AddItemToSpawnQueue(ItemPrefab itemPrefab, Vector2 position, Submarine sub, float? condition = null, int? quality = null, Action onSpawned = null) @@ -287,7 +285,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue2:ItemPrefabNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } - spawnQueue.Enqueue(new ItemSpawnInfo(itemPrefab, position, sub, onSpawned, condition, quality)); + spawnOrRemoveQueue.Enqueue(new ItemSpawnInfo(itemPrefab, position, sub, onSpawned, condition, quality)); } public void AddItemToSpawnQueue(ItemPrefab itemPrefab, Inventory inventory, float? condition = null, int? quality = null, Action onSpawned = null, bool spawnIfInventoryFull = true, bool ignoreLimbSlots = false, InvSlotType slot = InvSlotType.None) @@ -300,7 +298,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue3:ItemPrefabNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } - spawnQueue.Enqueue(new ItemSpawnInfo(itemPrefab, inventory, onSpawned, condition, quality) + spawnOrRemoveQueue.Enqueue(new ItemSpawnInfo(itemPrefab, inventory, onSpawned, condition, quality) { SpawnIfInventoryFull = spawnIfInventoryFull, IgnoreLimbSlots = ignoreLimbSlots, @@ -318,7 +316,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue4:SpeciesNameNullOrEmpty", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } - spawnQueue.Enqueue(new CharacterSpawnInfo(speciesName, worldPosition, onSpawn)); + spawnOrRemoveQueue.Enqueue(new CharacterSpawnInfo(speciesName, worldPosition, onSpawn)); } public void AddCharacterToSpawnQueue(Identifier speciesName, Vector2 position, Submarine sub, Action onSpawn = null) @@ -331,7 +329,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue5:SpeciesNameNullOrEmpty", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } - spawnQueue.Enqueue(new CharacterSpawnInfo(speciesName, position, sub, onSpawn)); + spawnOrRemoveQueue.Enqueue(new CharacterSpawnInfo(speciesName, position, sub, onSpawn)); } public void AddCharacterToSpawnQueue(Identifier speciesName, Vector2 worldPosition, CharacterInfo characterInfo, Action onSpawn = null) @@ -344,13 +342,13 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue4:SpeciesNameNullOrEmpty", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } - spawnQueue.Enqueue(new CharacterSpawnInfo(speciesName, worldPosition, characterInfo, onSpawn)); + spawnOrRemoveQueue.Enqueue(new CharacterSpawnInfo(speciesName, worldPosition, characterInfo, onSpawn)); } public void AddEntityToRemoveQueue(Entity entity) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } - if (removeQueue.Contains(entity) || entity.Removed || entity == null || entity.IdFreed) { return; } + if (entity == null || IsInRemoveQueue(entity) || entity.Removed || entity.IdFreed) { return; } if (entity is Item item) { AddItemToRemoveQueue(item); return; } if (entity is Character) { @@ -364,15 +362,15 @@ namespace Barotrauma #endif } - removeQueue.Enqueue(entity); + spawnOrRemoveQueue.Enqueue(entity); } public void AddItemToRemoveQueue(Item item) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } - if (removeQueue.Contains(item) || item.Removed) { return; } + if (IsInRemoveQueue(item) || item.Removed) { return; } - removeQueue.Enqueue(item); + spawnOrRemoveQueue.Enqueue(item); var containedItems = item.OwnInventory?.AllItems; if (containedItems == null) { return; } foreach (Item containedItem in containedItems) @@ -389,7 +387,11 @@ namespace Barotrauma /// public bool IsInSpawnQueue(Predicate predicate) { - return spawnQueue.Any(s => predicate(s)); + foreach (var spawnOrRemove in spawnOrRemoveQueue) + { + if (spawnOrRemove.TryGet(out IEntitySpawnInfo spawnInfo) && predicate(spawnInfo)) { return true; } + } + return false; } /// @@ -397,43 +399,51 @@ namespace Barotrauma /// public int CountSpawnQueue(Predicate predicate) { - return spawnQueue.Count(s => predicate(s)); + int count = 0; + foreach (var spawnOrRemove in spawnOrRemoveQueue) + { + if (spawnOrRemove.TryGet(out IEntitySpawnInfo spawnInfo) && predicate(spawnInfo)) { count++; } + } + return count; } public bool IsInRemoveQueue(Entity entity) { - return removeQueue.Contains(entity); + foreach (var spawnOrRemove in spawnOrRemoveQueue) + { + if (spawnOrRemove.TryGet(out Entity entityToRemove) && entityToRemove == entity) { return true; } + } + return false; } public void Update(bool createNetworkEvents = true) { if (GameMain.NetworkMember is { IsClient: true }) { return; } - while (spawnQueue.Count > 0) + while (spawnOrRemoveQueue.Count > 0) { - var entitySpawnInfo = spawnQueue.Dequeue(); - - var spawnedEntity = entitySpawnInfo.Spawn(); - if (spawnedEntity == null) { continue; } - - if (createNetworkEvents) + var spawnOrRemove = spawnOrRemoveQueue.Dequeue(); + if (spawnOrRemove.TryGet(out Entity entityToRemove)) + { + if (entityToRemove is Item item) + { + item.SendPendingNetworkUpdates(); + } + if (createNetworkEvents) + { + CreateNetworkEventProjSpecific(new RemoveEntity(entityToRemove)); + } + entityToRemove.Remove(); + } + else if (spawnOrRemove.TryGet(out IEntitySpawnInfo spawnInfo)) { - CreateNetworkEventProjSpecific(new SpawnEntity(spawnedEntity)); + var spawnedEntity = spawnInfo.Spawn(); + if (spawnedEntity == null) { continue; } + if (createNetworkEvents) + { + CreateNetworkEventProjSpecific(new SpawnEntity(spawnedEntity)); + } + spawnInfo.OnSpawned(spawnedEntity); } - entitySpawnInfo.OnSpawned(spawnedEntity); - } - - while (removeQueue.Count > 0) - { - var removedEntity = removeQueue.Dequeue(); - if (removedEntity is Item item) - { - item.SendPendingNetworkUpdates(); - } - if (createNetworkEvents) - { - CreateNetworkEventProjSpecific(new RemoveEntity(removedEntity)); - } - removedEntity.Remove(); } } @@ -441,8 +451,7 @@ namespace Barotrauma public void Reset() { - removeQueue.Clear(); - spawnQueue.Clear(); + spawnOrRemoveQueue.Clear(); #if CLIENT receivedEvents.Clear(); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index 4e56ebe95..4c9adeb13 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -35,6 +35,7 @@ namespace Barotrauma.Networking MEDICAL, //medical clinic TRANSFER_MONEY, // wallet transfers REWARD_DISTRIBUTION, // wallet reward distribution + CIRCUITBOX, READY_CHECK, READY_TO_SPAWN } @@ -80,11 +81,12 @@ namespace Barotrauma.Networking STARTGAMEFINALIZE, //finalize round initialization ENDGAME, - TRAITOR_MESSAGE, MISSION, EVENTACTION, + TRAITOR_MESSAGE, CREW, //anything related to managing bots in multiplayer MEDICAL, //medical clinic + CIRCUITBOX, MONEY, READY_CHECK //start, end and update a ready check } @@ -112,14 +114,6 @@ namespace Barotrauma.Networking EntityId: entity.ID); } - enum TraitorMessageType - { - Server, - ServerMessageBox, - Objective, - Console - } - enum VoteType { Unknown, @@ -131,7 +125,8 @@ namespace Barotrauma.Networking PurchaseAndSwitchSub, PurchaseSub, SwitchSub, - TransferMoney + TransferMoney, + Traitor, } public enum ReadyCheckState diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkExtensions.cs index ee01e2197..40c0e92b1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkExtensions.cs @@ -61,7 +61,7 @@ namespace Barotrauma.Networking deliveryMethod switch { DeliveryMethod.Reliable => Steamworks.P2PSend.Reliable, - DeliveryMethod.ReliableOrdered => Steamworks.P2PSend.Unreliable, + DeliveryMethod.ReliableOrdered => Steamworks.P2PSend.Reliable, _ => Steamworks.P2PSend.Unreliable }; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs index e19220255..658fee626 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs @@ -14,7 +14,7 @@ namespace Barotrauma.Networking /// /// How much skills drop towards the job's default skill levels when dying /// - const float SkillReductionOnDeath = 0.75f; + public static float SkillLossPercentageOnDeath => GameMain.NetworkMember?.ServerSettings?.SkillLossPercentageOnDeath ?? 50.0f; public enum State { @@ -26,7 +26,6 @@ namespace Barotrauma.Networking private readonly NetworkMember networkMember; private readonly Steering shuttleSteering; private readonly List shuttleDoors; - private const string RespawnContainerTag = "respawncontainer"; private readonly ItemContainer respawnContainer; //items created during respawn @@ -109,7 +108,7 @@ namespace Barotrauma.Networking { if (item.Submarine != RespawnShuttle) { continue; } - if (item.HasTag(RespawnContainerTag)) + if (item.HasTag(Tags.RespawnContainer)) { respawnContainer = item.GetComponent(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs index 91ebdc203..49c55f316 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs @@ -42,6 +42,7 @@ namespace Barotrauma.Networking DoSProtection, Karma, Talent, + Traitors, Error, } @@ -59,6 +60,7 @@ namespace Barotrauma.Networking { MessageType.DoSProtection, Color.OrangeRed }, { MessageType.Karma, new Color(75, 88, 255) }, { MessageType.Talent, new Color(125, 125, 255) }, + { MessageType.Traitors, new Color(107, 69, 158) }, { MessageType.Error, Color.Red } }; @@ -76,6 +78,7 @@ namespace Barotrauma.Networking { MessageType.DoSProtection, "DoSProtection" }, { MessageType.Karma, "Karma" }, { MessageType.Talent, "Talent" }, + { MessageType.Traitors, "Traitors" }, { MessageType.Error, "Error" } }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index 88c0c9776..b6dfb0840 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -15,11 +15,6 @@ namespace Barotrauma.Networking Manual = 0, Random = 1, Vote = 2 } - public enum YesNoMaybe - { - No = 0, Maybe = 1, Yes = 2 - } - public enum BotSpawnMode { Normal, Fill @@ -437,6 +432,16 @@ namespace Barotrauma.Networking private set; } + [Serialize(50f, IsPropertySaveable.Yes)] + /// + /// How much skills drop towards the job's default skill levels when dying + /// + public float SkillLossPercentageOnDeath + { + get; + private set; + } + [Serialize(60.0f, IsPropertySaveable.Yes)] public float AutoRestartInterval { @@ -485,13 +490,6 @@ namespace Barotrauma.Networking private set; } = true; - [Serialize(true, IsPropertySaveable.Yes)] - public bool AllowRagdollButton - { - get; - set; - } - [Serialize(true, IsPropertySaveable.Yes)] public bool AllowFileTransfers { @@ -722,19 +720,33 @@ namespace Barotrauma.Networking set; } - private YesNoMaybe traitorsEnabled; - [Serialize(YesNoMaybe.No, IsPropertySaveable.Yes)] - public YesNoMaybe TraitorsEnabled + private float traitorProbability; + [Serialize(0.0f, IsPropertySaveable.Yes)] + public float TraitorProbability { - get { return traitorsEnabled; } + get { return traitorProbability; } set { - if (traitorsEnabled == value) { return; } - traitorsEnabled = value; + if (MathUtils.NearlyEqual(traitorProbability, value)) { return; } + traitorProbability = MathHelper.Clamp(value, 0.0f, 1.0f); ServerDetailsChanged = true; } } + + private int traitorDangerLevel; + [Serialize(TraitorEventPrefab.MinDangerLevel, IsPropertySaveable.Yes)] + public int TraitorDangerLevel + { + get { return traitorDangerLevel; } + set + { + int clampedValue = MathHelper.Clamp(value, TraitorEventPrefab.MinDangerLevel, TraitorEventPrefab.MaxDangerLevel); + if (traitorDangerLevel == clampedValue) { return; } + traitorDangerLevel = clampedValue; + ServerDetailsChanged = true; + } + } [Serialize(defaultValue: 1, isSaveable: IsPropertySaveable.Yes)] public int TraitorsMinPlayerCount { @@ -742,34 +754,13 @@ namespace Barotrauma.Networking set; } - [Serialize(defaultValue: 90.0f, isSaveable: IsPropertySaveable.Yes)] - public float TraitorsMinStartDelay + [Serialize(defaultValue: 50.0f, isSaveable: IsPropertySaveable.Yes)] + public float MinPercentageOfPlayersForTraitorAccusation { get; set; } - [Serialize(defaultValue: 180.0f, isSaveable: IsPropertySaveable.Yes)] - public float TraitorsMaxStartDelay - { - get; - set; - } - - [Serialize(defaultValue: 30.0f, isSaveable: IsPropertySaveable.Yes)] - public float TraitorsMinRestartDelay - { - get; - set; - } - - [Serialize(defaultValue: 90.0f, isSaveable: IsPropertySaveable.Yes)] - public float TraitorsMaxRestartDelay - { - get; - set; - } - [Serialize(defaultValue: "", IsPropertySaveable.Yes)] public LanguageIdentifier Language { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs index 72e237b6b..1b9392702 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs @@ -30,6 +30,12 @@ namespace Barotrauma public static T HighestVoted(VoteType voteType, IEnumerable voters) { + return HighestVoted(voteType, voters, out _); + } + + public static T HighestVoted(VoteType voteType, IEnumerable voters, out int voteCount) + { + voteCount = 0; if (voteType == VoteType.Sub && !GameMain.NetworkMember.ServerSettings.AllowSubVoting) { return default; } if (voteType == VoteType.Mode && !GameMain.NetworkMember.ServerSettings.AllowModeVoting) { return default; } @@ -52,7 +58,7 @@ namespace Barotrauma selected = votable.Key; } } - + voteCount = highestVotes; return selected; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index c4873a2cf..7f9b856b1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -869,11 +869,21 @@ namespace Barotrauma } } - public void UpdateDrawPosition() + public void UpdateDrawPosition(bool interpolate = true) { - drawPosition = Timing.Interpolate(prevPosition, FarseerBody.Position); - drawPosition = ConvertUnits.ToDisplayUnits(drawPosition + drawOffset); - drawRotation = Timing.InterpolateRotation(prevRotation, FarseerBody.Rotation) + rotationOffset; + if (interpolate) + { + drawPosition = Timing.Interpolate(prevPosition, FarseerBody.Position); + drawPosition = ConvertUnits.ToDisplayUnits(drawPosition + drawOffset); + drawRotation = Timing.InterpolateRotation(prevRotation, FarseerBody.Rotation) + rotationOffset; + } + else + { + drawPosition = prevPosition = ConvertUnits.ToDisplayUnits(FarseerBody.Position); + drawRotation = prevRotation = FarseerBody.Rotation; + drawOffset = Vector2.Zero; + drawRotation = 0.0f; + } } public void CorrectPosition(List positionBuffer, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs index 9b8bc37d7..ceb11f4cf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs @@ -162,26 +162,25 @@ namespace Barotrauma sw.Stop(); GameMain.PerformanceCounter.AddElapsedTicks("Update:Level", sw.ElapsedTicks); - if (Character.Controlled != null) + if (Character.Controlled is { } controlled) { - if (Character.Controlled.SelectedItem != null && Character.Controlled.CanInteractWith(Character.Controlled.SelectedItem)) + if (controlled.SelectedItem != null && controlled.CanInteractWith(controlled.SelectedItem)) { - Character.Controlled.SelectedItem.UpdateHUD(cam, Character.Controlled, (float)deltaTime); + controlled.SelectedItem.UpdateHUD(cam, controlled, (float)deltaTime); } - if (Character.Controlled.Inventory != null) + if (controlled.Inventory != null) { - foreach (Item item in Character.Controlled.Inventory.AllItems) + foreach (Item item in controlled.Inventory.AllItems) { - if (Character.Controlled.HasEquippedItem(item)) + if (controlled.HasEquippedItem(item)) { - item.UpdateHUD(cam, Character.Controlled, (float)deltaTime); + item.UpdateHUD(cam, controlled, (float)deltaTime); } } } } - - sw.Restart(); + sw.Restart(); Character.UpdateAll((float)deltaTime, cam); #elif SERVER diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs index 226e1c3c1..780e23939 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs @@ -30,28 +30,12 @@ namespace Barotrauma GameMain.Server.ServerSettings.SelectedLevelDifficulty = difficulty; lastUpdateID++; } -#endif -#if CLIENT +#elif CLIENT levelDifficultyScrollBar.BarScroll = difficulty / 100.0f; levelDifficultyScrollBar.OnMoved(levelDifficultyScrollBar, levelDifficultyScrollBar.BarScroll); #endif } - public void ToggleTraitorsEnabled(int dir) - { -#if SERVER - if (GameMain.Server == null) return; - - lastUpdateID++; - - int index = (int)GameMain.Server.ServerSettings.TraitorsEnabled + dir; - if (index < 0) index = 2; - if (index > 2) index = 0; - - SetTraitorsEnabled((YesNoMaybe)index); -#endif - } - public void SetBotCount(int botCount) { #if SERVER @@ -89,13 +73,28 @@ namespace Barotrauma #endif } - public void SetTraitorsEnabled(YesNoMaybe enabled) + public void SetTraitorProbability(float probability) { -#if SERVER - if (GameMain.Server != null) GameMain.Server.ServerSettings.TraitorsEnabled = enabled; -#endif + if (GameMain.NetworkMember != null) + { + GameMain.NetworkMember.ServerSettings.TraitorProbability = probability; + } #if CLIENT - traitorProbabilityText.Text = TextManager.Get(enabled.ToString()); + traitorProbabilitySlider.BarScroll = probability; + traitorProbabilitySlider.OnMoved(traitorProbabilitySlider, traitorProbabilitySlider.BarScroll); +#endif + } + + public void SetTraitorDangerLevel(int dangerLevel) + { + if (GameMain.NetworkMember != null) + { + GameMain.NetworkMember.ServerSettings.TraitorDangerLevel = dangerLevel; + } +#if SERVER + if (GameMain.Server != null) { GameMain.Server.ServerSettings.TraitorDangerLevel = dangerLevel; } +#elif CLIENT + SetTraitorDangerIndicators(dangerLevel); #endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index 64032091c..3e477e0e2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs @@ -85,7 +85,11 @@ namespace Barotrauma AllowRotating, Attachable, HasBody, - Pickable + Pickable, + OnlyByStatusEffectsAndNetwork, + HasIntegratedButtons, + IsToggleableController, + HasConnectionPanel } public bool IsEditable(ISerializableEntity entity) @@ -115,6 +119,26 @@ namespace Barotrauma { return entity is Item item && item.GetComponent() != null; } + case ConditionType.HasIntegratedButtons: + { + return entity is Door door && door.HasIntegratedButtons; + } + case ConditionType.OnlyByStatusEffectsAndNetwork: +#if SERVER + return true; +#else + return false; +#endif + case ConditionType.IsToggleableController: + { + return entity is Controller controller && controller.IsToggle && controller.Item.GetComponent() != null; + } + case ConditionType.HasConnectionPanel: + { + return + (entity is Item item && item.GetComponent() != null) || + (entity is ItemComponent ic && ic.Item.GetComponent() != null); + } } return false; } @@ -228,7 +252,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError("Failed to set the value of the property \"" + Name + "\" of \"" + parentObject + "\" to " + value + " (not a valid " + PropertyInfo.PropertyType + ")", e); + DebugConsole.ThrowError($"Failed to set the value of the property \"{Name}\" of \"{parentObject}\" to {value} (not a valid {PropertyInfo.PropertyType})", e); return false; } try @@ -237,7 +261,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError("Failed to set the value of the property \"" + Name + "\" of \"" + parentObject.ToString() + "\" to " + value.ToString(), e); + DebugConsole.ThrowError($"Failed to set the value of the property \"{Name}\" of \"{parentObject}\" to {value}", e); return false; } } @@ -320,7 +344,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError("Failed to set the value of the property \"" + Name + "\" of \"" + parentObject.ToString() + "\" to " + value.ToString(), e); + DebugConsole.ThrowError($"Failed to set the value of the property \"{Name}\" of \"{parentObject}\" to {value}", e); return false; } @@ -403,14 +427,14 @@ namespace Barotrauma PropertyInfo.SetValue(parentObject, XMLExtensions.ParseStringArray((string)value).ToIdentifiers().ToArray()); return true; default: - DebugConsole.ThrowError("Failed to set the value of the property \"" + Name + "\" of \"" + parentObject.ToString() + "\" to " + value.ToString()); - DebugConsole.ThrowError("(Cannot convert a string to a " + PropertyType.ToString() + ")"); + DebugConsole.ThrowError($"Failed to set the value of the property \"{Name}\" of \"{parentObject}\" to {value}"); + DebugConsole.ThrowError($"(Cannot convert a string to a {PropertyType})"); return false; } } else if (PropertyType != value.GetType()) { - DebugConsole.ThrowError("Failed to set the value of the property \"" + Name + "\" of \"" + parentObject.ToString() + "\" to " + value.ToString()); + DebugConsole.ThrowError($"Failed to set the value of the property \"{Name}\" of \"{parentObject}\" to {value}"); DebugConsole.ThrowError("(Non-matching type, should be " + PropertyType + " instead of " + value.GetType() + ")"); return false; } @@ -420,7 +444,7 @@ namespace Barotrauma catch (Exception e) { - DebugConsole.ThrowError("Failed to set the value of the property \"" + Name + "\" of \"" + parentObject.ToString() + "\" to " + value.ToString(), e); + DebugConsole.ThrowError($"Failed to set the value of the property \"{Name}\" of \"{parentObject}\" to {value}", e); return false; } @@ -428,7 +452,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError("Error in SerializableProperty.TrySetValue", e); + DebugConsole.ThrowError($"Error in SerializableProperty.TrySetValue (Property: {PropertyInfo.Name})", e); return false; } } @@ -447,7 +471,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError("Error in SerializableProperty.TrySetValue", e); + DebugConsole.ThrowError($"Error in SerializableProperty.TrySetValue (Property: {PropertyInfo.Name})", e); return false; } @@ -468,7 +492,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError("Error in SerializableProperty.TrySetValue", e); + DebugConsole.ThrowError($"Error in SerializableProperty.TrySetValue (Property: {PropertyInfo.Name})", e); return false; } return true; @@ -488,7 +512,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError("Error in SerializableProperty.TrySetValue", e); + DebugConsole.ThrowError($"Error in SerializableProperty.TrySetValue (Property: {PropertyInfo.Name})", e); return false; } return true; @@ -1051,12 +1075,18 @@ namespace Barotrauma { if (!subElement.Name.ToString().Equals("upgrade", StringComparison.OrdinalIgnoreCase)) { continue; } var upgradeVersion = new Version(subElement.GetAttributeString("gameversion", "0.0.0.0")); - if (savedVersion >= upgradeVersion) { continue; } - + if (subElement.GetAttributeBool("campaignsaveonly", false)) + { + if ((GameMain.GameSession?.LastSaveVersion ?? GameMain.Version) >= upgradeVersion) { continue; } + } + else + { + if (savedVersion >= upgradeVersion) { continue; } + } foreach (XAttribute attribute in subElement.Attributes()) { var attributeName = attribute.NameAsIdentifier(); - if (attributeName == "gameversion") { continue; } + if (attributeName == "gameversion" || attributeName == "campaignsaveonly") { continue; } if (attributeName == "refreshrect") { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index ebd748ba2..d6d5d5887 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -17,7 +17,7 @@ namespace Barotrauma { public static class XMLExtensions { - private static ImmutableDictionary> converters + private readonly static ImmutableDictionary> Converters = new Dictionary>() { { typeof(string), (str, defVal) => str }, @@ -349,10 +349,11 @@ namespace Barotrauma } return false; } - - public static int GetAttributeInt(this XElement element, string name, int defaultValue) + + public static int GetAttributeInt(this XElement element, string name, int defaultValue) => GetAttributeInt(element?.GetAttribute(name), defaultValue); + + public static int GetAttributeInt(this XAttribute attribute, int defaultValue) { - var attribute = element?.GetAttribute(name); if (attribute == null) { return defaultValue; } int val = defaultValue; @@ -361,12 +362,12 @@ namespace Barotrauma { if (!Int32.TryParse(attribute.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out val)) { - val = (int)float.Parse(element.GetAttribute(name).Value, CultureInfo.InvariantCulture); + val = (int)float.Parse(attribute.Value, CultureInfo.InvariantCulture); } } catch (Exception e) { - LogAttributeError(attribute, element, e); + LogAttributeError(attribute, attribute.Parent, e); } return val; @@ -391,6 +392,25 @@ namespace Barotrauma return val; } + public static ushort GetAttributeUInt16(this XElement element, string name, ushort defaultValue) + { + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } + + ushort val = defaultValue; + + try + { + val = ushort.Parse(attribute.Value); + } + catch (Exception e) + { + LogAttributeError(attribute, element, e); + } + + return val; + } + public static UInt64 GetAttributeUInt64(this XElement element, string name, UInt64 defaultValue) { var attribute = element?.GetAttribute(name); @@ -738,8 +758,8 @@ namespace Barotrauma string[] elems = strValue.Split(','); if (elems.Length != 2) { return defaultValue; } - return ((T1)converters[typeof(T1)].Invoke(elems[0], defaultValue.Item1), - (T2)converters[typeof(T2)].Invoke(elems[1], defaultValue.Item2)); + return ((T1)Converters[typeof(T1)].Invoke(elems[0], defaultValue.Item1), + (T2)Converters[typeof(T2)].Invoke(elems[1], defaultValue.Item2)); } public static Point ParsePoint(string stringPoint, bool errorMessages = true) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index c88eb0204..8c044c67d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -74,11 +74,11 @@ namespace Barotrauma TutorialSkipWarning = true, CorpseDespawnDelay = 600, CorpsesPerSubDespawnThreshold = 5, - #if OSX +#if OSX UseDualModeSockets = false, - #else +#else UseDualModeSockets = true, - #endif +#endif DisableInGameHints = false, EnableSubmarineAutoSave = true, Graphics = GraphicsSettings.GetDefault(), @@ -114,7 +114,7 @@ namespace Barotrauma } #endif //RemoteMainMenuContentUrl gets set to default it left empty - lets allow leaving it empty to make it possible to disable the remote content - if (element.Attribute("RemoteMainMenuContentUrl")?.Value == string.Empty) + if (element.GetAttribute("RemoteMainMenuContentUrl")?.Value == string.Empty) { retVal.RemoteMainMenuContentUrl = string.Empty; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs b/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs index d2e3f5467..f954f79c4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs @@ -19,8 +19,7 @@ namespace Barotrauma { Target = target; Exclusive = element.GetAttributeBool("exclusive", Exclusive); - LogicalOperator = element.GetAttributeEnum(nameof(LogicalOperator), - element.GetAttributeEnum("comparison", LogicalOperator)); + LogicalOperator = element.GetAttributeEnum("comparison", LogicalOperator); foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs index 55e75b67d..bb39a43b5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs @@ -1,9 +1,7 @@ using Microsoft.Xna.Framework; -using System.Collections.Generic; using System.Xml.Linq; using System.Linq; using Barotrauma.Extensions; -using Barotrauma.IO; using System; using SpriteParams = Barotrauma.RagdollParams.SpriteParams; #if CLIENT @@ -14,28 +12,6 @@ namespace Barotrauma { public partial class Sprite { - public static IEnumerable LoadedSprites - { - get - { - List retVal = null; - lock (list) - { - retVal = list.Select(wRef => - { - if (wRef.TryGetTarget(out Sprite spr)) - { - return spr; - } - return null; - }).Where(s => s != null).ToList(); - } - return retVal; - } - } - - private readonly static List> list = new List>(); - /// /// Reference to the xml element from where the sprite was created. Can be null if the sprite was not defined in xml! /// @@ -119,7 +95,6 @@ namespace Barotrauma return FilePath + ": " + sourceRect; } - public Identifier Identifier { get; private set; } /// /// Identifier of the Map Entity so that we can link the sprite to its owner. /// @@ -130,17 +105,15 @@ namespace Barotrauma partial void CalculateSourceRect(); - private static void AddToList(Sprite elem) - { - lock (list) - { - list.Add(new WeakReference(elem)); - } - } + static partial void AddToList(Sprite sprite); public Sprite(ContentXElement element, string path = "", string file = "", bool lazyLoad = false) { - if (element is null) { return; } + if (element is null) + { + DebugConsole.ThrowError($"Sprite: xml element null in {file}. Failed to create the sprite!"); + return; + } this.LazyLoad = lazyLoad; SourceElement = element; if (!ParseTexturePath(path, file)) { return; } @@ -171,8 +144,10 @@ namespace Barotrauma size.Y *= sourceRect.Height; RelativeOrigin = SourceElement.GetAttributeVector2("origin", new Vector2(0.5f, 0.5f)); Depth = SourceElement.GetAttributeFloat("depth", 0.001f); +#if CLIENT Identifier = GetIdentifier(SourceElement); AddToList(this); +#endif } internal void LoadParams(SpriteParams spriteParams, bool isFlipped) @@ -235,12 +210,11 @@ namespace Barotrauma return $"{sourceElement}{parentElement?.ToString() ?? ""}".ToIdentifier(); } + static partial void RemoveFromList(Sprite sprite); + public void Remove() { - lock (list) - { - list.RemoveAll(wRef => !wRef.TryGetTarget(out Sprite s) || s == this); - } + RemoveFromList(this); DisposeTexture(); } @@ -308,12 +282,18 @@ namespace Barotrauma size.Y *= sourceRect.Height; RelativeOrigin = SourceElement.GetAttributeVector2("origin", new Vector2(0.5f, 0.5f)); Depth = SourceElement.GetAttributeFloat("depth", 0.001f); +#if CLIENT Identifier = GetIdentifier(SourceElement); +#endif } } public bool ParseTexturePath(string path = "", string file = "") { +#if SERVER + // Server doesn't care about texture paths at all + return true; +#endif if (file == "") { file = SourceElement.GetAttributeStringUnrestricted("texture", ""); diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs index 514e207b8..8d14f4ce7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs @@ -1,71 +1,246 @@ -using System; +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Xml.Linq; +using Barotrauma.Items.Components; namespace Barotrauma { - // TODO: This class should be refactored: - // - Use XElement instead of XAttribute in the constructor - // - Simplify, remove unnecessary conversions - // - Improve the flow so that the logic is undestandable. - // - Maybe add some test cases for the operators? - class PropertyConditional + /// + /// Conditionals are used by some in-game mechanics to require one + /// or more conditions to be met for those mechanics to be active. + /// For example, some StatusEffects use Conditionals to only trigger + /// if the affected character is alive. + /// + sealed class PropertyConditional { + // TODO: Make this testable and add tests + + /// + /// Category of properties to check against + /// public enum ConditionType { - Uncertain, - PropertyValue, + /// + /// If there exists an affliction with an identifier that matches the given key, check against the strength of that affliction. + /// Otherwise, check against the value of one of the target's properties. + /// + /// The target object's available properties depend on how that object is defined in the [source code](https://github.com/Regalis11/Barotrauma). + /// + /// This is not applicable if the element contains the attribute + /// `SkillRequirement="true"`. + /// + /// + /// + /// + /// + /// + PropertyValueOrAffliction, + + /// + /// Check against the target character's skill with the same name as the attribute. + /// + /// This is only applicable if the element contains the attribute + /// `SkillRequirement="true"`. + /// + /// + /// + /// + /// + SkillRequirement, + + /// + /// Check against the name of the target. + /// Name, + + /// + /// Check against the species identifier of the target. Only works on characters. + /// SpeciesName, + + /// + /// Check against the species group of the target. Only works on characters. + /// SpeciesGroup, + + /// + /// Check against the target's tags. Only works on items. + /// + /// Several tags can be checked against by using a comma-separated list. + /// HasTag, + + /// + /// Check against the tags of the target's active status effects. + /// + /// Several tags can be checked against by using a comma-separated list. + /// HasStatusTag, + + /// + /// Check against the target's specifier tags. In the vanilla game, these are the head index + /// and gender. See human.xml for more details. + /// + /// Several tags can be checked against by using a comma-separated list. + /// HasSpecifierTag, - Affliction, + + /// + /// Check against the target's entity type. + /// + /// The currently supported values are "character", "limb", "item", "structure" and "null". + /// EntityType, - LimbType, - SkillRequirement + + /// + /// Check against the target's limb type. See . + /// + LimbType } - public enum Comparison + public enum LogicalOperatorType { And, Or } - public enum OperatorType + /// + /// There are several ways to compare properties to values. + /// The comparison operator to use can be specified by placing one of the following before the value to compare against. + /// + public enum ComparisonOperatorType { None, + + /// + /// Require that the property being checked equals the given value. + /// + /// This is the default operator used if none is specified. + /// Equals, + + /// + /// Require that the property being checked doesn't equal the given value. + /// NotEquals, + + /// + /// Require that the property being checked is less than the given value. + /// + /// This can only be used to compare with numeric object properties, + /// affliction strengths and skill levels. + /// LessThan, + + /// + /// Require that the property being checked is less than or equal to the given value. + /// + /// This can only be used to compare with numeric object properties, + /// affliction strengths and skill levels. + /// LessThanEquals, + + /// + /// Require that the property being checked is greater than the given value. + /// + /// This can only be used to compare with numeric object properties, + /// affliction strengths and skill levels. + /// GreaterThan, + + /// + /// Require that the property being checked is greater than or equal to the given value. + /// + /// This can only be used to compare with numeric object properties, + /// affliction strengths and skill levels. + /// GreaterThanEquals } public readonly ConditionType Type; - public readonly OperatorType Operator; + public readonly ComparisonOperatorType ComparisonOperator; public readonly Identifier AttributeName; public readonly string AttributeValue; - public readonly string[] SplitAttributeValue; + public readonly ImmutableArray AttributeValueAsTags; public readonly float? FloatValue; - public readonly string TargetItemComponentName; + /// + /// If set to the name of one of the target's ItemComponents, the conditionals defined by this element check against the properties of that component. + /// Only works on items. + /// + public readonly string TargetItemComponent; - // Only used by attacks + /// + /// If set to true, the conditionals defined by this element check against the attacking character instead of the attacked character. + /// Only applies to a character's attacks. + /// public readonly bool TargetSelf; - // Only used by conditionals targeting an item (makes the conditional check the item/character whose inventory this item is inside) + /// + /// If set to true, the conditionals defined by this element check against the entity containing the target. + /// public readonly bool TargetContainer; - // Only used by conditionals targeting an item. By default, containers check the parent item. This allows you to check the grandparent instead. + + /// + /// If this and TargetContainer are set to true, the conditionals defined by this element check against the entity containing the target's container. + /// For example, diving suits have a status effect that targets contained oxygen tanks, with a conditional that only passes if the locker containing the suit is powered. + /// public readonly bool TargetGrandParent; + /// + /// If set to true, the conditionals defined by this element check against the items contained by the target. Only works with items. + /// public readonly bool TargetContainedItem; - // Remove this after refactoring - public static bool IsValid(XAttribute attribute) + public static IEnumerable FromXElement(XElement element, Predicate? predicate = null) + { + var targetItemComponent = element.GetAttributeString(nameof(TargetItemComponent), ""); + var targetContainer = element.GetAttributeBool(nameof(TargetContainer), false); + var targetSelf = element.GetAttributeBool(nameof(TargetSelf), false); + var targetGrandParent = element.GetAttributeBool(nameof(TargetGrandParent), false); + var targetContainedItem = element.GetAttributeBool(nameof(TargetContainedItem), false); + + ConditionType? overrideConditionType = null; + if (element.GetAttributeBool(nameof(ConditionType.SkillRequirement), false)) + { + overrideConditionType = ConditionType.SkillRequirement; + } + + foreach (var attribute in element.Attributes()) + { + if (!IsValid(attribute)) { continue; } + if (predicate != null && !predicate(attribute)) { continue; } + + var (comparisonOperator, attributeValueString) = ExtractComparisonOperatorFromConditionString(attribute.Value); + if (string.IsNullOrWhiteSpace(attributeValueString)) + { + DebugConsole.ThrowError($"Conditional attribute value is empty: {element}"); + continue; + } + + var conditionType = overrideConditionType ?? + (Enum.TryParse(attribute.Name.LocalName, ignoreCase: true, out ConditionType type) + ? type + : ConditionType.PropertyValueOrAffliction); + + yield return new PropertyConditional( + attributeName: attribute.NameAsIdentifier(), + comparisonOperator: comparisonOperator, + attributeValue: attributeValueString, + targetItemComponent: targetItemComponent, + targetSelf: targetSelf, + targetContainer: targetContainer, + targetGrandParent: targetGrandParent, + targetContainedItem: targetContainedItem, + conditionType: conditionType); + } + } + + private static bool IsValid(XAttribute attribute) { switch (attribute.Name.ToString().ToLowerInvariant()) { @@ -82,60 +257,62 @@ namespace Barotrauma } } - // TODO: use XElement instead of XAttribute (how to do without breaking the existing content?) - public PropertyConditional(XAttribute attribute) + private PropertyConditional( + Identifier attributeName, + ComparisonOperatorType comparisonOperator, + string attributeValue, + string targetItemComponent, + bool targetSelf, + bool targetContainer, + bool targetGrandParent, + bool targetContainedItem, + ConditionType conditionType) { - AttributeName = attribute.NameAsIdentifier(); - string attributeValueString = attribute.Value; - if (string.IsNullOrWhiteSpace(attributeValueString)) - { - DebugConsole.ThrowError($"Conditional attribute value is empty: {attribute.Parent}"); - return; - } - string valueString = attributeValueString; - string[] splitString = valueString.Split(' ', StringSplitOptions.RemoveEmptyEntries); - if (splitString.Length > 1) { valueString = string.Join(' ', splitString.Skip(1)); } - Operator = GetOperatorType(splitString[0]); + AttributeName = attributeName; - if (Operator == OperatorType.None) - { - Operator = OperatorType.Equals; - valueString = attributeValueString; - } + TargetItemComponent = targetItemComponent; + TargetSelf = targetSelf; + TargetContainer = targetContainer; + TargetGrandParent = targetGrandParent; + TargetContainedItem = targetContainedItem; - TargetItemComponentName = attribute.Parent.GetAttributeString("targetitemcomponent", ""); - TargetContainer = attribute.Parent.GetAttributeBool("targetcontainer", false); - TargetSelf = attribute.Parent.GetAttributeBool("targetself", false); - TargetGrandParent = attribute.Parent.GetAttributeBool("targetgrandparent", false); - TargetContainedItem = attribute.Parent.GetAttributeBool("targetcontaineditem", false); + Type = conditionType; - if (!Enum.TryParse(AttributeName.Value, true, out Type)) - { - Type = ConditionType.Uncertain; - } - - if (attribute.Parent.GetAttributeBool("skillrequirement", false)) - { - Type = ConditionType.SkillRequirement; - } - - AttributeValue = valueString; - SplitAttributeValue = valueString.Split(','); + ComparisonOperator = comparisonOperator; + AttributeValue = attributeValue; + AttributeValueAsTags = AttributeValue.Split(',') + .Select(s => s.ToIdentifier()) + .ToImmutableArray(); if (float.TryParse(AttributeValue, NumberStyles.Float, CultureInfo.InvariantCulture, out float value)) { FloatValue = value; } } - public static OperatorType GetOperatorType(string op) + public static (ComparisonOperatorType ComparisonOperator, string ConditionStr) ExtractComparisonOperatorFromConditionString(string str) + { + str ??= ""; + + ComparisonOperatorType op = ComparisonOperatorType.Equals; + string conditionStr = str; + if (str.IndexOf(' ') is var i and >= 0) + { + op = GetComparisonOperatorType(str[..i]); + if (op != ComparisonOperatorType.None) { conditionStr = str[(i + 1)..]; } + else { op = ComparisonOperatorType.Equals; } + } + return (op, conditionStr); + } + + public static ComparisonOperatorType GetComparisonOperatorType(string op) { //thanks xml for not letting me use < or > in attributes :( - switch (op) + switch (op.ToLowerInvariant()) { case "e": case "eq": case "equals": - return OperatorType.Equals; + return ComparisonOperatorType.Equals; case "ne": case "neq": case "notequals": @@ -143,311 +320,277 @@ namespace Barotrauma case "!e": case "!eq": case "!equals": - return OperatorType.NotEquals; + return ComparisonOperatorType.NotEquals; case "gt": case "greaterthan": - return OperatorType.GreaterThan; + return ComparisonOperatorType.GreaterThan; case "lt": case "lessthan": - return OperatorType.LessThan; + return ComparisonOperatorType.LessThan; case "gte": case "gteq": case "greaterthanequals": - return OperatorType.GreaterThanEquals; + return ComparisonOperatorType.GreaterThanEquals; case "lte": case "lteq": case "lessthanequals": - return OperatorType.LessThanEquals; + return ComparisonOperatorType.LessThanEquals; default: - return OperatorType.None; + return ComparisonOperatorType.None; } } + private bool ComparisonOperatorIsNotEquals => ComparisonOperator == ComparisonOperatorType.NotEquals; - public bool Matches(ISerializableEntity target) + public bool Matches(ISerializableEntity? target) { - return Matches(target, TargetContainedItem); + return TargetContainedItem + ? MatchesContained(target) + : MatchesDirect(target); } - public bool Matches(ISerializableEntity target, bool checkContained) + private bool MatchesContained(ISerializableEntity? target) { - var type = Type; - if (type == ConditionType.Uncertain) + var containedItems = target switch { - type = AfflictionPrefab.Prefabs.ContainsKey(AttributeName) - ? ConditionType.Affliction - : ConditionType.PropertyValue; - } - - if (checkContained) + Item item + => item.ContainedItems, + ItemComponent ic + => ic.Item.ContainedItems, + Character {Inventory: { } characterInventory} + => characterInventory.AllItems, + _ + => Enumerable.Empty() + }; + foreach (var containedItem in containedItems) { - if (target is Item item) - { - foreach (var containedItem in item.ContainedItems) - { - if (Matches(containedItem, checkContained: false)) { return true; } - } - return false; - } - else if (target is Items.Components.ItemComponent ic) - { - foreach (var containedItem in ic.Item.ContainedItems) - { - if (Matches(containedItem, checkContained: false)) { return true; } - } - return false; - } - else if (target is Character character) - { - if (character.Inventory == null) { return false; } - foreach (var containedItem in character.Inventory.AllItems) - { - if (Matches(containedItem, checkContained: false)) { return true; } - } - return false; - } + if (MatchesDirect(containedItem)) { return true; } } + return false; + } - switch (type) + private bool MatchesDirect(ISerializableEntity? target) + { + Character? targetChar = target as Character; + if (target is Limb limb) { targetChar = limb.character; } + switch (Type) { - case ConditionType.PropertyValue: - SerializableProperty property; - if (target?.SerializableProperties == null) { return Operator == OperatorType.NotEquals; } - if (target.SerializableProperties.TryGetValue(AttributeName, out property)) + case ConditionType.PropertyValueOrAffliction: + // If an AfflictionPrefab with identifier AttributeName exists, + // check for an affliction affecting the target + if (AfflictionPrefab.Prefabs.ContainsKey(AttributeName)) { - return Matches(target, property); + if (targetChar is { CharacterHealth: { } health }) + { + var affliction = health.GetAffliction(AttributeName); + float afflictionStrength = affliction?.Strength ?? 0f; + + return NumberMatchesRequirement(afflictionStrength); + } } - return false; - case ConditionType.Name: - if (target == null) { return Operator == OperatorType.NotEquals; } - return (Operator == OperatorType.Equals) == (target.Name == AttributeValue); + // Otherwise try checking for a property belonging to the target + else if (target?.SerializableProperties != null + && target.SerializableProperties.TryGetValue(AttributeName, out var property)) + { + return PropertyMatchesRequirement(target, property); + } + return ComparisonOperatorIsNotEquals; + case ConditionType.SkillRequirement: + if (targetChar != null) + { + float skillLevel = targetChar.GetSkillLevel(AttributeName.ToIdentifier()); + + return NumberMatchesRequirement(skillLevel); + } + return ComparisonOperatorIsNotEquals; case ConditionType.HasTag: - if (target == null) { return Operator == OperatorType.NotEquals; } - return MatchesTagCondition(target); + return ItemMatchesTagCondition(target); case ConditionType.HasStatusTag: - if (target == null) { return Operator == OperatorType.NotEquals; } - int matches = 0; - foreach (DurationListElement durationEffect in StatusEffect.DurationList) + if (target == null) { return ComparisonOperatorIsNotEquals; } + + // NOTE: This can be optimized further by returning + // when a match passes with the Equals operator and + // when a match fails with the NotEquals operator. + // The current form has better readability. + int numMatchingEffects = 0; + int numEffectsAffectingTarget = 0; + + foreach (var durationEffect in StatusEffect.DurationList) { if (!durationEffect.Targets.Contains(target)) { continue; } - foreach (string tag in SplitAttributeValue) - { - if (durationEffect.Parent.HasTag(tag)) - { - matches++; - } - } + numEffectsAffectingTarget++; + if (StatusEffectMatchesTagCondition(durationEffect.Parent)) { numMatchingEffects++; } } - foreach (DelayedListElement delayedEffect in DelayedEffect.DelayList) + + foreach (var delayedEffect in DelayedEffect.DelayList) { if (!delayedEffect.Targets.Contains(target)) { continue; } - foreach (string tag in SplitAttributeValue) - { - if (delayedEffect.Parent.HasTag(tag)) - { - matches++; - } - } + numEffectsAffectingTarget++; + if (StatusEffectMatchesTagCondition(delayedEffect.Parent)) { numMatchingEffects++; } } - return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; - case ConditionType.HasSpecifierTag: - { - if (target == null) { return Operator == OperatorType.NotEquals; } - if (!(target is Character { Info: { } characterInfo })) { return false; } - return (Operator == OperatorType.Equals) == - SplitAttributeValue.All(v => characterInfo.Head.Preset.TagSet.Contains(v)); - } - case ConditionType.SpeciesName: - { - if (target == null) { return Operator == OperatorType.NotEquals; } - if (!(target is Character targetCharacter)) { return false; } - return (Operator == OperatorType.Equals) == (targetCharacter.SpeciesName == AttributeValue); - } - case ConditionType.SpeciesGroup: - { - if (target == null) { return Operator == OperatorType.NotEquals; } - if (!(target is Character targetCharacter)) { return false; } - return (Operator == OperatorType.Equals) == CharacterParams.CompareGroup(AttributeValue.ToIdentifier(), targetCharacter.Group); - } - case ConditionType.EntityType: - switch (AttributeValue) - { - case "character": - case "Character": - return (Operator == OperatorType.Equals) == target is Character; - case "limb": - case "Limb": - return (Operator == OperatorType.Equals) == target is Limb; - case "item": - case "Item": - return (Operator == OperatorType.Equals) == target is Item; - case "structure": - case "Structure": - return (Operator == OperatorType.Equals) == target is Structure; - case "null": - return (Operator == OperatorType.Equals) == (target == null); - default: - return false; - } - case ConditionType.LimbType: - { - if (!(target is Limb limb)) - { - return false; - } - else - { - return limb.type.ToString().Equals(AttributeValue, StringComparison.OrdinalIgnoreCase); - } - } - case ConditionType.Affliction: - { - if (target == null) { return Operator == OperatorType.NotEquals; } - - Character targetChar = target as Character; - if (target is Limb limb) { targetChar = limb.character; } - if (targetChar != null) - { - var health = targetChar.CharacterHealth; - if (health == null) { return false; } - var affliction = health.GetAffliction(AttributeName.ToIdentifier()); - float afflictionStrength = affliction == null ? 0.0f : affliction.Strength; - - return ValueMatchesRequirement(afflictionStrength); - } - } - return false; - case ConditionType.SkillRequirement: - { - if (target == null) { return Operator == OperatorType.NotEquals; } - - if (target is Character targetChar) - { - float skillLevel = targetChar.GetSkillLevel(AttributeName.ToIdentifier()); - - return ValueMatchesRequirement(skillLevel); - } - } - return false; + return ComparisonOperatorIsNotEquals + ? numMatchingEffects >= numEffectsAffectingTarget // true when none of the effects have any of the tags + : numMatchingEffects > 0; // true when any effects have all tags default: - return false; + bool equals = CheckOnlyEquality(target); + return ComparisonOperatorIsNotEquals + ? !equals + : equals; } } - private bool ValueMatchesRequirement(float testedValue) + private bool CheckOnlyEquality(ISerializableEntity? target) { - if (FloatValue.HasValue) + switch (Type) { - float value = FloatValue.Value; - switch (Operator) + case ConditionType.Name: + if (target == null) { return false; } + + return target.Name == AttributeValue; + case ConditionType.HasSpecifierTag: { - case OperatorType.Equals: - return testedValue == value; - case OperatorType.GreaterThan: - return testedValue > value; - case OperatorType.GreaterThanEquals: - return testedValue >= value; - case OperatorType.LessThan: - return testedValue < value; - case OperatorType.LessThanEquals: - return testedValue <= value; - case OperatorType.NotEquals: - return testedValue != value; + if (target is not Character {Info: { } characterInfo}) + { + return false; + } + + return AttributeValueAsTags.All(characterInfo.Head.Preset.TagSet.Contains); + } + case ConditionType.SpeciesName: + { + if (target is Character targetCharacter) + { + return targetCharacter.SpeciesName == AttributeValue; + } + else if (target is Limb targetLimb) + { + return targetLimb.character.SpeciesName == AttributeValue; + } + return false; + } + case ConditionType.SpeciesGroup: + { + if (target is Character targetCharacter) + { + return CharacterParams.CompareGroup(AttributeValue.ToIdentifier(), targetCharacter.Params.Group); + } + else if (target is Limb targetLimb) + { + return CharacterParams.CompareGroup(AttributeValue.ToIdentifier(), targetLimb.character.Params.Group); + } + return false; + } + case ConditionType.EntityType: + return AttributeValue.ToLowerInvariant() switch + { + "character" + => target is Character, + "limb" + => target is Limb, + "item" + => target is Item, + "structure" + => target is Structure, + "null" + => target == null, + _ + => false + }; + case ConditionType.LimbType: + { + return target is Limb limb + && Enum.TryParse(AttributeValue, ignoreCase: true, out LimbType attributeLimbType) + && attributeLimbType == limb.type; } } return false; } - private bool MatchesTagCondition(ISerializableEntity target) + private bool SufficientTagMatches(int matches) { - if (!(target is Item item)) { return Operator == OperatorType.NotEquals; } - - int matches = 0; - foreach (string tag in SplitAttributeValue) - { - if (item.HasTag(tag)) - { - matches++; - } - } - //If operator is == then it needs to match everything, otherwise if its != there must be zero matches. - return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; + return ComparisonOperatorIsNotEquals + ? matches <= 0 + : matches >= AttributeValueAsTags.Length; } - public bool MatchesTagCondition(Identifier targetTag) + private bool ItemMatchesTagCondition(ISerializableEntity? target) + { + if (target is not Item item) { return ComparisonOperatorIsNotEquals; } + + int matches = 0; + foreach (var tag in AttributeValueAsTags) + { + if (item.HasTag(tag)) { matches++; } + } + return SufficientTagMatches(matches); + } + + public bool TargetTagMatchesTagCondition(Identifier targetTag) { if (targetTag.IsEmpty || Type != ConditionType.HasTag) { return false; } int matches = 0; - foreach (string tag in SplitAttributeValue) + foreach (var tag in AttributeValueAsTags) { - if (targetTag == tag) - { - matches++; - } + if (targetTag == tag) { matches++; } } - //If operator is == then it needs to match everything, otherwise if its != there must be zero matches. - return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; + return SufficientTagMatches(matches); } - // TODO: refactor and add tests - private bool Matches(ISerializableEntity target, SerializableProperty property) + private bool StatusEffectMatchesTagCondition(StatusEffect statusEffect) + { + int matches = 0; + foreach (var tag in AttributeValueAsTags) + { + if (statusEffect.HasTag(tag)) { matches++; } + } + return SufficientTagMatches(matches); + } + + private bool NumberMatchesRequirement(float testedValue) + { + if (!FloatValue.HasValue) { return ComparisonOperatorIsNotEquals; } + float value = FloatValue.Value; + return CompareFloat(testedValue, value, ComparisonOperator); + } + + private bool PropertyMatchesRequirement(ISerializableEntity target, SerializableProperty property) { Type type = property.PropertyType; if (type == typeof(float) || type == typeof(int)) { float floatValue = property.GetFloatValue(target); - switch (Operator) - { - case OperatorType.Equals: - return MathUtils.NearlyEqual(floatValue, FloatValue.Value); - case OperatorType.NotEquals: - return !MathUtils.NearlyEqual(floatValue, FloatValue.Value); - case OperatorType.GreaterThan: - return floatValue > FloatValue.Value; - case OperatorType.LessThan: - return floatValue < FloatValue.Value; - case OperatorType.GreaterThanEquals: - return floatValue >= FloatValue.Value; - case OperatorType.LessThanEquals: - return floatValue <= FloatValue.Value; - } - return false; + return NumberMatchesRequirement(floatValue); } - switch (Operator) + switch (ComparisonOperator) { - case OperatorType.Equals: + case ComparisonOperatorType.Equals: + case ComparisonOperatorType.NotEquals: + bool equals; + if (type == typeof(bool)) { - if (type == typeof(bool)) - { - return property.GetBoolValue(target) == (AttributeValue == "true" || AttributeValue == "True"); - } - var value = property.GetValue(target); - return Equals(value, AttributeValue); + bool attributeValueBool = AttributeValue.IsTrueString(); + equals = property.GetBoolValue(target) == attributeValueBool; } - case OperatorType.NotEquals: + else { - if (type == typeof(bool)) - { - return property.GetBoolValue(target) != (AttributeValue == "true" || AttributeValue == "True"); - } var value = property.GetValue(target); - return !Equals(value, AttributeValue); + equals = AreValuesEquivalent(value, AttributeValue); } - case OperatorType.GreaterThan: - case OperatorType.LessThanEquals: - case OperatorType.LessThan: - case OperatorType.GreaterThanEquals: + + return ComparisonOperatorIsNotEquals + ? !equals + : equals; + default: DebugConsole.ThrowError("Couldn't compare " + AttributeValue.ToString() + " (" + AttributeValue.GetType() + ") to property \"" + property.Name + "\" (" + type + ")! " + "Make sure the type of the value set in the config files matches the type of the property."); - break; + return false; } - return false; - static bool Equals(object value, string desiredValue) + static bool AreValuesEquivalent(object? value, string desiredValue) { if (value == null) { @@ -455,10 +598,31 @@ namespace Barotrauma } else { - return value.ToString().Equals(desiredValue); + return (value.ToString() ?? "").Equals(desiredValue); } } } - } + public static bool CompareFloat(float val1, float val2, ComparisonOperatorType op) + { + switch (op) + { + case ComparisonOperatorType.Equals: + return MathUtils.NearlyEqual(val1, val2); + case ComparisonOperatorType.GreaterThan: + return val1 > val2; + case ComparisonOperatorType.GreaterThanEquals: + return val1 >= val2; + case ComparisonOperatorType.LessThan: + return val1 < val2; + case ComparisonOperatorType.LessThanEquals: + return val1 <= val2; + case ComparisonOperatorType.NotEquals: + return !MathUtils.NearlyEqual(val1, val2); + default: + return false; + } + } + + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 20831e9be..fdf9eaab5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -188,6 +188,10 @@ namespace Barotrauma /// public readonly bool SpawnIfInventoryFull; /// + /// Should the item spawn even if this item isn't in an inventory? Only valid if the SpawnPosition is set to . Defaults to false. + /// + public readonly bool SpawnIfNotInInventory; + /// /// Should the item spawn even if the container can't contain items of this type or if it's already full? /// public readonly bool SpawnIfCantBeContained; @@ -221,6 +225,8 @@ namespace Barotrauma /// public readonly float Condition; + public bool InheritEventTags { get; private set; } + public ItemSpawnInfo(XElement element, string parentDebugName) { if (element.Attribute("name") != null) @@ -250,8 +256,9 @@ namespace Barotrauma } } - SpawnIfInventoryFull = element.GetAttributeBool("spawnifinventoryfull", false); - SpawnIfCantBeContained = element.GetAttributeBool("spawnifcantbecontained", true); + SpawnIfInventoryFull = element.GetAttributeBool(nameof(SpawnIfInventoryFull), false); + SpawnIfNotInInventory = element.GetAttributeBool(nameof(SpawnIfNotInInventory), false); + SpawnIfCantBeContained = element.GetAttributeBool(nameof(SpawnIfCantBeContained), true); Impulse = element.GetAttributeFloat("impulse", element.GetAttributeFloat("launchimpulse", element.GetAttributeFloat("speed", 0.0f))); Condition = MathHelper.Clamp(element.GetAttributeFloat("condition", 1.0f), 0.0f, 1.0f); @@ -264,6 +271,8 @@ namespace Barotrauma SpawnPosition = element.GetAttributeEnum("spawnposition", SpawnPositionType.This); RotationType = element.GetAttributeEnum("rotationtype", RotationRad != 0 ? SpawnRotationType.Fixed : SpawnRotationType.Target); + + InheritEventTags = element.GetAttributeBool(nameof(InheritEventTags), false); } } @@ -398,6 +407,9 @@ namespace Barotrauma "Can be used to for example spawn a character a bit up from the center of an item executing the effect.")] public Vector2 Offset { get; private set; } + [Serialize(false, IsPropertySaveable.No)] + public bool InheritEventTags { get; private set; } + public CharacterSpawnInfo(XElement element, string parentDebugName) { SerializableProperties = SerializableProperty.DeserializeProperties(this, element); @@ -491,11 +503,11 @@ namespace Barotrauma /// public int TargetSlot = -1; - private readonly List requiredItems = new List(); + private readonly List requiredItems; public readonly ImmutableArray<(Identifier propertyName, object value)> PropertyEffects; - private readonly PropertyConditional.Comparison conditionalComparison = PropertyConditional.Comparison.Or; + private readonly PropertyConditional.LogicalOperatorType conditionalLogicalOperator = PropertyConditional.LogicalOperatorType.Or; private readonly List propertyConditionals; public bool HasConditions => propertyConditionals != null && propertyConditionals.Any(); @@ -514,15 +526,7 @@ namespace Barotrauma /// /// Can be used in conditionals to check if a StatusEffect with a specific tag is currently running. Only relevant for effects with a non-zero duration. /// - private readonly HashSet tags; - - /// - /// How long the effect runs (in seconds). Note that if is true, - /// there can be multiple instances of the effect running at a time. - /// In other words, if the effect has a duration and executes every frame, you probably want - /// to make it non-stackable or it'll lead to a large number of overlapping effects running at the same time. - /// - private readonly float duration; + private readonly HashSet tags; /// /// How long _can_ the event run (in seconds). The difference to is that @@ -533,7 +537,7 @@ namespace Barotrauma private readonly float lifeTime; private float lifeTimer; - public Dictionary intervalTimers = new Dictionary(); + private Dictionary intervalTimers; /// /// Makes the effect only execute once. After it has executed, it'll never execute again (during the same round). @@ -576,39 +580,53 @@ namespace Barotrauma public readonly ActionType type = ActionType.OnActive; - public readonly List Explosions = new List(); + private readonly List explosions; + public IEnumerable Explosions + { + get { return explosions ?? Enumerable.Empty(); } + } - private readonly List spawnItems = new List(); + private readonly List spawnItems; /// /// If enabled, one of the items this effect is configured to spawn is selected randomly, as opposed to spawning all of them. /// private readonly bool spawnItemRandomly; - private readonly List spawnCharacters = new List(); + private readonly List spawnCharacters; - public readonly List giveTalentInfos = new List(); + public readonly List giveTalentInfos; - private readonly List aiTriggers = new List(); + private readonly List aiTriggers; - private readonly List triggeredEvents = new List(); + /// + /// A list of events triggered by this status effect. + /// The fields , , + /// can be used to mark the target of the effect, the entity executing it, or the user as targets for the scripted event. + /// + private readonly List triggeredEvents; /// /// If the effect triggers a scripted event, the target of this effect is added as a target for the event using the specified tag. /// For example, an item could have an effect that executes when used on some character, and triggers an event that makes said character say something. /// - private readonly Identifier triggeredEventTargetTag; + private readonly Identifier triggeredEventTargetTag = "statuseffecttarget".ToIdentifier(); /// /// If the effect triggers a scripted event, the entity executing this effect is added as a target for the event using the specified tag. /// For example, a character could have an effect that executes when the character takes damage, and triggers an event that makes said character say something. /// - private readonly Identifier triggeredEventEntityTag; + private readonly Identifier triggeredEventEntityTag = "statuseffectentity".ToIdentifier(); /// /// If the effect triggers a scripted event, the user of the StatusEffect (= the character who caused it to happen, e.g. a character who used an item) is added as a target for the event using the specified tag. /// For example, a gun could have an effect that executes when a character uses it, and triggers an event that makes said character say something. /// - private readonly Identifier triggeredEventUserTag; + private readonly Identifier triggeredEventUserTag = "statuseffectuser".ToIdentifier(); + + /// + /// Can be used to tag the target entity/entities as targets in an event. + /// + private readonly List<(Identifier eventIdentifier, Identifier tag)> eventTargetTags; private Character user; @@ -651,6 +669,11 @@ namespace Barotrauma /// public readonly ImmutableHashSet TargetIdentifiers; + /// + /// If set to the name of one of the target's ItemComponents, the effect is only applied on that component. + /// Only works on items. + /// + public readonly string TargetItemComponent; /// /// Which type of afflictions the target must receive for the StatusEffect to be applied. Only valid when the type of the effect is OnDamaged. /// @@ -673,16 +696,22 @@ namespace Barotrauma public IEnumerable SpawnCharacters { - get { return spawnCharacters; } + get { return spawnCharacters ?? Enumerable.Empty(); } } public readonly List<(Identifier AfflictionIdentifier, float ReduceAmount)> ReduceAffliction = new List<(Identifier affliction, float amount)>(); - private readonly List talentTriggers = new List(); - private readonly List giveExperiences = new List(); - private readonly List giveSkills = new List(); + private readonly List talentTriggers; + private readonly List giveExperiences; + private readonly List giveSkills; - public float Duration => duration; + /// + /// How long the effect runs (in seconds). Note that if is true, + /// there can be multiple instances of the effect running at a time. + /// In other words, if the effect has a duration and executes every frame, you probably want + /// to make it non-stackable or it'll lead to a large number of overlapping effects running at the same time. + /// + public readonly float Duration; /// /// How close to the entity executing the effect the targets must be. Only applicable if targeting NearbyCharacters or NearbyItems. @@ -710,8 +739,8 @@ namespace Barotrauma string[] newTags = value.Split(','); foreach (string tag in newTags) { - string newTag = tag.Trim(); - if (!tags.Contains(newTag)) tags.Add(newTag); + Identifier newTag = tag.Trim().ToIdentifier(); + if (!tags.Contains(newTag)) { tags.Add(newTag); }; } } } @@ -730,20 +759,21 @@ namespace Barotrauma protected StatusEffect(ContentXElement element, string parentDebugName) { - tags = new HashSet(element.GetAttributeString("tags", "").Split(',')); + tags = new HashSet(element.GetAttributeString("tags", "").Split(',').ToIdentifiers()); OnlyInside = element.GetAttributeBool("onlyinside", false); OnlyOutside = element.GetAttributeBool("onlyoutside", false); OnlyWhenDamagedByPlayer = element.GetAttributeBool("onlyplayertriggered", element.GetAttributeBool("onlywhendamagedbyplayer", false)); AllowWhenBroken = element.GetAttributeBool("allowwhenbroken", false); Interval = element.GetAttributeFloat("interval", 0.0f); - duration = element.GetAttributeFloat("duration", 0.0f); + Duration = element.GetAttributeFloat("duration", 0.0f); disableDeltaTime = element.GetAttributeBool("disabledeltatime", false); setValue = element.GetAttributeBool("setvalue", false); Stackable = element.GetAttributeBool("stackable", true); lifeTime = lifeTimer = element.GetAttributeFloat("lifetime", 0.0f); CheckConditionalAlways = element.GetAttributeBool("checkconditionalalways", false); + TargetItemComponent = element.GetAttributeString("targetitemcomponent", string.Empty); TargetSlot = element.GetAttributeInt("targetslot", -1); Range = element.GetAttributeFloat("range", 0.0f); @@ -782,9 +812,9 @@ namespace Barotrauma TargetIdentifiers = targetIdentifiers.ToImmutableHashSet(); } - triggeredEventTargetTag = element.GetAttributeIdentifier("eventtargettag", Identifier.Empty); - triggeredEventEntityTag = element.GetAttributeIdentifier("evententitytag", Identifier.Empty); - triggeredEventUserTag = element.GetAttributeIdentifier("eventusertag", Identifier.Empty); + triggeredEventTargetTag = element.GetAttributeIdentifier("eventtargettag", triggeredEventTargetTag); + triggeredEventEntityTag = element.GetAttributeIdentifier("evententitytag", triggeredEventEntityTag); + triggeredEventUserTag = element.GetAttributeIdentifier("eventusertag", triggeredEventUserTag); spawnItemRandomly = element.GetAttributeBool("spawnitemrandomly", false); @@ -834,7 +864,7 @@ namespace Barotrauma break; case "conditionalcomparison": case "comparison": - if (!Enum.TryParse(attribute.Value, ignoreCase: true, out conditionalComparison)) + if (!Enum.TryParse(attribute.Value, ignoreCase: true, out conditionalLogicalOperator)) { DebugConsole.ThrowError($"Invalid conditional comparison type \"{attribute.Value}\" in StatusEffect ({parentDebugName})"); } @@ -849,7 +879,7 @@ namespace Barotrauma } break; case "tags": - if (duration <= 0.0f || setValue) + if (Duration <= 0.0f || setValue) { //a workaround to "tags" possibly meaning either an item's tags or this status effect's tags: //if the status effect doesn't have a duration, assume tags mean an item's tags, not this status effect's tags @@ -866,6 +896,13 @@ namespace Barotrauma } } + if (Duration > 0.0f && !setValue) + { + //a workaround to "tags" possibly meaning either an item's tags or this status effect's tags: + //if the status effect has a duration, assume tags mean this status effect's tags and leave item tags untouched. + propertyAttributes.RemoveAll(a => a.Name.ToString().Equals("tags", StringComparison.OrdinalIgnoreCase)); + } + List<(Identifier propertyName, object value)> propertyEffects = new List<(Identifier propertyName, object value)>(); foreach (XAttribute attribute in propertyAttributes) { @@ -878,7 +915,8 @@ namespace Barotrauma switch (subElement.Name.ToString().ToLowerInvariant()) { case "explosion": - Explosions.Add(new Explosion(subElement, parentDebugName)); + explosions ??= new List(); + explosions.Add(new Explosion(subElement, parentDebugName)); break; case "fire": FireSize = subElement.GetAttributeFloat("size", 10.0f); @@ -909,6 +947,7 @@ namespace Barotrauma break; case "requireditem": case "requireditems": + requiredItems ??= new List(); RelatedItem newRequiredItem = RelatedItem.Load(subElement, returnEmpty: false, parentDebugName: parentDebugName); if (newRequiredItem == null) { @@ -928,13 +967,7 @@ namespace Barotrauma } break; case "conditional": - foreach (XAttribute attribute in subElement.Attributes()) - { - if (PropertyConditional.IsValid(attribute)) - { - propertyConditionals.Add(new PropertyConditional(attribute)); - } - } + propertyConditionals.AddRange(PropertyConditional.FromXElement(subElement)); break; case "affliction": AfflictionPrefab afflictionPrefab; @@ -989,9 +1022,14 @@ namespace Barotrauma break; case "spawnitem": var newSpawnItem = new ItemSpawnInfo(subElement, parentDebugName); - if (newSpawnItem.ItemPrefab != null) { spawnItems.Add(newSpawnItem); } + if (newSpawnItem.ItemPrefab != null) + { + spawnItems ??= new List(); + spawnItems.Add(newSpawnItem); + } break; case "triggerevent": + triggeredEvents ??= new List(); Identifier identifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); if (!identifier.IsEmpty) { @@ -1009,22 +1047,40 @@ namespace Barotrauma break; case "spawncharacter": var newSpawnCharacter = new CharacterSpawnInfo(subElement, parentDebugName); - if (!newSpawnCharacter.SpeciesName.IsEmpty) { spawnCharacters.Add(newSpawnCharacter); } + if (!newSpawnCharacter.SpeciesName.IsEmpty) + { + spawnCharacters ??= new List(); + spawnCharacters.Add(newSpawnCharacter); + } break; case "givetalentinfo": var newGiveTalentInfo = new GiveTalentInfo(subElement, parentDebugName); - if (newGiveTalentInfo.TalentIdentifiers.Any()) { giveTalentInfos.Add(newGiveTalentInfo); } + if (newGiveTalentInfo.TalentIdentifiers.Any()) + { + giveTalentInfos ??= new List(); + giveTalentInfos.Add(newGiveTalentInfo); + } break; case "aitrigger": + aiTriggers ??= new List(); aiTriggers.Add(new AITrigger(subElement)); break; case "talenttrigger": + talentTriggers ??= new List(); talentTriggers.Add(subElement.GetAttributeIdentifier("effectidentifier", Identifier.Empty)); break; + case "eventtarget": + eventTargetTags ??= new List<(Identifier eventIdentifier, Identifier tag)>(); + eventTargetTags.Add( + (subElement.GetAttributeIdentifier("eventidentifier", Identifier.Empty), + subElement.GetAttributeIdentifier("tag", Identifier.Empty))); + break; case "giveexperience": + giveExperiences ??= new List(); giveExperiences.Add(subElement.GetAttributeInt("amount", 0)); break; case "giveskill": + giveSkills ??= new List(); giveSkills.Add(new GiveSkill(subElement, parentDebugName)); break; } @@ -1071,7 +1127,7 @@ namespace Barotrauma } else { - return itemPrefab.Tags.Any(t => propertyConditionals.Any(pc => pc.MatchesTagCondition(t))); + return itemPrefab.Tags.Any(t => propertyConditionals.Any(pc => pc.TargetTagMatchesTagCondition(t))); } } @@ -1088,7 +1144,7 @@ namespace Barotrauma public virtual bool HasRequiredItems(Entity entity) { - if (entity == null) { return true; } + if (entity == null || requiredItems == null) { return true; } foreach (RelatedItem requiredItem in requiredItems) { if (entity is Item item) @@ -1163,101 +1219,83 @@ namespace Barotrauma return HasRequiredConditions(targets, propertyConditionals); } + private delegate bool ShouldShortCircuit(bool condition, out bool valueToReturn); + + /// + /// Indicates that the Or operator should short-circuit when a condition is true + /// + private static bool ShouldShortCircuitLogicalOrOperator(bool condition, out bool valueToReturn) + { + valueToReturn = true; + return condition; + } + + /// + /// Indicates that the And operator should short-circuit when a condition is false + /// + private static bool ShouldShortCircuitLogicalAndOperator(bool condition, out bool valueToReturn) + { + valueToReturn = false; + return !condition; + } + private bool HasRequiredConditions(IReadOnlyList targets, IReadOnlyList conditionals, bool targetingContainer = false) { if (conditionals.Count == 0) { return true; } - if (targets.Count == 0 && requiredItems.Count > 0 && requiredItems.All(ri => ri.MatchOnEmpty)) { return true; } - switch (conditionalComparison) + if (targets.Count == 0 && requiredItems != null && requiredItems.All(ri => ri.MatchOnEmpty)) { return true; } + + (ShouldShortCircuit, bool) shortCircuitMethodPair = conditionalLogicalOperator switch { - case PropertyConditional.Comparison.Or: - for (int i = 0; i < conditionals.Count; i++) + PropertyConditional.LogicalOperatorType.Or => (ShouldShortCircuitLogicalOrOperator, false), + PropertyConditional.LogicalOperatorType.And => (ShouldShortCircuitLogicalAndOperator, true), + _ => throw new NotImplementedException() + }; + var (shouldShortCircuit, didNotShortCircuit) = shortCircuitMethodPair; + + for (int i = 0; i < conditionals.Count; i++) + { + bool valueToReturn; + + var pc = conditionals[i]; + if (!pc.TargetContainer || targetingContainer) + { + if (shouldShortCircuit(AnyTargetMatches(targets, pc.TargetItemComponent, pc), out valueToReturn)) { return valueToReturn; } + continue; + } + + var target = FindTargetItemOrComponent(targets); + var targetItem = target as Item ?? (target as ItemComponent)?.Item; + if (targetItem?.ParentInventory == null) + { + //if we're checking for inequality, not being inside a valid container counts as success + //(not inside a container = the container doesn't have a specific tag/value) + bool comparisonIsNeq = pc.ComparisonOperator == PropertyConditional.ComparisonOperatorType.NotEquals; + if (shouldShortCircuit(comparisonIsNeq, out valueToReturn)) { - var pc = conditionals[i]; - if (pc.TargetContainer && !targetingContainer) - { - var target = FindTargetItemOrComponent(targets); - var targetItem = target as Item ?? (target as ItemComponent)?.Item; - if (targetItem?.ParentInventory == null) - { - //if we're checking for inequality, not being inside a valid container counts as success - //(not inside a container = the container doesn't have a specific tag/value) - if (pc.Operator == PropertyConditional.OperatorType.NotEquals) - { - return true; - } - continue; - } - var owner = targetItem.ParentInventory.Owner; - if (pc.TargetGrandParent && owner is Item ownerItem) - { - owner = ownerItem.ParentInventory?.Owner; - } - if (owner is Item container) - { - if (pc.Type == PropertyConditional.ConditionType.HasTag) - { - //if we're checking for tags, just check the Item object, not the ItemComponents - if (pc.Matches(container)) { return true; } - } - else - { - if (AnyTargetMatches(container.AllPropertyObjects, pc.TargetItemComponentName, pc)) { return true; } - } - } - if (owner is Character character && pc.Matches(character)) { return true; } - } - else - { - if (AnyTargetMatches(targets, pc.TargetItemComponentName, pc)) { return true; } - } + return valueToReturn; } - return false; - case PropertyConditional.Comparison.And: - for (int i = 0; i < conditionals.Count; i++) + continue; + } + var owner = targetItem.ParentInventory.Owner; + if (pc.TargetGrandParent && owner is Item ownerItem) + { + owner = ownerItem.ParentInventory?.Owner; + } + if (owner is Item container) + { + if (pc.Type == PropertyConditional.ConditionType.HasTag) { - var pc = conditionals[i]; - if (pc.TargetContainer && !targetingContainer) - { - var target = FindTargetItemOrComponent(targets); - var targetItem = target as Item ?? (target as ItemComponent)?.Item; - if (targetItem?.ParentInventory == null) - { - //if we're checking for inequality, not being inside a valid container counts as success - //(not inside a container = the container doesn't have a specific tag/value) - if (pc.Operator == PropertyConditional.OperatorType.NotEquals) - { - continue; - } - return false; - } - var owner = targetItem.ParentInventory.Owner; - if (pc.TargetGrandParent && owner is Item ownerItem) - { - owner = ownerItem.ParentInventory?.Owner; - } - if (owner is Item container) - { - if (pc.Type == PropertyConditional.ConditionType.HasTag) - { - //if we're checking for tags, just check the Item object, not the ItemComponents - if (!pc.Matches(container)) { return false; } - } - else - { - if (!AnyTargetMatches(container.AllPropertyObjects, pc.TargetItemComponentName, pc)) { return false; } - } - } - if (owner is Character character && !pc.Matches(character)) { return false; } - } - else - { - if (!AnyTargetMatches(targets, pc.TargetItemComponentName, pc)) { return false; } - } + //if we're checking for tags, just check the Item object, not the ItemComponents + if (shouldShortCircuit(pc.Matches(container), out valueToReturn)) { return valueToReturn; } } - return true; - default: - throw new NotImplementedException(); + else + { + if (shouldShortCircuit(AnyTargetMatches(container.AllPropertyObjects, pc.TargetItemComponent, pc), out valueToReturn)) { return valueToReturn; } + } + } + if (owner is Character character && shouldShortCircuit(pc.Matches(character), out valueToReturn)) { return valueToReturn; } } + return didNotShortCircuit; static bool AnyTargetMatches(IReadOnlyList targets, string targetItemComponentName, PropertyConditional conditional) { @@ -1313,6 +1351,7 @@ namespace Barotrauma { if (OnlyInside && itemComponent.Item.CurrentHull == null) { return false; } if (OnlyOutside && itemComponent.Item.CurrentHull != null) { return false; } + if (!TargetItemComponent.IsNullOrEmpty() && itemComponent.Name != TargetItemComponent) { return false; } if (TargetIdentifiers == null) { return true; } if (TargetIdentifiers.Contains("itemcomponent")) { return true; } if (itemComponent.Item.HasTag(TargetIdentifiers)) { return true; } @@ -1348,9 +1387,10 @@ namespace Barotrauma } private static readonly List intervalsToRemove = new List(); + public bool ShouldWaitForInterval(Entity entity, float deltaTime) { - if (Interval > 0.0f && entity != null) + if (Interval > 0.0f && entity != null && intervalTimers != null) { if (intervalTimers.ContainsKey(entity)) { @@ -1374,13 +1414,13 @@ namespace Barotrauma if (!IsValidTarget(target)) { return; } - if (duration > 0.0f && !Stackable) + if (Duration > 0.0f && !Stackable) { //ignore if not stackable and there's already an identical statuseffect DurationListElement existingEffect = DurationList.Find(d => d.Parent == this && d.Targets.FirstOrDefault() == target); if (existingEffect != null) { - existingEffect.Reset(Math.Max(existingEffect.Timer, duration), user); + existingEffect.Reset(Math.Max(existingEffect.Timer, Duration), user); return; } } @@ -1419,13 +1459,13 @@ namespace Barotrauma return; } - if (duration > 0.0f && !Stackable) + if (Duration > 0.0f && !Stackable) { //ignore if not stackable and there's already an identical statuseffect DurationListElement existingEffect = DurationList.Find(d => d.Parent == this && d.Targets.SequenceEqual(currentTargets)); if (existingEffect != null) { - existingEffect?.Reset(Math.Max(existingEffect.Timer, duration), user); + existingEffect?.Reset(Math.Max(existingEffect.Timer, Duration), user); return; } } @@ -1528,7 +1568,7 @@ namespace Barotrauma for (int j = 0; j < useItemCount; j++) { if (item.Removed) { continue; } - item.Use(deltaTime, useTargetCharacter, useTargetLimb); + item.Use(deltaTime, user: null, useTargetLimb, useTargetCharacter); } } } @@ -1619,9 +1659,9 @@ namespace Barotrauma } } - if (duration > 0.0f) + if (Duration > 0.0f) { - DurationList.Add(new DurationListElement(this, entity, targets, duration, user)); + DurationList.Add(new DurationListElement(this, entity, targets, Duration, user)); } else { @@ -1649,9 +1689,12 @@ namespace Barotrauma } } - foreach (Explosion explosion in Explosions) + if (explosions != null) { - explosion.Explode(position, damageSource: entity, attacker: user); + foreach (Explosion explosion in explosions) + { + explosion.Explode(position, damageSource: entity, attacker: user); + } } bool isNotClient = GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient; @@ -1660,7 +1703,7 @@ namespace Barotrauma { var target = targets[i]; //if the effect has a duration, these will be done in the UpdateAll method - if (duration > 0) { break; } + if (Duration > 0) { break; } if (target == null) { continue; } foreach (Affliction affliction in Afflictions) { @@ -1686,6 +1729,7 @@ namespace Barotrauma { if (limb.IsSevered) { continue; } if (limb.character.Removed || limb.Removed) { continue; } + if (targetLimbs != null && !targetLimbs.Contains(limb.type)) { continue; } newAffliction = GetMultipliedAffliction(affliction, entity, limb.character, deltaTime, multiplyAfflictionsByMaxVitality); AttackResult result = limb.character.DamageLimb(position, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source, allowStacking: !setValue); limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, disableDeltaTime ? result.Damage : result.Damage / deltaTime, allowBeheading: true, attacker: affliction.Source); @@ -1735,7 +1779,7 @@ namespace Barotrauma } } - if (aiTriggers.Count > 0) + if (aiTriggers != null) { Character targetCharacter = target as Character; if (targetCharacter == null) @@ -1770,7 +1814,7 @@ namespace Barotrauma } } - if (talentTriggers.Count > 0) + if (talentTriggers != null) { Character targetCharacter = CharacterFromTarget(target); if (targetCharacter != null && !targetCharacter.Removed) @@ -1785,16 +1829,19 @@ namespace Barotrauma if (isNotClient) { // these effects do not need to be run clientside, as they are replicated from server to clients anyway - foreach (int giveExperience in giveExperiences) + if (giveExperiences != null) { - Character targetCharacter = CharacterFromTarget(target); - if (targetCharacter != null && !targetCharacter.Removed) + foreach (int giveExperience in giveExperiences) { - targetCharacter?.Info?.GiveExperience(giveExperience); + Character targetCharacter = CharacterFromTarget(target); + if (targetCharacter != null && !targetCharacter.Removed) + { + targetCharacter?.Info?.GiveExperience(giveExperience); + } } } - if (giveSkills.Count > 0) + if (giveSkills != null) { foreach (GiveSkill giveSkill in giveSkills) { @@ -1811,7 +1858,7 @@ namespace Barotrauma } } - if (giveTalentInfos.Count > 0) + if (giveTalentInfos != null) { Character targetCharacter = CharacterFromTarget(target); if (targetCharacter?.Info == null) { continue; } @@ -1836,6 +1883,17 @@ namespace Barotrauma } } } + + if (eventTargetTags != null) + { + foreach ((Identifier eventId, Identifier tag) in eventTargetTags) + { + if (GameMain.GameSession.EventManager.ActiveEvents.FirstOrDefault(e => e.Prefab.Identifier == eventId) is ScriptedEvent ev) + { + targets.Where(t => t is Entity).ForEach(t => ev.AddTarget(tag, (Entity)t)); + } + } + } } } @@ -1845,7 +1903,7 @@ namespace Barotrauma fire.Size = new Vector2(FireSize, fire.Size.Y); } - if (isNotClient && GameMain.GameSession?.EventManager is { } eventManager) + if (isNotClient && triggeredEvents != null && GameMain.GameSession?.EventManager is { } eventManager) { foreach (EventPrefab eventPrefab in triggeredEvents) { @@ -1877,81 +1935,83 @@ namespace Barotrauma if (isNotClient && entity != null && Entity.Spawner != null) //clients are not allowed to spawn entities { - foreach (CharacterSpawnInfo characterSpawnInfo in spawnCharacters) + if (spawnCharacters != null) { - var characters = new List(); - for (int i = 0; i < characterSpawnInfo.Count; i++) + foreach (CharacterSpawnInfo characterSpawnInfo in spawnCharacters) { - Entity.Spawner.AddCharacterToSpawnQueue(characterSpawnInfo.SpeciesName, position + Rand.Vector(characterSpawnInfo.Spread, Rand.RandSync.Unsynced) + characterSpawnInfo.Offset, - onSpawn: newCharacter => - { - if (characterSpawnInfo.TotalMaxCount > 0) + var characters = new List(); + for (int i = 0; i < characterSpawnInfo.Count; i++) + { + Entity.Spawner.AddCharacterToSpawnQueue(characterSpawnInfo.SpeciesName, position + Rand.Vector(characterSpawnInfo.Spread, Rand.RandSync.Unsynced) + characterSpawnInfo.Offset, + onSpawn: newCharacter => { - if (Character.CharacterList.Count(c => c.SpeciesName == characterSpawnInfo.SpeciesName && c.TeamID == newCharacter.TeamID) > characterSpawnInfo.TotalMaxCount) + if (characterSpawnInfo.TotalMaxCount > 0) { - Entity.Spawner?.AddEntityToRemoveQueue(newCharacter); - return; - } - } - if (newCharacter.AIController is EnemyAIController enemyAi && - enemyAi.PetBehavior != null && - entity is Item item && - item.ParentInventory is CharacterInventory inv) - { - enemyAi.PetBehavior.Owner = inv.Owner as Character; - } - characters.Add(newCharacter); - if (characters.Count == characterSpawnInfo.Count) - { - SwarmBehavior.CreateSwarm(characters.Cast()); - } - if (!characterSpawnInfo.AfflictionOnSpawn.IsEmpty) - { - if (!AfflictionPrefab.Prefabs.TryGet(characterSpawnInfo.AfflictionOnSpawn, out AfflictionPrefab afflictionPrefab)) - { - DebugConsole.NewMessage($"Could not apply an affliction to the spawned character(s). No affliction with the identifier \"{characterSpawnInfo.AfflictionOnSpawn}\" found.", Color.Red); - return; - } - newCharacter.CharacterHealth.ApplyAffliction(newCharacter.AnimController.MainLimb, afflictionPrefab.Instantiate(characterSpawnInfo.AfflictionStrength)); - } - if (characterSpawnInfo.Stun > 0) - { - newCharacter.SetStun(characterSpawnInfo.Stun); - } - foreach (var target in targets) - { - if (!(target is Character character)) { continue; } - if (characterSpawnInfo.TransferInventory && character.Inventory != null && newCharacter.Inventory != null) - { - if (character.Inventory.Capacity != newCharacter.Inventory.Capacity) { return; } - for (int i = 0; i < character.Inventory.Capacity && i < newCharacter.Inventory.Capacity; i++) + if (Character.CharacterList.Count(c => c.SpeciesName == characterSpawnInfo.SpeciesName && c.TeamID == newCharacter.TeamID) > characterSpawnInfo.TotalMaxCount) { - character.Inventory.GetItemsAt(i).ForEachMod(item => newCharacter.Inventory.TryPutItem(item, i, allowSwapping: true, allowCombine: false, user: null)); + Entity.Spawner?.AddEntityToRemoveQueue(newCharacter); + return; } } - if (characterSpawnInfo.TransferBuffs || characterSpawnInfo.TransferAfflictions) + if (newCharacter.AIController is EnemyAIController enemyAi && + enemyAi.PetBehavior != null && + entity is Item item && + item.ParentInventory is CharacterInventory inv) { - foreach (Affliction affliction in character.CharacterHealth.GetAllAfflictions()) + enemyAi.PetBehavior.Owner = inv.Owner as Character; + } + characters.Add(newCharacter); + if (characters.Count == characterSpawnInfo.Count) + { + SwarmBehavior.CreateSwarm(characters.Cast()); + } + if (!characterSpawnInfo.AfflictionOnSpawn.IsEmpty) + { + if (!AfflictionPrefab.Prefabs.TryGet(characterSpawnInfo.AfflictionOnSpawn, out AfflictionPrefab afflictionPrefab)) { - if (!characterSpawnInfo.TransferAfflictions && characterSpawnInfo.TransferBuffs && affliction.Prefab.IsBuff) + DebugConsole.NewMessage($"Could not apply an affliction to the spawned character(s). No affliction with the identifier \"{characterSpawnInfo.AfflictionOnSpawn}\" found.", Color.Red); + return; + } + newCharacter.CharacterHealth.ApplyAffliction(newCharacter.AnimController.MainLimb, afflictionPrefab.Instantiate(characterSpawnInfo.AfflictionStrength)); + } + if (characterSpawnInfo.Stun > 0) + { + newCharacter.SetStun(characterSpawnInfo.Stun); + } + foreach (var target in targets) + { + if (target is not Character character) { continue; } + if (characterSpawnInfo.TransferInventory && character.Inventory != null && newCharacter.Inventory != null) + { + if (character.Inventory.Capacity != newCharacter.Inventory.Capacity) { return; } + for (int i = 0; i < character.Inventory.Capacity && i < newCharacter.Inventory.Capacity; i++) { - newCharacter.CharacterHealth.ApplyAffliction(newCharacter.AnimController.MainLimb, affliction.Prefab.Instantiate(affliction.Strength)); - } - if (characterSpawnInfo.TransferAfflictions) - { - newCharacter.CharacterHealth.ApplyAffliction(newCharacter.AnimController.MainLimb, affliction.Prefab.Instantiate(affliction.Strength)); + character.Inventory.GetItemsAt(i).ForEachMod(item => newCharacter.Inventory.TryPutItem(item, i, allowSwapping: true, allowCombine: false, user: null)); } } - } - if (i == characterSpawnInfo.Count) // Only perform the below actions if this is the last character being spawned. - { - if (characterSpawnInfo.TransferControl) + if (characterSpawnInfo.TransferBuffs || characterSpawnInfo.TransferAfflictions) { + foreach (Affliction affliction in character.CharacterHealth.GetAllAfflictions()) + { + if (!characterSpawnInfo.TransferAfflictions && characterSpawnInfo.TransferBuffs && affliction.Prefab.IsBuff) + { + newCharacter.CharacterHealth.ApplyAffliction(newCharacter.AnimController.MainLimb, affliction.Prefab.Instantiate(affliction.Strength)); + } + if (characterSpawnInfo.TransferAfflictions) + { + newCharacter.CharacterHealth.ApplyAffliction(newCharacter.AnimController.MainLimb, affliction.Prefab.Instantiate(affliction.Strength)); + } + } + } + if (i == characterSpawnInfo.Count) // Only perform the below actions if this is the last character being spawned. + { + if (characterSpawnInfo.TransferControl) + { #if CLIENT if (Character.Controlled == target) - { - Character.Controlled = newCharacter; - } + { + Character.Controlled = newCharacter; + } #elif SERVER foreach (Client c in GameMain.Server.ConnectedClients) { @@ -1960,31 +2020,44 @@ namespace Barotrauma } #endif } - if (characterSpawnInfo.RemovePreviousCharacter) { Entity.Spawner?.AddEntityToRemoveQueue(character); } - } - } - }); - } - } - - if (spawnItemRandomly) - { - if (spawnItems.Count > 0) - { - SpawnItem(spawnItems.GetRandomUnsynced()); - } - } - else - { - foreach (ItemSpawnInfo itemSpawnInfo in spawnItems) - { - for (int i = 0; i < itemSpawnInfo.Count; i++) - { - SpawnItem(itemSpawnInfo); + if (characterSpawnInfo.RemovePreviousCharacter) { Entity.Spawner?.AddEntityToRemoveQueue(character); } + } + } + if (characterSpawnInfo.InheritEventTags) + { + foreach (var activeEvent in GameMain.GameSession.EventManager.ActiveEvents) + { + if (activeEvent is ScriptedEvent scriptedEvent) + { + scriptedEvent.InheritTags(entity, newCharacter); + } + } + } + }); } } } + if (spawnItems != null) + { + if (spawnItemRandomly) + { + if (spawnItems.Count > 0) + { + SpawnItem(spawnItems.GetRandomUnsynced()); + } + } + else + { + foreach (ItemSpawnInfo itemSpawnInfo in spawnItems) + { + for (int i = 0; i < itemSpawnInfo.Count; i++) + { + SpawnItem(itemSpawnInfo); + } + } + } + } void SpawnItem(ItemSpawnInfo chosenItemSpawnInfo) { @@ -1997,7 +2070,7 @@ namespace Barotrauma switch (chosenItemSpawnInfo.SpawnPosition) { case ItemSpawnInfo.SpawnPositionType.This: - Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, position + Rand.Vector(chosenItemSpawnInfo.Spread, Rand.RandSync.Unsynced), onSpawned: newItem => + Entity.Spawner?.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, position + Rand.Vector(chosenItemSpawnInfo.Spread, Rand.RandSync.Unsynced), onSpawned: newItem => { Projectile projectile = newItem.GetComponent(); if (entity != null) @@ -2082,17 +2155,11 @@ namespace Barotrauma rotation += spread; if (projectile != null) { - Vector2 spawnPos; - if (projectile.Hitscan) - { - spawnPos = sourceBody != null ? sourceBody.SimPosition : entity.SimPosition; - } - else - { - spawnPos = ConvertUnits.ToSimUnits(worldPos); - } + var sourceEntity = (sourceBody as ISpatialEntity) ?? entity; + Vector2 spawnPos = sourceEntity.SimPosition; projectile.Shoot(user, spawnPos, spawnPos, rotation, ignoredBodies: user?.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(), createNetworkEvent: true); + projectile.Item.Submarine = sourceEntity?.Submarine; } else if (newItem.body != null) { @@ -2101,7 +2168,7 @@ namespace Barotrauma newItem.body.ApplyLinearImpulse(impulseDir * chosenItemSpawnInfo.Impulse); } } - newItem.Condition = newItem.MaxCondition * chosenItemSpawnInfo.Condition; + OnItemSpawned(newItem, chosenItemSpawnInfo); }); break; case ItemSpawnInfo.SpawnPositionType.ThisInventory: @@ -2140,7 +2207,7 @@ namespace Barotrauma allowedSlots.Remove(InvSlotType.Any); character.Inventory.TryPutItem(item, null, allowedSlots); } - item.Condition = item.MaxCondition * chosenItemSpawnInfo.Condition; + OnItemSpawned(item, chosenItemSpawnInfo); }); } } @@ -2160,7 +2227,14 @@ namespace Barotrauma { Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, inventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull, onSpawned: (Item newItem) => { - newItem.Condition = newItem.MaxCondition * chosenItemSpawnInfo.Condition; + OnItemSpawned(newItem, chosenItemSpawnInfo); + }); + } + else if (chosenItemSpawnInfo.SpawnIfNotInInventory) + { + Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, position, onSpawned: (Item newItem) => + { + OnItemSpawned(newItem, chosenItemSpawnInfo); }); } } @@ -2190,7 +2264,7 @@ namespace Barotrauma { Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, containedInventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull, onSpawned: (Item newItem) => { - newItem.Condition = newItem.MaxCondition * chosenItemSpawnInfo.Condition; + OnItemSpawned(newItem, chosenItemSpawnInfo); }); } break; @@ -2199,6 +2273,20 @@ namespace Barotrauma } break; } + void OnItemSpawned(Item newItem, ItemSpawnInfo itemSpawnInfo) + { + newItem.Condition = newItem.MaxCondition * itemSpawnInfo.Condition; + if (itemSpawnInfo.InheritEventTags) + { + foreach (var activeEvent in GameMain.GameSession.EventManager.ActiveEvents) + { + if (activeEvent is ScriptedEvent scriptedEvent) + { + scriptedEvent.InheritTags(entity, newItem); + } + } + } + } } } @@ -2210,6 +2298,7 @@ namespace Barotrauma } if (Interval > 0.0f && entity != null) { + intervalTimers ??= new Dictionary(); intervalTimers[entity] = Interval; } @@ -2384,7 +2473,7 @@ namespace Barotrauma float afflictionMultiplier = !setValue && !disableDeltaTime ? deltaTime : 1.0f; if (entity is Item sourceItem) { - if (sourceItem.HasTag("medical")) + if (sourceItem.HasTag(Barotrauma.Tags.MedicalItem)) { afflictionMultiplier *= 1 + targetCharacter.GetStatValue(StatTypes.MedicalItemEffectivenessMultiplier); if (user is not null) @@ -2458,17 +2547,16 @@ namespace Barotrauma DurationList.Clear(); } - public void AddTag(string tag) + public void AddTag(Identifier tag) { if (tags.Contains(tag)) { return; } tags.Add(tag); } - public bool HasTag(string tag) + public bool HasTag(Identifier tag) { if (tag == null) { return true; } - - return (tags.Contains(tag) || tags.Contains(tag.ToLowerInvariant())); + return tags.Contains(tag) || tags.Contains(tag); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/AuthTicket.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/AuthTicket.cs index 94676dc7c..a68b959dd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/AuthTicket.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/AuthTicket.cs @@ -1,18 +1,67 @@ -namespace Barotrauma.Steam +#nullable enable +using System; +using System.Threading.Tasks; +using Barotrauma.Networking; + +namespace Barotrauma.Steam { static partial class SteamManager { - private static Steamworks.AuthTicket currentTicket = null; - public static Steamworks.AuthTicket GetAuthSessionTicket() + private static Option currentMultiplayerTicket = Option.None; + public static Option GetAuthSessionTicketForMultiplayer(Endpoint remoteHostEndpoint) { if (!IsInitialized) { - return null; + return Option.None; } - currentTicket?.Cancel(); - currentTicket = Steamworks.SteamUser.GetAuthSessionTicket(); - return currentTicket; + if (currentMultiplayerTicket.TryUnwrap(out var ticketToCancel)) + { + ticketToCancel.Cancel(); + } + currentMultiplayerTicket = Option.None; + + var netIdentity = remoteHostEndpoint switch + { + LidgrenEndpoint { Address: LidgrenAddress { NetAddress: var ipAddr }, Port: var ipPort } + => (Steamworks.Data.NetIdentity)Steamworks.Data.NetAddress.From(ipAddr, (ushort)ipPort), + SteamP2PEndpoint { SteamId: var steamId } + => (Steamworks.Data.NetIdentity)(Steamworks.SteamId)steamId.Value, + _ + => throw new ArgumentOutOfRangeException(nameof(remoteHostEndpoint)) + }; + var newTicket = Steamworks.SteamUser.GetAuthSessionTicket(netIdentity); + + currentMultiplayerTicket = newTicket != null + ? Option.Some(newTicket) + : Option.None; + + return currentMultiplayerTicket; + } + + private const string GameAnalyticsConsentIdentity = "BarotraumaGameAnalyticsConsent"; + + private static Option currentGameAnalyticsConsentTicket = Option.None; + public static async Task> GetAuthTicketForGameAnalyticsConsent() + { + if (!IsInitialized) + { + return Option.None; + } + + if (currentGameAnalyticsConsentTicket.TryUnwrap(out var ticketToCancel)) + { + ticketToCancel.Cancel(); + } + currentGameAnalyticsConsentTicket = Option.None; + + var newTicket = await Steamworks.SteamUser.GetAuthTicketForWebApi(identity: GameAnalyticsConsentIdentity); + + currentGameAnalyticsConsentTicket = newTicket != null + ? Option.Some(newTicket) + : Option.None; + + return currentGameAnalyticsConsentTicket; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs index 69e268be3..a832a5abe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs @@ -613,7 +613,8 @@ namespace Barotrauma.Steam public static async Task CopyDirectory(string fileListDir, string modName, string from, string to, ShouldCorrectPaths shouldCorrectPaths) { - from = Path.GetFullPath(from); to = Path.GetFullPath(to); + from = Path.GetFullPath(from); + to = Path.GetFullPath(to); Directory.CreateDirectory(to); string convertFromTo(string from) @@ -623,10 +624,16 @@ namespace Barotrauma.Steam string[] subDirs = Directory.GetDirectories(from); foreach (var file in files) { + //ignore hidden files + if (Path.GetFileName(file).StartsWith('.')) { continue; } await CopyFile(fileListDir, modName, file, convertFromTo(file), shouldCorrectPaths); } - foreach (var dir in subDirs) { await CopyDirectory(fileListDir, modName, dir, convertFromTo(dir), shouldCorrectPaths); } + foreach (var dir in subDirs) + { + if (Path.GetFileName(dir) is { } dirName && dirName.StartsWith('.')) { continue; } + await CopyDirectory(fileListDir, modName, dir, convertFromTo(dir), shouldCorrectPaths); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs index 6fae4f6cb..c42374a47 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs @@ -319,7 +319,7 @@ namespace Barotrauma if (causeOfDeath.DamageSource is Item item) { - if (item.HasTag("tool")) + if (item.HasTag(Tags.ToolItem)) { UnlockAchievement(causeOfDeath.Killer, "killtool".ToIdentifier()); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Tags.cs b/Barotrauma/BarotraumaShared/SharedSource/Tags.cs new file mode 100644 index 000000000..282c7d503 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Tags.cs @@ -0,0 +1,102 @@ +using Microsoft.Xna.Framework; + +namespace Barotrauma; + +public static class Tags +{ + public static readonly Identifier Fuel = "reactorfuel".ToIdentifier(); + public static readonly Identifier Reactor = "reactor".ToIdentifier(); + + public static readonly Identifier JunctionBox = "junctionbox".ToIdentifier(); + + public static readonly Identifier Turret = "turret".ToIdentifier(); + public static readonly Identifier Hardpoint = "hardpoint".ToIdentifier(); + public static readonly Identifier Periscope = "periscope".ToIdentifier(); + public static readonly Identifier TurretAmmoSource = "turretammosource".ToIdentifier(); + + public static readonly Identifier GeneticMaterial = "geneticmaterial".ToIdentifier(); + public static readonly Identifier GeneticDevice = "geneticdevice".ToIdentifier(); + + public static readonly Identifier HeavyDivingGear = "deepdiving".ToIdentifier(); + public static readonly Identifier LightDivingGear = "lightdiving".ToIdentifier(); + + public static readonly Identifier FPGACircuit = "fpgacircuit".ToIdentifier(); + public static readonly Identifier RedWire = new Identifier("redwire"); + + /// + /// Diving gear that's suitable for wearing indoors (-> the bots don't try to unequip it when they don't need diving gear) + /// + public static readonly Identifier DivingGearWearableIndoors = "divinggear_wearableindoors".ToIdentifier(); + + public static readonly Identifier DockingPort = "dock".ToIdentifier(); + public static readonly Identifier Ballast = "ballast".ToIdentifier(); + public static readonly Identifier Airlock = "airlock".ToIdentifier(); + + public static readonly Identifier HiddenItemContainer = "hidden".ToIdentifier(); + + public static readonly Identifier MedicalItem = new Identifier("medical"); + + public static readonly Identifier WeldingFuel = "weldingfuel".ToIdentifier(); + public static readonly Identifier DivingGear = "diving".ToIdentifier(); + public static readonly Identifier OxygenSource = "oxygensource".ToIdentifier(); + public static readonly Identifier FireExtinguisher = "fireextinguisher".ToIdentifier(); + public static readonly Identifier FallbackLocker = "locker".ToIdentifier(); + public static readonly Identifier DontTakeItems = "donttakeitems".ToIdentifier(); + public static readonly Identifier ToolItem = "tool".ToIdentifier(); + public static readonly Identifier LogicItem = "logic".ToIdentifier(); + public static readonly Identifier NavTerminal = "navterminal".ToIdentifier(); + public static readonly Identifier IdCard = "identitycard".ToIdentifier(); + public static readonly Identifier WireItem = "wire".ToIdentifier(); + public static readonly Identifier ChairItem = "chair".ToIdentifier(); + public static readonly Identifier ArtifactHolder = "artifactholder".ToIdentifier(); + public static readonly Identifier Thalamus = "thalamus".ToIdentifier(); + + public static readonly Identifier Crate = "crate".ToIdentifier(); + public static readonly Identifier DontSellItems = "dontsellitems".ToIdentifier(); + public static readonly Identifier CargoContainer = "cargocontainer".ToIdentifier(); + + public static readonly Identifier ItemIgnoredByAI = "ignorebyai".ToIdentifier(); + + public static readonly Identifier GuardianShelter = "guardianshelter".ToIdentifier(); + + public static readonly Identifier AllowCleanup = "allowcleanup".ToIdentifier(); + + public static readonly Identifier Weapon = "weapon".ToIdentifier(); + public static readonly Identifier StunnerItem = "stunner".ToIdentifier(); + public static readonly Identifier MobileRadio = "mobileradio".ToIdentifier(); + + /// + /// Any handcuffs. + /// + public static readonly Identifier HandLockerItem = "handlocker".ToIdentifier(); + + /// + /// Vanilla handcuffs. + /// + public static readonly Identifier Handcuffs = "handcuffs".ToIdentifier(); + + /// + /// A battery cell or similar. + /// + public static readonly Identifier MobileBattery = "mobilebattery".ToIdentifier(); + + public static readonly Identifier Traitor = "traitor".ToIdentifier(); + public static readonly Identifier SecondaryTraitor = "secondarytraitor".ToIdentifier(); + public static readonly Identifier AnyTraitor = "anytraitor".ToIdentifier(); + public static readonly Identifier NonTraitor = "nontraitor".ToIdentifier(); + public static readonly Identifier NonTraitorPlayer = "nontraitorplayer".ToIdentifier(); + public static readonly Identifier TraitorMissionItem = "traitormissionitem".ToIdentifier(); + public static readonly Identifier TraitorGuidelinesForSecurity = "traitorguidelinesforsecurity".ToIdentifier(); + + /// + /// Container where the initial gear (diving suit, oxygen tank, etc) of respawning players is placed + /// + public static readonly Identifier RespawnContainer = "respawncontainer".ToIdentifier(); + + /// + /// Container spawned for the gear of a player who despawns (duffel bag) + /// + public static readonly Identifier DespawnContainer = "despawncontainer".ToIdentifier(); + +} + diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs index 3dfef48c5..8fb92ae72 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs @@ -406,7 +406,7 @@ namespace Barotrauma return new ReplaceLString(new TagLString(tag), StringComparison.OrdinalIgnoreCase, replacements); } - public static void ConstructDescription(ref LocalizedString description, XElement descriptionElement) + public static void ConstructDescription(ref LocalizedString description, XElement descriptionElement, Func? customTagReplacer = null) { /* @@ -415,21 +415,28 @@ namespace Barotrauma */ - - LocalizedString extraDescriptionLine = Get(descriptionElement.GetAttributeIdentifier("tag", Identifier.Empty)); + Identifier tag = descriptionElement.GetAttributeIdentifier("tag", Identifier.Empty); + LocalizedString extraDescriptionLine = Get(tag).Fallback(tag.Value); foreach (XElement replaceElement in descriptionElement.Elements()) { if (replaceElement.NameAsIdentifier() != "replace") { continue; } - Identifier tag = replaceElement.GetAttributeIdentifier("tag", Identifier.Empty); - string[] replacementValues = replaceElement.GetAttributeStringArray("value", Array.Empty()); + Identifier variableTag = replaceElement.GetAttributeIdentifier("tag", Identifier.Empty); LocalizedString replacementValue = string.Empty; - for (int i = 0; i < replacementValues.Length; i++) + if (customTagReplacer != null) { - replacementValue += Get(replacementValues[i]).Fallback(replacementValues[i]); - if (i < replacementValues.Length - 1) + replacementValue = customTagReplacer(replaceElement.GetAttributeString("value", string.Empty)); + } + if (replacementValue.IsNullOrWhiteSpace()) + { + string[] replacementValues = replaceElement.GetAttributeStringArray("value", Array.Empty()); + for (int i = 0; i < replacementValues.Length; i++) { - replacementValue += ", "; + replacementValue += Get(replacementValues[i]).Fallback(replacementValues[i]); + if (i < replacementValues.Length - 1) + { + replacementValue += ", "; + } } } if (replaceElement.Attribute("color") != null) @@ -437,15 +444,18 @@ namespace Barotrauma string colorStr = replaceElement.GetAttributeString("color", "255,255,255,255"); replacementValue = $"‖color:{colorStr}‖" + replacementValue + "‖color:end‖"; } - extraDescriptionLine = extraDescriptionLine.Replace(tag, replacementValue); + extraDescriptionLine = extraDescriptionLine.Replace(variableTag, replacementValue); } if (!(description is RawLString { Value: "" })) { description += "\n"; } //TODO: this is cursed description += extraDescriptionLine; } - public static LocalizedString FormatCurrency(int amount) + public static LocalizedString FormatCurrency(int amount, bool includeCurrencySymbol = true) { - return GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", amount)); + string valueString = string.Format(CultureInfo.InvariantCulture, "{0:N0}", amount); + return includeCurrencySymbol ? + GetWithVariable("currencyformat", "[credits]", valueString) : + valueString; } public static LocalizedString GetServerMessage(string serverMessage) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEvent.cs new file mode 100644 index 000000000..556ad87e5 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEvent.cs @@ -0,0 +1,126 @@ +#nullable enable +using Barotrauma.Networking; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + partial class TraitorEvent : ScriptedEvent + { + public enum State + { + Incomplete, + Completed, + Failed + } + + public Action? OnStateChanged; + + private new readonly TraitorEventPrefab prefab; + + public new TraitorEventPrefab Prefab => prefab; + + private LocalizedString codeWord; + + private State currentState; + public State CurrentState + { + get { return currentState; } + set + { + if (currentState == value) { return; } + currentState = value; + OnStateChanged?.Invoke(); + } + } + + private Client? traitor; + + public Client? Traitor => traitor; + + private readonly HashSet secondaryTraitors = new HashSet(); + + public IEnumerable SecondaryTraitors => secondaryTraitors; + + public override string ToString() + { + return $"{nameof(TraitorEvent)} ({prefab.Identifier})"; + } + + private readonly static HashSet nonActionChildElementNames = new HashSet() + { + "icon".ToIdentifier(), + "reputationrequirement".ToIdentifier(), + "missionrequirement".ToIdentifier(), + "levelrequirement".ToIdentifier() + }; + protected override IEnumerable NonActionChildElementNames => nonActionChildElementNames; + + public TraitorEvent(TraitorEventPrefab prefab) : base(prefab) + { + this.prefab = prefab; + codeWord = string.Empty; + } + + public override void Init(EventSet? parentSet = null) + { + base.Init(parentSet); + if (traitor == null) + { + DebugConsole.ThrowError($"Error when initializing event \"{prefab.Identifier}\": traitor not set.\n" + Environment.StackTrace); + } + } + + public override LocalizedString ReplaceVariablesInEventText(LocalizedString str) + { + if (codeWord.IsNullOrEmpty()) + { + //store the code word so the same random word is used in all the actions in the event + codeWord = TextManager.Get("traitor.codeword"); + } + + return str + .Replace("[traitor]", traitor?.Name ?? "none") + .Replace("[target]", (GetTargets("target".ToIdentifier()).FirstOrDefault() as Character)?.DisplayName ?? "none") + .Replace("[codeword]", codeWord.Value); + } + + public void SetTraitor(Client traitor) + { + if (traitor.Character == null) + { + throw new InvalidOperationException($"Tried to set a client who's not controlling a character (\"{traitor.Name}\") as the traitor."); + } + this.traitor = traitor; + traitor.Character.IsTraitor = true; + AddTarget(Tags.Traitor, traitor.Character); + AddTarget(Tags.AnyTraitor, traitor.Character); + AddTargetPredicate(Tags.NonTraitor, e => e is Character c && (c.IsPlayer || c.IsBot) && !c.IsTraitor && c.TeamID == traitor.TeamID && !c.IsIncapacitated); + AddTargetPredicate(Tags.NonTraitorPlayer, e => e is Character c && c.IsPlayer && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated); + } + + public void SetSecondaryTraitors(IEnumerable traitors) + { + int index = 0; + foreach (var traitor in traitors) + { + if (traitor.Character == null) + { + throw new InvalidOperationException($"Tried to set a client who's not controlling a character (\"{traitor.Name}\") as a secondary traitor."); + } + if (this.traitor == traitor) + { + DebugConsole.ThrowError($"Tried to assign the main traitor {traitor.Name} as a secondary traitor."); + continue; + } + secondaryTraitors.Add(traitor); + traitor.Character.IsTraitor = true; + AddTarget(Tags.SecondaryTraitor, traitor.Character); + AddTarget((Tags.SecondaryTraitor.ToString() + index).ToIdentifier(), traitor.Character); + AddTarget(Tags.AnyTraitor, traitor.Character); + index++; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEventPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEventPrefab.cs new file mode 100644 index 000000000..f4e90ae36 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEventPrefab.cs @@ -0,0 +1,350 @@ +#nullable enable +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Globalization; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + class TraitorEventPrefab : EventPrefab + { + class MissionRequirement + { + public Identifier MissionIdentifier; + public Identifier MissionTag; + public MissionType MissionType; + + public MissionRequirement(XElement element, TraitorEventPrefab prefab) + { + MissionIdentifier = element.GetAttributeIdentifier(nameof(MissionIdentifier), Identifier.Empty); + MissionTag = element.GetAttributeIdentifier(nameof(MissionTag), Identifier.Empty); + MissionType = element.GetAttributeEnum(nameof(MissionType), MissionType.None); + if (MissionIdentifier.IsEmpty && MissionTag.IsEmpty && MissionType == MissionType.None) + { + DebugConsole.ThrowError($"Error in traitor event \"{prefab.Identifier}\". Mission requirement with no {nameof(MissionIdentifier)}, {nameof(MissionTag)} or {nameof(MissionType)}."); + } + } + + public bool Match(Mission mission) + { + if (mission == null) { return MissionIdentifier.IsEmpty && MissionTag.IsEmpty && MissionType == MissionType.None; } + if (!MissionIdentifier.IsEmpty) + { + return mission.Prefab.Identifier == MissionIdentifier; + } + else if (!MissionTag.IsEmpty) + { + return mission.Prefab.Tags.Contains(MissionTag); + } + else if (MissionType != MissionType.None) + { + return mission.Prefab.Type == MissionType; + } + return false; + } + } + + class LevelRequirement + { + private enum LevelType + { + LocationConnection, + Outpost, + Any + } + + private readonly LevelType levelType; + public ImmutableArray LocationTypes { get; } + + /// + /// Minimimum difficulty of the level for this event to get selected. Defaults to 0. + /// + private readonly float minDifficulty; + /// + /// Minimimum difficulty of the level for this event to get selected. Defaults to 5 or , whichever is lower. + /// + private readonly float minDifficultyInCampaign; + + //feels a little weird to have something this specific here, but couldn't think of a better way to implement this + public ImmutableArray RequiredItemConditionals; + + public LevelRequirement(XElement element, TraitorEventPrefab prefab) + { + levelType = element.GetAttributeEnum(nameof(LevelType), LevelType.Any); + LocationTypes = element.GetAttributeIdentifierArray("locationtype", Array.Empty()).ToImmutableArray(); + minDifficulty = element.GetAttributeFloat(nameof(minDifficulty), 0.0f); + minDifficultyInCampaign = element.GetAttributeFloat(nameof(minDifficultyInCampaign), Math.Max(minDifficulty, 5.0f)); + List requiredItemConditionals = new List(); + foreach (var subElement in element.Elements()) + { + if (subElement.NameAsIdentifier() == "itemconditional") + { + requiredItemConditionals.AddRange(PropertyConditional.FromXElement(subElement)); + } + } + RequiredItemConditionals = requiredItemConditionals.ToImmutableArray(); + } + + public bool Match(Level level) + { + if (level?.LevelData == null) { return false; } + switch (levelType) + { + case LevelType.LocationConnection: + if (level.LevelData.Type != LevelData.LevelType.LocationConnection) { return false; } + break; + case LevelType.Outpost: + if (level.LevelData.Type != LevelData.LevelType.Outpost) { return false; } + break; + } + if (GameMain.GameSession?.Campaign != null) + { + if (level.Difficulty < minDifficultyInCampaign) { return false; } + } + else + { + if (level.Difficulty < minDifficulty) { return false; } + } + if (level.StartLocation == null) + { + if (LocationTypes.Any()) { return false; } + } + else + { + if (LocationTypes.Any() && !LocationTypes.Contains(level.StartLocation.Type.Identifier)) { return false; } + } + if (RequiredItemConditionals.Any()) + { + bool matchFound = false; + foreach (var item in Item.ItemList) + { + if (RequiredItemConditionals.All(c => item.ConditionalMatches(c))) + { + matchFound = true; + break; + } + } + if (!matchFound) { return false; } + } + return true; + } + } + + class ReputationRequirement + { + public Identifier Faction; + public Identifier CompareToFaction; + public float CompareToValue; + + public readonly PropertyConditional.ComparisonOperatorType Operator; + + public ReputationRequirement(XElement element, TraitorEventPrefab prefab) + { + Faction = element.GetAttributeIdentifier(nameof(Faction), Identifier.Empty); + + string conditionStr = element.GetAttributeString("reputation", string.Empty); + + string[] splitString = conditionStr.Split(' '); + string value; + if (splitString.Length > 0) + { + //the first part of the string is the operator, skip it + value = string.Join(" ", splitString.Skip(1)); + } + else + { + DebugConsole.ThrowError( + $"{conditionStr} in {prefab.Identifier} is too short."+ + "It should start with an operator followed by a faction identifier or a floating point value."); + return; + } + Operator = PropertyConditional.GetComparisonOperatorType(splitString[0]); + + if (float.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var floatVal)) + { + CompareToValue = floatVal; + } + else + { + CompareToFaction = value.ToIdentifier(); + } + } + + public bool Match(CampaignMode campaign) + { + var faction1 = campaign.GetFaction(Faction); + if (faction1 == null) + { + DebugConsole.ThrowError($"Could not find the faction {Faction}."); + return false; + } + if (!CompareToFaction.IsEmpty) + { + var faction2 = campaign.GetFaction(Faction); + if (faction2 == null) + { + DebugConsole.ThrowError($"Could not find the faction {CompareToFaction}."); + return false; + } + return PropertyConditional.CompareFloat(faction1.Reputation.Value, faction2.Reputation.Value, Operator); + } + return PropertyConditional.CompareFloat(faction1.Reputation.Value, CompareToValue, Operator); + } + } + + public readonly Sprite? Icon; + public readonly Color IconColor; + + public const int MinDangerLevel = 1; + public const int MaxDangerLevel = 3; + + public ImmutableHashSet Tags; + + private readonly ImmutableArray reputationRequirements; + private readonly ImmutableArray missionRequirements; + private readonly ImmutableArray levelRequirements; + public bool HasReputationRequirements => reputationRequirements.Any(); + public bool HasMissionRequirements => missionRequirements.Any(); + public bool HasLevelRequirements => levelRequirements.Any(); + + /// + /// An event with one of these tags must've been completed previously for this event to trigger. + /// + public ImmutableHashSet RequiredCompletedTags; + + public readonly int DangerLevel; + + /// + /// Minimum number of non-spectating human players on the server for the event to get selected. + /// + public readonly int MinPlayerCount; + + /// + /// Number of players to assign as a "secondary traitor". + /// If both this and are defined, this is treated as a minimum number of secondary traitors. + /// + public readonly int SecondaryTraitorAmount; + + /// + /// Percentage of players to assign as a "secondary traitor". + /// + public readonly float SecondaryTraitorPercentage; + + /// + /// Does accusing a secondary traitor count as correctly identifying the traitor? + /// + public readonly bool AllowAccusingSecondaryTraitor; + + /// + /// Money penalty if the crew votes a wrong player as the traitor + /// + public readonly int MoneyPenaltyForUnfoundedTraitorAccusation; + + /// + /// Is this event chainable, i.e. does the same traitor get another, higher-lvl one if they complete this one successfully? + /// + public readonly bool IsChainable; + + public readonly float StealPercentageOfExperience; + + public TraitorEventPrefab(ContentXElement element, RandomEventsFile file, Identifier fallbackIdentifier = default) + : base(element, file, fallbackIdentifier) + { + DangerLevel = MathHelper.Clamp(element.GetAttributeInt(nameof(DangerLevel), MinDangerLevel), MinDangerLevel, MaxDangerLevel); + + MinPlayerCount = element.GetAttributeInt(nameof(MinPlayerCount), 0); + + SecondaryTraitorAmount = element.GetAttributeInt(nameof(SecondaryTraitorAmount), 0); + SecondaryTraitorPercentage = element.GetAttributeFloat(nameof(SecondaryTraitorPercentage), 0.0f); + + AllowAccusingSecondaryTraitor = element.GetAttributeBool(nameof(AllowAccusingSecondaryTraitor), true); + + MoneyPenaltyForUnfoundedTraitorAccusation = element.GetAttributeInt(nameof(MoneyPenaltyForUnfoundedTraitorAccusation), 100); + + Tags = element.GetAttributeIdentifierImmutableHashSet(nameof(Tags), ImmutableHashSet.Empty); + RequiredCompletedTags = element.GetAttributeIdentifierImmutableHashSet(nameof(RequiredCompletedTags), ImmutableHashSet.Empty); + + StealPercentageOfExperience = element.GetAttributeFloat(nameof(StealPercentageOfExperience), 0.0f); + + IsChainable = element.GetAttributeBool(nameof(IsChainable), true); + + List reputationRequirements = new List(); + List levelRequirements = new List(); + List missionRequirements = new List(); + foreach (var subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "reputationrequirement": + reputationRequirements.Add(new ReputationRequirement(subElement!, this)); + break; + case "missionrequirement": + missionRequirements.Add(new MissionRequirement(subElement!, this)); + break; + case "levelrequirement": + levelRequirements.Add(new LevelRequirement(subElement!, this)); + break; + case "icon": + Icon = new Sprite(subElement); + IconColor = subElement.GetAttributeColor("color", Color.White); + break; + } + } + this.reputationRequirements = reputationRequirements.ToImmutableArray(); + this.levelRequirements = levelRequirements.ToImmutableArray(); + this.missionRequirements = missionRequirements.ToImmutableArray(); + } + + public bool ReputationRequirementsMet(CampaignMode? campaign) + { + if (campaign == null) + { + //no requirements in the campaign + return true; + } + foreach (ReputationRequirement requirement in reputationRequirements) + { + if (!requirement.Match(campaign)) { return false; } + } + return true; + } + public bool MissionRequirementsMet(GameSession? gameSession) + { + if (gameSession == null) { return false; } + foreach (MissionRequirement requirement in missionRequirements) + { + if (gameSession.Missions.None(m => requirement.Match(m))) { return false; } + } + return true; + } + public bool LevelRequirementsMet(Level? level) + { + if (level == null) { return false; } + //by default (if no requirements are specified) traitor events happen in LocationConnections. + if (levelRequirements.None() && level.Type != LevelData.LevelType.LocationConnection) + { + return false; + } + foreach (LevelRequirement requirement in levelRequirements) + { + if (!requirement.Match(level)) { return false; } + } + return true; + } + + public override void Dispose() + { + Icon?.Remove(); + } + + public override string ToString() + { + return $"{nameof(TraitorEventPrefab)} ({Identifier})"; + } + + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorManager.cs new file mode 100644 index 000000000..ffa848166 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorManager.cs @@ -0,0 +1,49 @@ +#nullable enable + +using Barotrauma.Networking; +using System.Linq; + +namespace Barotrauma +{ + sealed partial class TraitorManager + { + public struct TraitorResults : INetSerializableStruct + { + [NetworkSerialize] + public byte VotedAsTraitorClientSessionId; + + [NetworkSerialize] + public bool VotedCorrectTraitor; + + [NetworkSerialize] + public bool ObjectiveSuccessful; + + [NetworkSerialize] + public int MoneyPenalty; + + [NetworkSerialize] + public Identifier TraitorEventIdentifier; + + public TraitorResults(Client? votedAsTraitor, TraitorEvent traitorEvent) + { + VotedAsTraitorClientSessionId = votedAsTraitor?.SessionId ?? 0; + VotedCorrectTraitor = votedAsTraitor == traitorEvent.Traitor; + if (traitorEvent.Prefab.AllowAccusingSecondaryTraitor && !VotedCorrectTraitor) + { + VotedCorrectTraitor = traitorEvent.SecondaryTraitors.Contains(votedAsTraitor); + } + ObjectiveSuccessful = traitorEvent.CurrentState == TraitorEvent.State.Completed; + MoneyPenalty = votedAsTraitor != null && !VotedCorrectTraitor ? + traitorEvent.Prefab.MoneyPenaltyForUnfoundedTraitorAccusation : + 0; + TraitorEventIdentifier = traitorEvent.Prefab.Identifier; + } + + public Client? GetTraitorClient() + { + int sessionId = VotedAsTraitorClientSessionId; + return GameMain.NetworkMember?.ConnectedClients?.FirstOrDefault(c => c.SessionId == sessionId); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorMissionResult.cs b/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorMissionResult.cs deleted file mode 100644 index a8e58b990..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorMissionResult.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; - -namespace Barotrauma -{ - partial class TraitorMissionResult - { - public readonly string EndMessage; - - public readonly Identifier MissionIdentifier; - - public readonly bool Success; - - public readonly List Characters = new List(); - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/CrossThread.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/CrossThread.cs index a7dedde89..2e78f5bef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/CrossThread.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/CrossThread.cs @@ -7,7 +7,7 @@ namespace Barotrauma { public delegate void TaskDelegate(); - private class Task + private sealed class Task { public TaskDelegate Deleg; public ManualResetEvent Mre; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs index 9bc971557..fbfe75287 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs @@ -2,6 +2,7 @@ using Barotrauma.IO; using System; +using System.Collections.Generic; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; @@ -98,6 +99,16 @@ namespace Barotrauma return new Md5Hash(hash); } + public static Md5Hash MergeHashes(IEnumerable hashes) + { + using IncrementalHash incrementalHash = IncrementalHash.CreateHash(HashAlgorithmName.MD5); + foreach (var hash in hashes) + { + incrementalHash.AppendData(hash.ByteRepresentation); + } + return BytesAsHash(incrementalHash.GetHashAndReset()); + } + public static Md5Hash CalculateForBytes(byte[] bytes) { return new Md5Hash(bytes, calculate: true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs index 112281e50..63e0d5af6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs @@ -41,6 +41,19 @@ namespace Barotrauma public Option Bind(Func> binder) where TType : notnull => TryUnwrap(out T? selfValue) ? binder(selfValue) : Option.None; + public T Match(Func some, Func none) + => TryUnwrap(out T? selfValue) ? some(selfValue) : none(); + + public void Match(Action some, Action none) + { + if (TryUnwrap(out T? selfValue)) + { + some(selfValue); + return; + } + none(); + } + public T Fallback(T fallback) => TryUnwrap(out var v) ? v : fallback; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs index f4adb8a06..dc80cf2f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs @@ -1,4 +1,5 @@ -using System; +#nullable enable +using System; using System.Collections.Generic; using System.IO.Compression; using System.Linq; @@ -9,11 +10,14 @@ using System.Text.RegularExpressions; using Barotrauma.IO; using Microsoft.Xna.Framework; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; namespace Barotrauma { static class SaveUtil { + public const string GameSessionFileName = "gamesession.xml"; + private static readonly string LegacySaveFolder = Path.Combine("Data", "Saves"); private static readonly string LegacyMultiplayerSaveFolder = Path.Combine(LegacySaveFolder, "Multiplayer"); @@ -39,8 +43,6 @@ namespace Barotrauma public static readonly string SubmarineDownloadFolder = Path.Combine("Submarines", "Downloaded"); public static readonly string CampaignDownloadFolder = Path.Combine("Data", "Saves", "Multiplayer_Downloaded"); - public delegate void ProgressDelegate(string sMessage); - public static string TempPath { #if SERVER @@ -72,7 +74,7 @@ namespace Barotrauma try { - GameMain.GameSession.Save(Path.Combine(TempPath, "gamesession.xml")); + GameMain.GameSession.Save(Path.Combine(TempPath, GameSessionFileName)); } catch (Exception e) { @@ -82,7 +84,7 @@ namespace Barotrauma try { - string mainSubPath = null; + string? mainSubPath = null; if (GameMain.GameSession.SubmarineInfo != null) { mainSubPath = Path.Combine(TempPath, GameMain.GameSession.SubmarineInfo.Name + ".sub"); @@ -115,7 +117,7 @@ namespace Barotrauma try { - CompressDirectory(TempPath, filePath, null); + CompressDirectory(TempPath, filePath); } catch (Exception e) { @@ -130,9 +132,9 @@ namespace Barotrauma Submarine.Unload(); GameMain.GameSession = null; DebugConsole.Log("Loading save file: " + filePath); - DecompressToDirectory(filePath, TempPath, null); + DecompressToDirectory(filePath, TempPath); - XDocument doc = XMLExtensions.TryLoadXml(Path.Combine(TempPath, "gamesession.xml")); + XDocument doc = XMLExtensions.TryLoadXml(Path.Combine(TempPath, GameSessionFileName)); if (doc == null) { return; } if (!IsSaveFileCompatible(doc)) @@ -149,40 +151,26 @@ namespace Barotrauma string subPath = Path.Combine(TempPath, saveDoc.Root.GetAttributeString("submarine", "")) + ".sub"; selectedSub = new SubmarineInfo(subPath); - List ownedSubmarines = null; - var ownedSubsElement = saveDoc.Root.Element("ownedsubmarines"); - if (ownedSubsElement != null) + List ownedSubmarines = new List(); + + var ownedSubsElement = saveDoc.Root?.Element("ownedsubmarines"); + if (ownedSubsElement == null) { return ownedSubmarines; } + + foreach (var subElement in ownedSubsElement.Elements()) { - ownedSubmarines = new List(); - foreach (var subElement in ownedSubsElement.Elements()) - { - string subName = subElement.GetAttributeString("name", ""); - string ownedSubPath = Path.Combine(TempPath, subName + ".sub"); - ownedSubmarines.Add(new SubmarineInfo(ownedSubPath)); - } + string subName = subElement.GetAttributeString("name", ""); + string ownedSubPath = Path.Combine(TempPath, subName + ".sub"); + ownedSubmarines.Add(new SubmarineInfo(ownedSubPath)); } return ownedSubmarines; } - public static XDocument LoadGameSessionDoc(string filePath) - { - DebugConsole.Log("Loading game session doc: " + filePath); - try - { - DecompressToDirectory(filePath, TempPath, null); - } - catch (Exception e) - { - DebugConsole.ThrowError("Error decompressing " + filePath, e); - return null; - } + public static bool IsSaveFileCompatible(XDocument? saveDoc) + => IsSaveFileCompatible(saveDoc?.Root); - return XMLExtensions.TryLoadXml(Path.Combine(TempPath, "gamesession.xml")); - } - - public static bool IsSaveFileCompatible(XDocument saveDoc) + public static bool IsSaveFileCompatible(XElement? saveDocRoot) { - if (saveDoc?.Root?.Attribute("version") == null) { return false; } + if (saveDocRoot?.Attribute("version") == null) { return false; } return true; } @@ -192,14 +180,13 @@ namespace Barotrauma { File.Delete(filePath); } - catch (Exception e) { DebugConsole.ThrowError("ERROR: deleting save file \"" + filePath + "\" failed.", e); } //deleting a multiplayer save file -> also delete character data - var fullPath = Path.GetFullPath(Path.GetDirectoryName(filePath)); + var fullPath = Path.GetFullPath(Path.GetDirectoryName(filePath) ?? ""); if (fullPath.Equals(Path.GetFullPath(DefaultMultiplayerSaveFolder)) || fullPath == Path.GetFullPath(GetSaveFolder(SaveType.Multiplayer))) @@ -286,12 +273,12 @@ namespace Barotrauma List saveInfos = new List(); foreach (string file in files) { - XDocument doc = LoadGameSessionDoc(file); - if (!includeInCompatible && !IsSaveFileCompatible(doc)) + var docRoot = ExtractGameSessionRootElementFromSaveFile(file); + if (!includeInCompatible && !IsSaveFileCompatible(docRoot)) { continue; } - if (doc?.Root == null) + if (docRoot == null) { saveInfos.Add(new CampaignMode.SaveInfo( FilePath: file, @@ -304,7 +291,7 @@ namespace Barotrauma List enabledContentPackageNames = new List(); //backwards compatibility - string enabledContentPackagePathsStr = doc.Root.GetAttributeStringUnrestricted("selectedcontentpackages", string.Empty); + string enabledContentPackagePathsStr = docRoot.GetAttributeStringUnrestricted("selectedcontentpackages", string.Empty); foreach (string packagePath in enabledContentPackagePathsStr.Split('|')) { if (string.IsNullOrEmpty(packagePath)) { continue; } @@ -312,7 +299,7 @@ namespace Barotrauma string fileName = Path.GetFileNameWithoutExtension(packagePath); if (fileName == "filelist") { - enabledContentPackageNames.Add(Path.GetFileName(Path.GetDirectoryName(packagePath))); + enabledContentPackageNames.Add(Path.GetFileName(Path.GetDirectoryName(packagePath) ?? "")); } else { @@ -320,7 +307,7 @@ namespace Barotrauma } } - string enabledContentPackageNamesStr = doc.Root.GetAttributeStringUnrestricted("selectedcontentpackagenames", string.Empty); + string enabledContentPackageNamesStr = docRoot.GetAttributeStringUnrestricted("selectedcontentpackagenames", string.Empty); //split on pipes, excluding pipes preceded by \ foreach (string packageName in Regex.Split(enabledContentPackageNamesStr, @"(? 255) + { + throw new Exception( + $"Failed to compress \"{sDir}\" (file name length > 255)."); + } + // File name length is encoded as a 32-bit little endian integer here + zipStream.WriteByte((byte)sRelativePath.Length); + zipStream.WriteByte(0); + zipStream.WriteByte(0); + zipStream.WriteByte(0); + // File name content is encoded as little-endian UTF-16 + var strBytes = Encoding.Unicode.GetBytes(sRelativePath.CleanUpPathCrossPlatform(correctFilenameCase: false)); + zipStream.Write(strBytes, 0, strBytes.Length); //Compress file content byte[] bytes = File.ReadAllBytes(Path.Combine(sDir, sRelativePath)); @@ -402,25 +397,26 @@ namespace Barotrauma zipStream.Write(bytes, 0, bytes.Length); } - public static void CompressDirectory(string sInDir, string sOutFile, ProgressDelegate progress) + public static void CompressDirectory(string sInDir, string sOutFile) { IEnumerable sFiles = Directory.GetFiles(sInDir, "*.*", System.IO.SearchOption.AllDirectories); - int iDirLen = sInDir[sInDir.Length - 1] == Path.DirectorySeparatorChar ? sInDir.Length : sInDir.Length + 1; + int iDirLen = sInDir[^1] == Path.DirectorySeparatorChar ? sInDir.Length : sInDir.Length + 1; - using (FileStream outFile = File.Open(sOutFile, System.IO.FileMode.Create, System.IO.FileAccess.Write)) - using (GZipStream str = new GZipStream(outFile, CompressionMode.Compress)) - foreach (string sFilePath in sFiles) - { - string sRelativePath = sFilePath.Substring(iDirLen); - progress?.Invoke(sRelativePath); - CompressFile(sInDir, sRelativePath, str); - } + using var outFile = File.Open(sOutFile, System.IO.FileMode.Create, System.IO.FileAccess.Write) + ?? throw new Exception($"Failed to create file \"{sOutFile}\""); + using GZipStream str = new GZipStream(outFile, CompressionMode.Compress); + foreach (string sFilePath in sFiles) + { + string sRelativePath = sFilePath.Substring(iDirLen); + CompressFile(sInDir, sRelativePath, str); + } } public static System.IO.Stream DecompressFileToStream(string fileName) { - using FileStream originalFileStream = File.Open(fileName, System.IO.FileMode.Open, System.IO.FileAccess.Read); + using FileStream originalFileStream = File.Open(fileName, System.IO.FileMode.Open, System.IO.FileAccess.Read) + ?? throw new Exception($"Failed to open file \"{fileName}\""); System.IO.MemoryStream streamToReturn = new System.IO.MemoryStream(); using GZipStream gzipStream = new GZipStream(originalFileStream, CompressionMode.Decompress); @@ -443,10 +439,11 @@ namespace Barotrauma return fileDirFull.StartsWith(rootDirFull, StringComparison.OrdinalIgnoreCase); } - - private static bool DecompressFile(bool writeFile, string sDir, System.IO.BinaryReader reader, ProgressDelegate progress, out string fileName) + + private static bool DecompressFile(System.IO.BinaryReader reader, [NotNullWhen(returnValue: true)]out string? fileName, [NotNullWhen(returnValue: true)]out byte[]? fileContent) { fileName = null; + fileContent = null; if (reader.PeekChar() < 0) { return false; } @@ -455,7 +452,7 @@ namespace Barotrauma if (nameLen > 255) { throw new Exception( - $"Failed to decompress \"{sDir}\" (file name length > 255). The file may be corrupted."); + $"Failed to decompress (file name length > 255). The file may be corrupted."); } byte[] strBytes = reader.ReadBytes(nameLen * sizeof(char)); @@ -463,57 +460,40 @@ namespace Barotrauma .Replace('\\', '/'); fileName = sFileName; - progress?.Invoke(sFileName); //Decompress file content int contentLen = reader.ReadInt32(); - byte[] contentBytes = reader.ReadBytes(contentLen); + fileContent = reader.ReadBytes(contentLen); - string sFilePath = Path.Combine(sDir, sFileName); - string sFinalDir = Path.GetDirectoryName(sFilePath); - - if (!IsExtractionPathValid(sDir, sFinalDir)) - { - throw new InvalidOperationException( - $"Error extracting \"{sFileName}\": cannot be extracted to parent directory"); - } - - if (!writeFile) { return true; } - - Directory.CreateDirectory(sFinalDir); - int maxRetries = 4; - for (int i = 0; i <= maxRetries; i++) - { - try - { - using (FileStream outFile = File.Open(sFilePath, System.IO.FileMode.Create, System.IO.FileAccess.Write)) - { - outFile.Write(contentBytes, 0, contentLen); - } - break; - } - catch (System.IO.IOException e) - { - if (i >= maxRetries || !File.Exists(sFilePath)) { throw; } - DebugConsole.NewMessage("Failed decompress file \"" + sFilePath + "\" {" + e.Message + "}, retrying in 250 ms...", Color.Red); - Thread.Sleep(250); - } - } return true; } - public static void DecompressToDirectory(string sCompressedFile, string sDir, ProgressDelegate progress) + public static void DecompressToDirectory(string sCompressedFile, string sDir) { DebugConsole.Log("Decompressing " + sCompressedFile + " to " + sDir + "..."); - int maxRetries = 4; + const int maxRetries = 4; for (int i = 0; i <= maxRetries; i++) { try { - using (var memStream = DecompressFileToStream(sCompressedFile)) - using (System.IO.BinaryReader reader = new System.IO.BinaryReader(memStream)) - while (DecompressFile(true, sDir, reader, progress, out _)) { }; + using var memStream = DecompressFileToStream(sCompressedFile); + using var reader = new System.IO.BinaryReader(memStream); + while (DecompressFile(reader, out var fileName, out var contentBytes)) + { + string sFilePath = Path.Combine(sDir, fileName); + string sFinalDir = Path.GetDirectoryName(sFilePath) ?? ""; + if (!IsExtractionPathValid(sDir, sFinalDir)) + { + throw new InvalidOperationException( + $"Error extracting \"{fileName}\": cannot be extracted to parent directory"); + } + + Directory.CreateDirectory(sFinalDir); + using var outFile = File.Open(sFilePath, System.IO.FileMode.Create, System.IO.FileAccess.Write) + ?? throw new Exception($"Failed to create file \"{sFilePath}\""); + outFile.Write(contentBytes, 0, contentBytes.Length); + } break; } catch (System.IO.IOException e) @@ -527,18 +507,20 @@ namespace Barotrauma public static IEnumerable EnumerateContainedFiles(string sCompressedFile) { - int maxRetries = 4; + const int maxRetries = 4; HashSet paths = new HashSet(); for (int i = 0; i <= maxRetries; i++) { try { - using (var memStream = DecompressFileToStream(sCompressedFile)) - using (System.IO.BinaryReader reader = new System.IO.BinaryReader(memStream)) - while (DecompressFile(false, "", reader, null, out string fileName)) - { - paths.Add(fileName); - } + paths.Clear(); + using var memStream = DecompressFileToStream(sCompressedFile); + using var reader = new System.IO.BinaryReader(memStream); + while (DecompressFile(reader, out var fileName, out _)) + { + paths.Add(fileName); + } + break; } catch (System.IO.IOException e) { @@ -554,44 +536,96 @@ namespace Barotrauma return paths; } - public static void CopyFolder(string sourceDirName, string destDirName, bool copySubDirs, bool overwriteExisting = false) + /// + /// Extracts the save file (including all the subs in it) to a temporary folder and returns the game session document. + /// If you only need the gamesession doc, use instead. + /// + /// + /// + public static XDocument? DecompressSaveAndLoadGameSessionDoc(string savePath) { - // Get the subdirectories for the specified directory. - DirectoryInfo dir = new DirectoryInfo(sourceDirName); - - if (!dir.Exists) + DebugConsole.Log("Loading game session doc: " + savePath); + try { - throw new System.IO.DirectoryNotFoundException( - "Source directory does not exist or could not be found: " - + sourceDirName); + DecompressToDirectory(savePath, TempPath); } - - IEnumerable dirs = dir.GetDirectories(); - // If the destination directory doesn't exist, create it. - if (!Directory.Exists(destDirName)) + catch (Exception e) { - Directory.CreateDirectory(destDirName); + DebugConsole.ThrowError("Error decompressing " + savePath, e); + return null; } + return XMLExtensions.TryLoadXml(Path.Combine(TempPath, "gamesession.xml")); + } - // Get the files in the directory and copy them to the new location. - IEnumerable files = dir.GetFiles(); - foreach (FileInfo file in files) + /// + /// Extract *only* the root element of the gamesession.xml file in the given save. + /// For performance reasons, none of its child elements are returned. + /// + public static XElement? ExtractGameSessionRootElementFromSaveFile(string savePath) + { + const int maxRetries = 4; + for (int i = 0; i <= maxRetries; i++) { - string tempPath = Path.Combine(destDirName, file.Name); - if (!overwriteExisting && File.Exists(tempPath)) { continue; } - file.CopyTo(tempPath, true); - } - - // If copying subdirectories, copy them and their contents to new location. - if (copySubDirs) - { - foreach (DirectoryInfo subdir in dirs) + try { - string tempPath = Path.Combine(destDirName, subdir.Name); - CopyFolder(subdir.FullName, tempPath, copySubDirs, overwriteExisting); + using var memStream = DecompressFileToStream(savePath); + using var reader = new System.IO.BinaryReader(memStream); + while (DecompressFile(reader, out var fileName, out var fileContent)) + { + if (fileName != GameSessionFileName) { continue; } + + // Found the file! Here's a quick byte-wise parser to find the root element + int tagOpenerStartIndex = -1; + for (int j = 0; j < fileContent.Length; j++) + { + if (fileContent[j] == '<') + { + // Found a tag opener: return null if we had already found one + if (tagOpenerStartIndex >= 0) { return null; } + tagOpenerStartIndex = j; + } + else if (j > 0 && fileContent[j] == '?' && fileContent[j - 1] == '<') + { + // Found the XML version element, skip this + tagOpenerStartIndex = -1; + } + else if (fileContent[j] == '>') + { + // Found a tag closer, if we know where the tag opener is then we've found the root element + if (tagOpenerStartIndex < 0) { continue; } + + string elemStr = Encoding.UTF8.GetString(fileContent.AsSpan()[tagOpenerStartIndex..j]) + "/>"; + try + { + return XElement.Parse(elemStr); + } + catch (Exception e) + { + DebugConsole.NewMessage( + $"Failed to parse gamesession root in \"{savePath}\": {{{e.Message}}}.", + Color.Red); + // Parsing the element failed! Return null instead of crashing here + return null; + } + } + } + } + break; + } + catch (System.IO.IOException e) + { + if (i >= maxRetries || !File.Exists(savePath)) { throw; } + + DebugConsole.NewMessage( + $"Failed to decompress file \"{savePath}\" for root extraction {{{e.Message}}}, retrying in 250 ms...", + Color.Red); + Thread.Sleep(250); } } + + return null; } + public static void DeleteDownloadedSubs() { if (Directory.Exists(SubmarineDownloadFolder)) @@ -614,7 +648,7 @@ namespace Barotrauma } } - public static void ClearFolder(string folderName, string[] ignoredFileNames = null) + public static void ClearFolder(string folderName, string[]? ignoredFileNames = null) { DirectoryInfo dir = new DirectoryInfo(folderName); @@ -640,7 +674,7 @@ namespace Barotrauma foreach (DirectoryInfo di in dir.GetDirectories()) { ClearFolder(di.FullName, ignoredFileNames); - int maxRetries = 4; + const int maxRetries = 4; for (int i = 0; i <= maxRetries; i++) { try diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index 9cbc56614..9605361b0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -27,6 +27,31 @@ namespace Barotrauma } } + internal readonly record struct SquareLine(Vector2[] Points, SquareLine.LineType Type) + { + internal enum LineType + { + /// + /// Normal 4 point line + /// + /// + /// ┏━━━ end + /// ┃ + /// start ━━━┛ + /// + FourPointForwardsLine, + /// + /// A line where the end is behind the start and 2 extra points are used to draw it + /// + /// + /// start ━┓ + /// ┏━━━━━━┛ + /// ┗━ end + /// + SixPointBackwardsLine + } + } + static partial class ToolBox { public static bool IsProperFilenameCase(string filename) @@ -396,6 +421,10 @@ namespace Barotrauma public static T SelectWeightedRandom(IEnumerable objects, Func weightMethod, Random random) { + if (typeof(PrefabWithUintIdentifier).IsAssignableFrom(typeof(T))) + { + objects = objects.OrderBy(p => (p as PrefabWithUintIdentifier)?.UintIdentifier ?? 0); + } List objectList = objects.ToList(); List weights = objectList.Select(o => weightMethod(o)).ToList(); return SelectWeightedRandom(objectList, weights, random); @@ -408,7 +437,7 @@ namespace Barotrauma public static T SelectWeightedRandom(IList objects, IList weights, Random random) { - if (objects.Count == 0) { return default(T); } + if (objects.Count == 0) { return default; } if (objects.Count != weights.Count) { @@ -427,7 +456,7 @@ namespace Barotrauma } randomNum -= weights[i]; } - return default(T); + return default; } public static UInt32 IdentifierToUint32Hash(Identifier id, MD5 md5) @@ -806,5 +835,43 @@ namespace Barotrauma if (other.IsIPv4MappedToIPv6) { other = other.MapToIPv4(); } return self.Equals(other); } + + public static SquareLine GetSquareLineBetweenPoints(Vector2 start, Vector2 end, float knobLength = 24f) + { + Vector2[] points = new Vector2[6]; + + // set the start and end points + points[0] = points[1] = points[2] = start; + points[5] = points[4] = points[3] = end; + + points[2].X += (points[3].X - points[2].X) / 2; + points[2].X = Math.Max(points[2].X, points[0].X + knobLength); + points[3].X = points[2].X; + + bool isBehind = false; + + // if the node is "behind" us do some magic to make the line curve to prevent overlapping + if (points[2].X <= points[0].X + knobLength) + { + isBehind = true; + points[1].X += knobLength; + points[2].X = points[2].X; + points[2].Y += (points[4].Y - points[1].Y) / 2; + } + + if (points[3].X >= points[5].X - knobLength) + { + isBehind = true; + points[4].X -= knobLength; + points[3].X = points[4].X; + points[3].Y -= points[3].Y - points[2].Y; + } + + SquareLine.LineType type = isBehind + ? SquareLine.LineType.SixPointBackwardsLine + : SquareLine.LineType.FourPointForwardsLine; + + return new SquareLine(points, type); + } } } diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 75b4957f2..44eaec1b1 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,290 @@ +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.1.14.0 +------------------------------------------------------------------------------------------------------------------------------------------------- + +Traitor overhaul: +- Completely redesigned traitor system, which also now works in the campaign mode. +- The traitor missions (now called "events") have been implemented using the scripted event system, and can be created in the event editor. We unfortunately had to drop support for the old system; even though we aim to not do any backwards-compatibility breaking changes now that we're past 1.0, after evaluating the number of mods that include custom traitor missions and seeing how few there are, we felt that it's better to depreciate the old system instead of maintaining two separate systems. We're sorry for the inconvenience, but we're hopeful the move to the scripted event system will allow modders to create more complex traitor events much more easily! +- A total of 27 completely new traitor events. +- Multi-traitor events and code words are back! +- Instead of the yes/no/maybe traitor setting, you can choose the probability of a traitor event per round (e.g. 50% = a traitor event happens on half the rounds on average). +- Each event has a "danger level" that describes how destructive/dangerous the event is. You can choose how dangerous events can get chosen in the server lobby, for example if you don't want very destructive traitor objectives in the campaign mode. +- If a traitor completes their objective successfully, they may get assigned a more dangerous objective on future rounds (up until the maximum defined danger level is reached). +- Players who haven't been traitors before/recently have a higher chance of getting selected as traitors. +- In the campaign mode, completing a traitor objective gives the traitors rewards such as getting to "steal" a portion of the mission experience. +- The players can vote which player they suspect as the traitor. At the end of the round, if at least half of the players have voted for the same player, that player will be accused as a traitor. If the accusation is correct, the traitor's objective will fail and they will receive no rewards. If the accusation is incorrect, the crew will receive a monetary penalty. +- Added various new traitor items, perhaps most notably the Radio Jammer, a handheld battery-powered device that temporarily disables radio communications (both text and voice chat). + +Stacking and storage changes: +- Added Backpack, essentially a larger variant of the toolbelt with a small speed debuff. Can be obtained through the new assistant talent "Bag It Up", and purchased in stores around mid-way through the campaign. +- Oxygen and fuel tanks no longer stack in player inventories, but their capacity has been increased to compensate. A stack of oxygen tanks would last nearly 45 minutes (and that's not even accounting for high-quality tanks), which meant you could easily carry so much oxygen that running out is practically never a risk. +- Increased stack sizes of most items (such as materials, ammo, meds) to 32. +- Increased cabinet capacities. +- Storage containers can no longer be put in normal cabinet slots. The intention is to prevent being able to use storage containers as a way to significantly increase cabinet capacity (all the way up to 2880!). The larger stack sizes and increased cabinet capacities still allow storing a very large number of items, without having to resort to storage containers which make managing the items in the cabinet more inconvenient. +- Cabinets now have a handful of special "extra slots" specifically for storage containers. Storage containers have other uses besides just increasing storage capacity, and these extra slots are intended to keep them a viable option for those other uses. +- Added buttons for merging stacks and alphabetical sorting to all cabinets. +- Dropped stacks of items behave as one physical object and can be picked up by clicking once, instead of the stack becoming a bunch of individual items that need to be picked up one-by-one. +- Restricted the maximum stack sizes of character inventories and holdable/wearable containers to 8. Meaning you can carry 8 items per slot, but cabinets and crates can fit the "full" x32 stacks. + +Talents: +- Fixed "By the Book" giving you the bonuses regardless if you complete a mission or not. +- Changed how Scavenger talent works: previously it had a chance of doubling the loot in a container, which often led to excessive amounts of loot (and other times, no extra loot at all). Now the chance works per-item instead. +- Fixed nuclear shells and depth charges fabricated with the "Nuclear Option" talent providing incendium when deconstructed, even though it's not used in the fabrication recipe. +- Fixed having one captain with the "Leading by Example" talent and another with "Family" messing up the afflictions applied by them (one trying to apply "Excellent Morale" and the other trying to remove it). + +Misc changes and additions: +- Added Circuit Box, an item that allows creating circuits without having to fabricate and place each component individually on the sub's walls. +- Added lights that indicate the ammo status to turret loaders. +- Characters "grab" the device/item they're interacting with, making it easier to see e.g. whether a character is taking something from a cabinet or just standing next to it. +- Allowed any type of ammo box to be used in the turret ammo box recycling recipes. +- Improved explosion particle effects. +- Added a new abandoned outpost mission variant (relating to one of the shadier factions). +- Pets become unhappy faster. Previously the happiness decrease rate was so low the pets were more likely to starve to death than to become unhappy. +- Pets no longer regenerate health when they're well-fed. This was a leftover from the 1st implementation of the pets, and is no longer necessary, because they now regenerate health while eating. +- Repositioned item interfaces retain their positions between rounds. +- Killing/handcuffing the terrorists during escort missions is no longer required to complete the mission. Fixes other means of detaining them (such as brigging, stunning or paralyzing) not being considered valid before. +- It's now possible to access the gene splicer slot of dead/incapacitated characters. +- Increased fruits' impact tolerances to prevent them from breaking when falling. +- Tweaks to the sound effects when under high pressure. +- Underwater scooter's light turns on when aiming (not just when actually using the scooter). +- Captain's pipe and cigar are used by holding RMB (not LMB+RMB) to make their usage consistent with other "consumables". +- Added broken state sprites for Thalamus' fleshspike and fleshgun. Adjusted the fleshgun particle effect. +- Made characters only flip from one side of a ladder to another when aiming. Makes it easier to interact with things next to the ladder, when the character doesn't automatically flip to the "wrong side" when you try to highlight something. +- Beacon station and cave markers are removed from the sonar once the beacon/cave mission is completed. +- The Output and FalseOutput of switches and periscopes are editable. +- Added a "disable_output" pin to supercapacitors and batteries. Particularly useful for auto-operated turrets, as it can be used to power them down. +- Made auto-operated turrets much better at using pulse lasers, chainguns and other turrets that need to wind up before firing. Previously the AI was too eager to switch between targets, as opposed to waiting for the turret to wind up and actually fire. +- Added "activate_out" output to doors (outputs a signal when someone toggles the door) + an option to make the door NOT open/close when someone tries to toggle it. Can be used to run a signal through some external circuit when someone tries to open a door with integrated buttons. + +Balance: +- Adjusted ammo fabrication recipes to fix lead being too scarce (considering it's needed for almost all ammo types). Now ammunition can be fabricated from different materials, not just lead. +- Revisited the contents, the difficulty, and the rewards of monster missions. +- Balanced reputation gains from missions: it should now be less easy to maintain a positive reputation with all factions. +- Slightly increased character impact damage (i.e. damage taken when you fall or get thrown against something). +- Being under high pressure causes organ damage. Without this, it's possible to basically ignore high pressure as long as you're quick (e.g. you can briefly go outside or to a breached ballast to fix walls). +- Made gravity sphere, guardiansteamcannon and guardianbeamweapon cause a bit of radiation sickness. Afflictions that can't be treated with morphine + bandages should be more common, so here's some! +- Piezo crystals cause a bit of stun and burns on characters. +- Nerfed fractalguardian_emp a bit: longer attack cooldown, shorted EMP range. The previous values made it too easy to get softlocked. +- Added 50% flow resistance to makeshift armor to make it more effective in indoors combat. +- Revisited the effects of liquid oxygenite and the oxygen related effects of deusizine to keep them balanced. Reduced the required skill for applying liquid oxygenite to compensate for the failure effects. +- Reduced Mudraptor's priority of eating dead bodies with the intention of making it not as easy to distract them killing one in a pack. +- Improved the monster nest mission progression by introducing harder variants of all the missions. Adjusted the existing nest missions a bit. +- Buffed the ancient weapon: increase the burn damage from 40 to 50, make them ignore 50% of armor protection, allow them to cut off limbs and break (some) armor. Add stun for stopping power. +- Defined fire damage (on ballast flora) and AI combat priority for the ancient weapon. +- Set the treatmentthreshold for oxygen loss to 100, so that the bots won't try to treat patients so eagerly. Now they should react only slightly before the target faints. Note that this also affects when the suitable treatments are shown in the health interface. + +Optimization: +- Optimizations to situations when there's lots of NPCs nearby. Particularly noticeable in very crowded outposts. +- Optimized the Load Game menu in both singleplayer and multiplayer, dramatically reducing the time the game freezes when opening it. +- Optimized flashlights and other directional lights. +- Loading optimizations (loading screens should now be noticeably quicker). +- Thalamus: spawn the initial cells when the AI is loaded (at the round start) instead of doing that when the player is close by. Gets rid of the notable lag spike when approaching a Thalamus infested wreck. +- Miscellaneous smaller optimizations. + +Submarines: +- Reworked Berilia. +- Reworked R-29. +- Linked fabricators to the cabinets next to them in all vanilla subs. +- Various fixes and improvements to Orca. +- Fixed waypoints outside the submarine not being disabled as obstructed when the connection doesn't overlap a wall. +- Waypoint fixes to most player subs. +- Fixes to transferring items between subs: some items that didn't fit into cabinets weren't put into crates even if they could've been, and some items spawned unnecessary crates because they were configured to spawn in a crate despite being too big to put in one. +- Fixed incorrect "low on fuel" warning when switching to a new sub with no fuel, even if you have fuel in your current sub and opted to transfer them to the new one. + +Multiplayer: +- The skill loss on death can be adjusted in the server settings. Defaults to 50% (previously 75%). +- Fixed characters keeping all the injuries they've received while being "braindead" (killed due to disconnecting), meaning if you for example hack the character to pieces while braindead, and the character respawns next round, they'll spawn dead. +- If a client joins when their character is "braindead" (killed due to disconnection), the character is now revived and the client immediately regains control of it. But only if the character's vitality is above 0 - if they have received other lethal injuries or despawned, they'll have to wait for a "normal" respawn. +- Fixed icon in server details panel in the server list saying that every server is modded. +- Fixed clients spawning as characters even if they've opted to spectate if the game mode is switched from campaign to some other mode and back. +- Fixed fabricator sometimes starting to fabricate an incorrect amount if you click the "start" button quickly after adjusting the amount. +- Fixed occasional "received invalid SetAttackTarget message" errors in multiplayer. Happened when some attack caused the target to be killed and despawned immediately (e.g. when a pet was instakilled by a monster). +- Fixed inability to attach items to walls outside the sub in MP. +- The outpost manager is always killable in the Tormsdale report event even if the killing outpost NPCs is disallowed on the server. +- Fixed certain kinds of network messages being sent using an unreliable delivery method (potentially causing occasional issues with file transfers and traitor messages). +- Fixed inability to choose or vote for a sub if you don't have all the content packages required for the sub. +- Fixed clients not seeing the votes of anyone who voted before them on the ready check. +- Fixed dropping off ladders when you open your own health interface in multiplayer. +- Fixed changing character appearance triggering the "wait X seconds until you can rename again" warning. +- Fixed replaced shuttles not getting repaired client-side when you purchase the replacement, making it look like you can't repair them (because they're already repaired server-side). +- Fixed handheld sonar beacon's textbox becoming empty when you enter something in it in multiplayer. +- Fixed "Attempted to create a network event for an item That hasn't been fully initialized yet" console error when spawning a sonar beacon mid-round. +- Fixed characters getting removed at the end of the round if they've died and then been revived with the "revive" console command. + +AI: +- Fixed searchlights not attracting monsters. +- Fixed bots "cleaning up" (or stealing) batteries from portable pumps. +- Fixed bots never cleaning up detached wires. +- Fixed bots sometimes ignoring the leaks right next to doors/hatches. +- Fixed bots cleaning up items that are being eaten by a pet. +- Monsters not ignore provocative items in the inventories of characters they're configured to ignore (with the targeting state "Idle"). Fixes husks attacking characters that are wearing cultist robes with a diving suit. +- Autopilot now avoids the floating ice chunks using the same logic it uses to avoid ice spires. +- Defined AI combat priority for the alien pistol. +- Adjusted the combat priorities of flamer and steam prototype gun. +- Fixed bots sometimes drowning in the ballast because they didn't think they'd need diving gear if the ballast is not full (but still has enough water to drown). +- Fixed bots sometimes preferring to get a new weapon instead of reloading their current one, even if they had easy access to more ammo. +- Fixed bots accepting orders but not doing anything while handcuffed. The bots will now refuse to do things that they need hands when they are handcuffed. +- Fixed bots still sometimes getting stuck on corners around staircases. +- Bots don't attempt to repair or fix leaks in non-friendly subs (e.g. wrecks or ruins). +- Bots don't attempt to clean items in the main sub in outpost levels. +- Fixed bots sometimes just swimming around when they should be able to get back to the sub. +- Fixed orders persisting even if the target no longer exists after a sub switch (e.g. a bot might attempt to operate a turret that doesn't exist, resulting in them doing nothing). +- Fixed "follow" order not always persisting between rounds. +- Bots don't try to use stabilozine to treat deliriumine poisoning. It only slows down the progress of the poisoning, which is kind of pointless in the case of the non-lethal deliriumine poisoning. + +Alien ruins and artifacts: +- The ruins now start smaller and grow bigger gradually, depending on the difficulty level. +- Ruins start to appear a little later in the campaign (in the second biome). +- Initially only "scan ruin" missions are given, "ruin salvage" and "clear ruin" missions don't appear until later. +- Adjusted the loot distribution in the ruins. +- Fixed guardians and defensebot(?) spawning the blood decal when they get dismembered. +- Fixed guardian pod damage sounds sometimes being inaudible. +- Artifact transport cases require batteries to nullify the effects of the artifacts. The batteries last a little over 8 minutes. Also added some animations and lights to the case when powered. +- Made Faraday Artifact's EMP effect a little stronger, and added a discharge coil effect out of water. +- Thermal Artifact now emits steam in water that burns and damages walls. +- Nasonov Artifact now also disrupts the sonar. Added particle effects. +- Psychosis Artifact now generates watcher's gaze periodically, causing psychosis/nausea and strengthening nearby monsters. +- Sky Artifact now drains water on top of draining oxygen, making them a little more tricky to deal with. +- Doubled the price of all artifacts. +- Fixed artifacts never spawning as random loot in ruins. + +Fixes: +- Fixed 2nd end level being impossible to complete if you've already completed the campaign once, because the logbooks didn't respawn on the 2nd loop. +- Fixed inability to equip one-handed items when you're holding a one-handed container that item can go inside (e.g. trying to equip a revolver while holding a storage container would just put it in the container). +- Fixed an exploit that allowed giving your ID card access to places it shouldn't have access to (by placing it in the inventory of e.g. a bot captain and saving and reloading). +- Fixed mollusk and skitter genes giving much less vigor/hyperactivity than intended, making them practically useless. +- Fixed crashing if you change the language when there's scrolling texts (such as "your skills might be insufficient...") visible on the screen. +- Fixed an exploit that allowed using beds/chairs and crouching to trigger a sort of "noclip". +- Fixed particle "z-fighting" (particles flickering in front of each other). Was particularly noticeable with smoke particles. +- Fixed lighting and LOS behaving strangely on very long walls (not extruding far enough to cover the whole screen, allowing you to see through the wall). +- Fixed rifle reload sounds sometimes not playing if you fire in very rapid succession. +- Fixed files listed in file dialogs (such as when choosing a thumbnail for a mod) being in an inconsistent order on Linux. +- Fixed gaps generating strangely on "Shuttle Shell A Glass A". +- Fixed autoturrets being unable to fire inside hulls. +- Fixed “badvibrations3” event (last part of the event chain where you're assigned to salvage a psychosis artifact) never triggering. +- Fixed "sleight of hand" event not giving you the trinket if you choose the 1st dialog option. +- Fixed husk symbiosis getting removed from characters after finishing the campaign. +- Fixed heavy wrench not stunning unskilled users like it should. +- Fixed Jovian radiation no longer causing any noticeable radiation sickness, because it healed faster than the Jovian radiation causes it. +- Fixed chaingun's "winding up" sound not following the sub. +- Only allow putting syringes in autoinjectors (autoinjecting bandages doesn't make much sense, and doesn't work properly). +- Fixed submarines getting stopped by the collider of the acid mist emitted by acid grenades. +- Fixed connection panel layout not working properly on very large resolutions (pins being excessively large and/or going below the borders of the panel's frame). +- Fixed an issue in the "toy hammer" task given by the Jestmaster. The event would never complete, and instead just endlessly increase your reputation when hitting the monster with the hammer. +- Fixed occasional "no suitable return target found" console errors when leaving a location. +- Fixed explosive slugs exploding in the barrel when shot into the void in the sub editor's test mode. +- Fixed attaching items in-game not aligning them with the grid the same way as in the sub editor (offsetting them by half the size of the grid). +- Fixed event trigger icon (the yellow exclamation mark) hiding the conversation icon when an event targets the outpost manager. +- Fixed husks sometimes moving at an unintendedly slow speed on land, making the walking animation look strange (sort of sliding while barely moving their feet). +- Fixed text display's light turning off when copying one in the sub editor. +- Fixed 40mm grenades not exploding in nuclear shells. +- Fixed bringing the cursor over the inventory interrupting using tools and weapons. +- Minerals can't be grabbed before they've been detached with a plasma cutter (it was confusing to be able to grab the item and have the "detaching" progress bar appear, despite it doing nothing). +- Fixed sonar not displaying markers for other subs in the sub editor test mode. +- Fixed fabricator UI not refreshing the skill/time requirements when selecting a linked cabinet instead of the fabricator itself. +- Fixed fabricator not being able to pull materials from the user's inventory if the user selected a linked cabinet instead of the fabricator itself. +- Fixed submarine preview highlighting hulls that have the same name even if they're not linked or near each other. +- Fixed rope pull force originating from the item's origin instead of the barrel. +- Improved how electrical discharge coils determine which walls are outer walls: use the actual position of the structure's body, and check if there's hulls at either side of it. +- Fixed submarine wall "shell a combo 1 1" being incorrectly configured as a horizontal wall. Caused EDC to behave strangely on these walls. +- Fixed monsters sometimes spawning within the player's sight in wrecks (or ruins). +- Fixed corpses carried to the sub from a wreck just disappearing at the end of the round along with the items in their inventory. +- Fixed infinite loop in Character.CanInteractWith if the target item is in a container it's linked to, and has been configured to be displayed side-by-side. For example, if you place a cabinet linked to a fabricator inside the fabricator. +- Fixed incorrect type of airlock module being sometimes used in levels between outposts (e.g. a colony airlock module even though the outpost is a normal one). +- Fixed 40mm acid grenades sometimes appearing to explode in the grenade launcher client-side. +- Fixed nuclear reactor's explosion effects sometimes not being visible client-side. +- Fixed holdable item's sprite origin not getting mirrored on the x-axis. There was a previous attempt at fixing this, but it didn't work correctly: it adjusted the position where the item is held, in a way that made the sprite appear at the correct position. The sprite origin shouldn't affect the position of the item's body though, just where the sprite is drawn relative to the body. The previous fix also didn't take flipping into account, causing the body to be "the wrong way around" when facing left. +- Turn the reactor auto temp on at round end, if the reactor is actively managed by a bot. Fixes reactor sometimes generating too much power at the beginning of the rounds. Only happens later in the game when the bot operating the reactor is skilled enough to manually manage the reactor. +- Fixed thalamus being allowed to load the cheap nuclear shells (talent item) when it loads the railgun. +- Fixed a rounding error making some hairs/attachments in mods impossible to select. +- Fixed some properties of wearable InheritLimbDepth, InheritScale, InheritSourceRect, InheritOrigin being forced to true, and ObscureOtherWearables being forced to None on all wearable sprites except items regardless of what's set in XML. +- Fixed repair tools hitting severed (or hidden) limbs, which sometimes caused the target to not take damage. +- Fixed ancient weapons not being able to cut minerals. +- Fixed "hidden in game" entities + propeller's damage area indicator being visible in images generated with "wikiimage_sub". +- Fixed gaps that aren't linked to anything (e.g. gaps fully inside a hull) counting as hull breaches on the status monitor. +- Fixed contained items in a held item (e.g. flashlight in a rifle, harpoons in a harpoon gun) rotating "choppily". +- Fixed particle "z-fighting" (flickering in front of each other). Was particularly noticeable with alpha-blended particles like smoke. +- Fixed damage from structure damage "shrapnel explosions" being very inconsistent. The explosion didn't ignore obstacles, meaning any structures between the explosion and the character (including the wall the shrapnel comes from) would drastically reduce the damage. This meant the damage was usually very low, but with specific kinds of wall configurations could be enough to instakill a character. +- Fixed characters letting go of the character they're dragging when moving out from or into a sub. +- Changed the formula that converts the rectangular steering vector on the nav terminal to circular. The previous formula caused the "steering arrow" to point at a slightly different direction than where the submarine was actually trying to head to. Most noticeable when controlling turrets using the nav terminal. +- Fixed diving masks reducing movement speed. +- Fixed water in docked shuttles/drones not affecting the buoyancy of the main submarine as much as it should. +- Fixed sub editor sometimes deleting previously selected entities when deleting something using the right-click context menu. +- Fixed monsters sometimes spawning inside the sub after you restore a beacon station. +- Fixed trying to return back from a "deadend location" with no outpost sometimes not working. +- Fixed fractal guardians damaging themselves with their steam cannons. +- Fixed a rare crash caused by an exception in Explosion.RangedStructureDamage. Happened when structure damage triggered an explosion, and said explosion caused another explosion that caused more structure damage. Could happen e.g. when the shrapnel from a breach kills a terminal cell, which explodes and causes more structure damage. +- Fixed auto-operated turrets being able to function without power. +- Fixed it being possible for the last location before the end levels to turn into an outpost if you manage to spread habitation all the way there. +- Changed sonar flora sprite depths to prevent them from being obscured by the edge chunk objects. +- Fixed some headgear (such as separatist captain hats) clipping through PUCS's helmet. +- Fixed lights shining through items characters are holding. +- Fixed ability to clip your turrets through enemy subs' hulls and launch projectiles inside the sub. +- Fixed "heal [H]" hint being visible when focusing on other characters while climbing ladders, and the health interface opening for one frame if you attempt to heal. +- Fixed inability to damage doors with melee weapons from outside the sub. +- Fixed riot shield rotating in a really strange way when swimming (in the wrong direction relative to the character's rotation). +- Fixed Electrical Discharge Coil behaving strangely (drawing huge amounts of power, but never discharging) when there's not enough power and power flowback through EDCs. +- Fixed "show hidden afflictions" button not scaling correctly when switching the resolution. +- Fixed the abandoned outpost you rescue Subra from never turning to a normal outpost. +- Fixed flashlight not being turned off when swapping the item to the belt slot, if there's something else in the slot (like a toolbelt). +- Fixed incorrect draw order of the diving suit's arms (the "shoulder pad" rendered behing the upper arm). +- Fixed "chem withdrawal" and "drunknodebuffs" (variant of drunkenness if you have a talent that nullifies the negative effects) not healing by itself, meaning it was impossible for them to fully heal except by using the medical clinic. +- Fixed captains spawning without tobacco in the pipe (literally unplayable). + +Modding: +- Added "AllowAsBiomeGate" property to location types to make it possible to prevent specific location types from being used as the biome gate locations. Previously this was hard-coded: any location with an outpost was allowed, excluding the vanilla "abandoned" location type. +- The equipment slot icons are now defined in the style xml, making them moddable. +- Fixed crashing when the particle a particle emitter is configured to emit can't be found. +- Fixed bots trying to sit in chairs someone is carrying (not possible in the vanilla game). +- Fixed crashing if a mod contains a wearable that alphaclips other wearables (e.g. diving helmet) and its texture can't be found. +- Fixed linked shuttles getting disconnected from ruins and beacon stations when moving them into place. +- Fixed shuttles attached to ruins, beacon stations and outposts having default crush depth. Now it's forced to at least the depth at the bottom of the level + 1000 m. +- Fabricators can now be made equippable without crashing the game. +- Added "TargetItemComponent" property to statuseffects. Can be used to restrict which components of an item the effect targets. +- Fixed inheriting ragdoll texture not working correctly if you have monster A which is a variant of monster B, and monster B is overridden by a mod (e.g. large crawler when the base crawler is overridden by a mod). +- Fixed StatusEffects that target the parent (= the container an item is inside) not being able to access the properties of the container's components, just the properties of the Item class. +- Fixed crashing if a bot tries to repair with a repair tool that doesn't need any kind of fuel. +- Fixed outpost generator being too eager to use modules meant for a different type of outpost (e.g. abandoned outpost modules in normal outposts) if it can't fit the correct kind of module in the current outpost layout. Now the generator instead tries to change the layout to accommodate for the correct kind of module. +- Fixed MatchOnEmpty and RequireEmpty requiring the whole inventory to be empty even if targeting a specific slot. +- Fixed contained items in contained items being positioned incorrectly when the "middle" container has been flipped from right to left (e.g. railgun shell whose tip is a separate contained item). +- Added IsFlipped property to limbs and characters. +- Conditionals that target limbs can check the character's SpeciesName/SpeciesGroup. +- Always and OnActive effects can be used on limbs. +- Fixed items that are set to display the state of a contained item on the inventory slot (like a gun showing how full the magazine is) always displaying the condition of the contained item, not how full it is. This wasn't a problem in the vanilla game because all vanilla magazines use the condition to simulate the amount of ammo left, but made it impossible to display the state of a magazine that actually contains the bullets as individual items. +- Throwable components can be used in conjunction with Projectile components to create throwable items that can hit and damage characters. +- Fixed bots detaching repairable items from walls when they're trying to repair. Doesn't affect any vanilla content because there's no detachable repairable items. +- Fixed projectiles not sticking to level walls in multiplayer. Does not affect any vanilla content, because there are no projectiles that stick to level walls. +- Fixed ParticleEmitter's DrawOnTop attribute being ignored, causing the DrawOnTop attribute of the particle prefab to always take priority. +- Fixed items deactivating themselves if they have OnContained/OnNotContained effects but nothing else that needs to update (i.e. OnContained/OnNotContained effects not working on items that don't do anything except executing those effects). +- Fixed "packet size exceeded" errors when there's a very large number of items in an inventory (previously only possible in mods, but with the larger stack sizes, also affected the vanilla game). +- Hidden files and directories (ones starting with a dot, e.g. source control folders) aren't uploaded when publishing a mod on the Workshop. +- Fixed penetration defined in RangedWeapon not doing anything. +- Improvements to decorative level object culling. Fixes level objects disappearing from the screen too eagerly when there's lots of objects visible. Rarely occurred in the vanilla game, but was a common problem with mods that significantly increase the amount of level objects. +- Motion sensors set to detect monsters ignore creatures in the "human" group (i.e. you can have friendly non-human characters that are technically monsters, but not have them trigger motion sensors set to only detect monsters). +- Fixed bots ignoring ItemContainer's ContainableRestrictions when reloading ammo (e.g. if you'd set only certain types of ammo to be contained in a loader, bots wouldn't care). +- Fixed only properties that are editable in the editor getting copied to cloned entities in the sub editor. E.g. if you'd edited something like a door's welding state in an item assembly, copypasting the door would make it unwelded. +- New actions for scripted events: CheckVisibilityAction, CountTargetsAction, WaitForItemUsedAction, WaitForItemFabricatedAction, OnRoundEndAction, EventLogAction. +- Made the tutorial objective list non-tutorial-specific and usable in multiplayer, and renamed TutorialSegmentAction as EventObjectiveAction. + +--------------------------------------------------------------------------------------------------------- +v1.0.21.0 +--------------------------------------------------------------------------------------------------------- + +Fixes: +- Fixed LOS effect sometimes "lagging behind" when the sub is moving fast. +- Fixed some minor visual issues (occasional jitter/flickering) on the LOS effect. +- Fixed some issues in the bot AI that we're causing a large performance hit particularly in situations when there's lots of bots in a sub with leaks. +- Fixed bots abandoning their orders (such as operating a turret) if the room is unsafe (e.g. flooded). +- Fixed an issue in character syncing that occasionally caused disconnects with the error message "Exception thrown while reading segment EntityPosition, tried to read too much data from segment". +- Fixed wires set to be hidden in-game (e.g. invisible circuits built outside the sub) being visible on the Electrician's Goggles. +- Fixed an issue with level resources that caused crashes with certain mods (e.g. ones that include subs with piezo crystals). +- Fixed NPCs waiting on some outpost modules never reaching their targets, causing peculiar behavior. +- Fixed waypoints sometimes not getting connected between outpost modules if there's a very short hallway between them. Addresses some cities missing connections between waypoints, causing AI to be unable to navigate through the modules. +- Fixed some UI layout issues (most noticeably, ultra-wide crew list) on certain resolutions like 3440x1440. +- Fixed campaign saves occasionally failing to load with the error "an item with the same key has already been added". Seemed to only occur when using certain mods. +- Fixed crashing when you e.g. use a pet from some mod in the campaign, disable the mod and reload the save. +- Waypoint adjustments to most submarines, outposts, wrecks, and beacons. Especially on ladders. Should take care of the remaining AI issues on ladders (the old subs in the saves don't get updated, but the fixes apply to new subs that you don't yet own. And ofc all the subs in a new game!) + --------------------------------------------------------------------------------------------------------- v1.0.20.1 --------------------------------------------------------------------------------------------------------- @@ -5,7 +292,7 @@ v1.0.20.1 Fixes: - Fixed hidden structures not colliding anymore. - Wiring debugger: Fixed tooltip rendering under the outer frame of the connection panel. -- Wiring debugger: Fixed the glow sprite on connections having an inconsistent size in different resolutions- +- Wiring debugger: Fixed the glow sprite on connections having an inconsistent size in different resolutions. - Fixed jailbreak_sootman event getting stuck at the 1st SpawnAction, preventing most of the event from working at all. --------------------------------------------------------------------------------------------------------- diff --git a/Barotrauma/BarotraumaShared/hintmanager.xml b/Barotrauma/BarotraumaShared/hintmanager.xml index 3f843afaa..b7611ed1b 100644 --- a/Barotrauma/BarotraumaShared/hintmanager.xml +++ b/Barotrauma/BarotraumaShared/hintmanager.xml @@ -57,17 +57,26 @@ - + - + - + + + + + + + + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/libsteam_api.dylib b/Barotrauma/BarotraumaShared/libsteam_api.dylib deleted file mode 100644 index 97b6446eef52ff3760bebf2b856e446cbf61616d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 446352 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 z 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Ð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 diff --git a/Barotrauma/BarotraumaShared/libsteam_api64.dylib b/Barotrauma/BarotraumaShared/libsteam_api64.dylib deleted file mode 100644 index 97b6446eef52ff3760bebf2b856e446cbf61616d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 446352 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 z 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Ð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 diff --git a/Barotrauma/BarotraumaShared/libsteam_api64.so b/Barotrauma/BarotraumaShared/libsteam_api64.so deleted file mode 100644 index 33762a726e93c776de33f892ffa85e00a9fc7289..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 398662 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⊥qb(yM!= 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

mYym}_Kx4_#}r3itdPFK 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 iR_V;bR6#bf+M>PGFr#^~nND4f)36@Tk public void OnCompleted( Action continuation ) { - Dispatch.OnCallComplete( call, continuation, server ); + if (IsCompleted) + continuation(); + else + Dispatch.OnCallComplete(call, continuation, server); } /// diff --git a/Libraries/Facepunch.Steamworks/Classes/AuthTicketForWebApi.cs b/Libraries/Facepunch.Steamworks/Classes/AuthTicketForWebApi.cs new file mode 100644 index 000000000..d1a363494 --- /dev/null +++ b/Libraries/Facepunch.Steamworks/Classes/AuthTicketForWebApi.cs @@ -0,0 +1,38 @@ +using System; + +namespace Steamworks; + +public class AuthTicketForWebApi : IDisposable +{ + public byte[]? Data { get; private set; } + public uint Handle { get; private set; } + + public bool Canceled { get; private set; } + + public AuthTicketForWebApi( byte[] data, uint handle ) + { + Data = data; + Handle = handle; + } + + /// + /// Cancels a ticket. + /// You should cancel your ticket when you close the game or leave a server. + /// + public void Cancel() + { + if (Handle != 0) + { + SteamUser.Internal?.CancelAuthTicket(Handle); + } + + Handle = 0; + Data = null; + Canceled = true; + } + + public void Dispose() + { + Cancel(); + } +} diff --git a/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Posix.csproj b/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Posix.csproj index 26bfe8f64..0601c1f2d 100644 --- a/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Posix.csproj +++ b/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Posix.csproj @@ -5,7 +5,7 @@ $(DefineConstants);PLATFORM_POSIX64;PLATFORM_POSIX;PLATFORM_64 netstandard2.1 true - latest + latest true false Steamworks @@ -16,6 +16,15 @@ ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + + + PreserveNewest + + + PreserveNewest + + + diff --git a/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Win64.csproj b/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Win64.csproj index 29d8dfcd1..b51a96383 100644 --- a/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Win64.csproj +++ b/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Win64.csproj @@ -5,7 +5,7 @@ $(DefineConstants);PLATFORM_WIN64;PLATFORM_WIN;PLATFORM_64 netstandard2.1 true - 8.0 + 8.0 true true Steamworks @@ -20,17 +20,23 @@ https://github.com/Facepunch/Facepunch.Steamworks Facepunch.Steamworks.jpg facepunch;steam;unity;steamworks;valve - latest + 10 MIT https://github.com/Facepunch/Facepunch.Steamworks.git git + true - + + true + / + + Always - - + true + content + diff --git a/Libraries/Facepunch.Steamworks/Generated/CustomEnums.cs b/Libraries/Facepunch.Steamworks/Generated/CustomEnums.cs index 6fc870701..c7b55f544 100644 --- a/Libraries/Facepunch.Steamworks/Generated/CustomEnums.cs +++ b/Libraries/Facepunch.Steamworks/Generated/CustomEnums.cs @@ -23,6 +23,7 @@ namespace Steamworks StoreAuthURLResponse = 165, MarketEligibilityResponse = 166, DurationControl = 167, + GetTicketForWebApiResponse = 168, GSClientApprove = 201, GSClientDeny = 202, GSClientKick = 203, @@ -51,6 +52,9 @@ namespace Steamworks FriendsEnumerateFollowingList = 346, SetPersonaNameResponse = 347, UnreadChatMessagesChanged = 348, + OverlayBrowserProtocolNavigation = 349, + EquippedProfileItemsChanged = 350, + EquippedProfileItems = 351, FavoritesListChanged = 502, LobbyInvite = 503, LobbyEnter = 504, @@ -69,11 +73,14 @@ namespace Steamworks SteamShutdown = 704, CheckFileSignature = 705, GamepadTextInputDismissed = 714, + AppResumingFromSuspend = 736, + FloatingGamepadTextInputDismissed = 738, + FilterTextDictionaryChanged = 739, DlcInstalled = 1005, - RegisterActivationCodeResponse = 1008, NewUrlLaunchParameters = 1014, AppProofOfPurchaseKeyResponse = 1021, FileDetailsResult = 1023, + TimedTrialStatus = 1030, UserStatsReceived = 1101, UserStatsStored = 1102, UserAchievementStored = 1103, @@ -93,11 +100,10 @@ namespace Steamworks P2PSessionConnectFail = 1203, SteamNetConnectionStatusChangedCallback = 1221, SteamNetAuthenticationStatus = 1222, + SteamNetworkingFakeIPResult = 1223, + SteamNetworkingMessagesSessionRequest = 1251, + SteamNetworkingMessagesSessionFailed = 1252, SteamRelayNetworkStatus = 1281, - RemoteStorageAppSyncedClient = 1301, - RemoteStorageAppSyncedServer = 1302, - RemoteStorageAppSyncProgress = 1303, - RemoteStorageAppSyncStatusCheck = 1305, RemoteStorageFileShareResult = 1307, RemoteStoragePublishFileResult = 1309, RemoteStorageDeletePublishedFileResult = 1311, @@ -122,6 +128,7 @@ namespace Steamworks RemoteStoragePublishedFileUpdated = 1330, RemoteStorageFileWriteAsyncComplete = 1331, RemoteStorageFileReadAsyncComplete = 1332, + RemoteStorageLocalFileChange = 1333, GSStatsReceived = 1800, GSStatsStored = 1801, HTTPRequestCompleted = 2101, @@ -129,6 +136,10 @@ namespace Steamworks HTTPRequestDataReceived = 2103, ScreenshotReady = 2301, ScreenshotRequested = 2302, + SteamInputDeviceConnected = 2801, + SteamInputDeviceDisconnected = 2802, + SteamInputConfigurationLoaded = 2803, + SteamInputGamepadSlotChange = 2804, SteamUGCQueryCompleted = 3401, SteamUGCRequestUGCDetailsResult = 3402, CreateItemResult = 3403, @@ -146,6 +157,8 @@ namespace Steamworks RemoveAppDependencyResult = 3415, GetAppDependenciesResult = 3416, DeleteItemResult = 3417, + UserSubscribedItemsListChanged = 3418, + WorkshopEULAStatus = 3420, SteamAppInstalled = 3901, SteamAppUninstalled = 3902, PlaybackStatusHasChanged = 4001, @@ -187,8 +200,6 @@ namespace Steamworks HTML_UpdateToolTip = 4525, HTML_HideToolTip = 4526, HTML_BrowserRestarted = 4527, - BroadcastUploadStart = 4604, - BroadcastUploadStop = 4605, GetVideoURLResult = 4611, GetOPFSettingsResult = 4624, SteamInventoryResultReady = 4700, @@ -213,6 +224,7 @@ namespace Steamworks ActiveBeaconsUpdated = 5306, SteamRemotePlaySessionConnected = 5701, SteamRemotePlaySessionDisconnected = 5702, + SteamRemotePlayTogetherGuestInvite = 5703, } internal static partial class CallbackTypeFactory { @@ -233,6 +245,7 @@ namespace Steamworks { CallbackType.StoreAuthURLResponse, typeof( StoreAuthURLResponse_t )}, { CallbackType.MarketEligibilityResponse, typeof( MarketEligibilityResponse_t )}, { CallbackType.DurationControl, typeof( DurationControl_t )}, + { CallbackType.GetTicketForWebApiResponse, typeof( GetTicketForWebApiResponse_t )}, { CallbackType.GSClientApprove, typeof( GSClientApprove_t )}, { CallbackType.GSClientDeny, typeof( GSClientDeny_t )}, { CallbackType.GSClientKick, typeof( GSClientKick_t )}, @@ -261,6 +274,9 @@ namespace Steamworks { CallbackType.FriendsEnumerateFollowingList, typeof( FriendsEnumerateFollowingList_t )}, { CallbackType.SetPersonaNameResponse, typeof( SetPersonaNameResponse_t )}, { CallbackType.UnreadChatMessagesChanged, typeof( UnreadChatMessagesChanged_t )}, + { CallbackType.OverlayBrowserProtocolNavigation, typeof( OverlayBrowserProtocolNavigation_t )}, + { CallbackType.EquippedProfileItemsChanged, typeof( EquippedProfileItemsChanged_t )}, + { CallbackType.EquippedProfileItems, typeof( EquippedProfileItems_t )}, { CallbackType.FavoritesListChanged, typeof( FavoritesListChanged_t )}, { CallbackType.LobbyInvite, typeof( LobbyInvite_t )}, { CallbackType.LobbyEnter, typeof( LobbyEnter_t )}, @@ -279,11 +295,14 @@ namespace Steamworks { CallbackType.SteamShutdown, typeof( SteamShutdown_t )}, { CallbackType.CheckFileSignature, typeof( CheckFileSignature_t )}, { CallbackType.GamepadTextInputDismissed, typeof( GamepadTextInputDismissed_t )}, + { CallbackType.AppResumingFromSuspend, typeof( AppResumingFromSuspend_t )}, + { CallbackType.FloatingGamepadTextInputDismissed, typeof( FloatingGamepadTextInputDismissed_t )}, + { CallbackType.FilterTextDictionaryChanged, typeof( FilterTextDictionaryChanged_t )}, { CallbackType.DlcInstalled, typeof( DlcInstalled_t )}, - { CallbackType.RegisterActivationCodeResponse, typeof( RegisterActivationCodeResponse_t )}, { CallbackType.NewUrlLaunchParameters, typeof( NewUrlLaunchParameters_t )}, { CallbackType.AppProofOfPurchaseKeyResponse, typeof( AppProofOfPurchaseKeyResponse_t )}, { CallbackType.FileDetailsResult, typeof( FileDetailsResult_t )}, + { CallbackType.TimedTrialStatus, typeof( TimedTrialStatus_t )}, { CallbackType.UserStatsReceived, typeof( UserStatsReceived_t )}, { CallbackType.UserStatsStored, typeof( UserStatsStored_t )}, { CallbackType.UserAchievementStored, typeof( UserAchievementStored_t )}, @@ -301,11 +320,10 @@ namespace Steamworks { CallbackType.P2PSessionConnectFail, typeof( P2PSessionConnectFail_t )}, { CallbackType.SteamNetConnectionStatusChangedCallback, typeof( SteamNetConnectionStatusChangedCallback_t )}, { CallbackType.SteamNetAuthenticationStatus, typeof( SteamNetAuthenticationStatus_t )}, + { CallbackType.SteamNetworkingFakeIPResult, typeof( SteamNetworkingFakeIPResult_t )}, + { CallbackType.SteamNetworkingMessagesSessionRequest, typeof( SteamNetworkingMessagesSessionRequest_t )}, + { CallbackType.SteamNetworkingMessagesSessionFailed, typeof( SteamNetworkingMessagesSessionFailed_t )}, { CallbackType.SteamRelayNetworkStatus, typeof( SteamRelayNetworkStatus_t )}, - { CallbackType.RemoteStorageAppSyncedClient, typeof( RemoteStorageAppSyncedClient_t )}, - { CallbackType.RemoteStorageAppSyncedServer, typeof( RemoteStorageAppSyncedServer_t )}, - { CallbackType.RemoteStorageAppSyncProgress, typeof( RemoteStorageAppSyncProgress_t )}, - { CallbackType.RemoteStorageAppSyncStatusCheck, typeof( RemoteStorageAppSyncStatusCheck_t )}, { CallbackType.RemoteStorageFileShareResult, typeof( RemoteStorageFileShareResult_t )}, { CallbackType.RemoteStoragePublishFileResult, typeof( RemoteStoragePublishFileResult_t )}, { CallbackType.RemoteStorageDeletePublishedFileResult, typeof( RemoteStorageDeletePublishedFileResult_t )}, @@ -330,6 +348,7 @@ namespace Steamworks { CallbackType.RemoteStoragePublishedFileUpdated, typeof( RemoteStoragePublishedFileUpdated_t )}, { CallbackType.RemoteStorageFileWriteAsyncComplete, typeof( RemoteStorageFileWriteAsyncComplete_t )}, { CallbackType.RemoteStorageFileReadAsyncComplete, typeof( RemoteStorageFileReadAsyncComplete_t )}, + { CallbackType.RemoteStorageLocalFileChange, typeof( RemoteStorageLocalFileChange_t )}, { CallbackType.GSStatsReceived, typeof( GSStatsReceived_t )}, { CallbackType.GSStatsStored, typeof( GSStatsStored_t )}, { CallbackType.HTTPRequestCompleted, typeof( HTTPRequestCompleted_t )}, @@ -337,6 +356,10 @@ namespace Steamworks { CallbackType.HTTPRequestDataReceived, typeof( HTTPRequestDataReceived_t )}, { CallbackType.ScreenshotReady, typeof( ScreenshotReady_t )}, { CallbackType.ScreenshotRequested, typeof( ScreenshotRequested_t )}, + { CallbackType.SteamInputDeviceConnected, typeof( SteamInputDeviceConnected_t )}, + { CallbackType.SteamInputDeviceDisconnected, typeof( SteamInputDeviceDisconnected_t )}, + { CallbackType.SteamInputConfigurationLoaded, typeof( SteamInputConfigurationLoaded_t )}, + { CallbackType.SteamInputGamepadSlotChange, typeof( SteamInputGamepadSlotChange_t )}, { CallbackType.SteamUGCQueryCompleted, typeof( SteamUGCQueryCompleted_t )}, { CallbackType.SteamUGCRequestUGCDetailsResult, typeof( SteamUGCRequestUGCDetailsResult_t )}, { CallbackType.CreateItemResult, typeof( CreateItemResult_t )}, @@ -354,6 +377,8 @@ namespace Steamworks { CallbackType.RemoveAppDependencyResult, typeof( RemoveAppDependencyResult_t )}, { CallbackType.GetAppDependenciesResult, typeof( GetAppDependenciesResult_t )}, { CallbackType.DeleteItemResult, typeof( DeleteItemResult_t )}, + { CallbackType.UserSubscribedItemsListChanged, typeof( UserSubscribedItemsListChanged_t )}, + { CallbackType.WorkshopEULAStatus, typeof( WorkshopEULAStatus_t )}, { CallbackType.SteamAppInstalled, typeof( SteamAppInstalled_t )}, { CallbackType.SteamAppUninstalled, typeof( SteamAppUninstalled_t )}, { CallbackType.PlaybackStatusHasChanged, typeof( PlaybackStatusHasChanged_t )}, @@ -395,8 +420,6 @@ namespace Steamworks { CallbackType.HTML_UpdateToolTip, typeof( HTML_UpdateToolTip_t )}, { CallbackType.HTML_HideToolTip, typeof( HTML_HideToolTip_t )}, { CallbackType.HTML_BrowserRestarted, typeof( HTML_BrowserRestarted_t )}, - { CallbackType.BroadcastUploadStart, typeof( BroadcastUploadStart_t )}, - { CallbackType.BroadcastUploadStop, typeof( BroadcastUploadStop_t )}, { CallbackType.GetVideoURLResult, typeof( GetVideoURLResult_t )}, { CallbackType.GetOPFSettingsResult, typeof( GetOPFSettingsResult_t )}, { CallbackType.SteamInventoryResultReady, typeof( SteamInventoryResultReady_t )}, @@ -421,6 +444,7 @@ namespace Steamworks { CallbackType.ActiveBeaconsUpdated, typeof( ActiveBeaconsUpdated_t )}, { CallbackType.SteamRemotePlaySessionConnected, typeof( SteamRemotePlaySessionConnected_t )}, { CallbackType.SteamRemotePlaySessionDisconnected, typeof( SteamRemotePlaySessionDisconnected_t )}, + { CallbackType.SteamRemotePlayTogetherGuestInvite, typeof( SteamRemotePlayTogetherGuestInvite_t )}, }; } } diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamAppList.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamAppList.cs index 84d17aa29..b67457561 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamAppList.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamAppList.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamAppList : SteamInterface + internal unsafe class ISteamAppList : SteamInterface { internal ISteamAppList( bool IsGameServer ) @@ -49,8 +49,7 @@ namespace Steamworks #endregion internal int GetAppName( AppId nAppID, out string pchName ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchName = memory; + using var mempchName = Helpers.TakeMemory(); var returnValue = _GetAppName( Self, nAppID, mempchName, (1024 * 32) ); pchName = Helpers.MemoryToString( mempchName ); return returnValue; @@ -63,8 +62,7 @@ namespace Steamworks #endregion internal int GetAppInstallDir( AppId nAppID, out string pchDirectory ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchDirectory = memory; + using var mempchDirectory = Helpers.TakeMemory(); var returnValue = _GetAppInstallDir( Self, nAppID, mempchDirectory, (1024 * 32) ); pchDirectory = Helpers.MemoryToString( mempchDirectory ); return returnValue; diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamApps.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamApps.cs index 3e5e1c326..a64317fc6 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamApps.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamApps.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamApps : SteamInterface + internal unsafe class ISteamApps : SteamInterface { internal ISteamApps( bool IsGameServer ) @@ -18,9 +18,6 @@ namespace Steamworks [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamApps_v008", CallingConvention = Platform.CC)] internal static extern IntPtr SteamAPI_SteamApps_v008(); public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamApps_v008(); - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamGameServerApps_v008", CallingConvention = Platform.CC)] - internal static extern IntPtr SteamAPI_SteamGameServerApps_v008(); - public override IntPtr GetServerInterfacePointer() => SteamAPI_SteamGameServerApps_v008(); #region FunctionMeta @@ -159,8 +156,7 @@ namespace Steamworks #endregion internal bool BGetDLCDataByIndex( int iDLC, ref AppId pAppID, [MarshalAs( UnmanagedType.U1 )] ref bool pbAvailable, out string pchName ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchName = memory; + using var mempchName = Helpers.TakeMemory(); var returnValue = _BGetDLCDataByIndex( Self, iDLC, ref pAppID, ref pbAvailable, mempchName, (1024 * 32) ); pchName = Helpers.MemoryToString( mempchName ); return returnValue; @@ -204,8 +200,7 @@ namespace Steamworks #endregion internal bool GetCurrentBetaName( out string pchName ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchName = memory; + using var mempchName = Helpers.TakeMemory(); var returnValue = _GetCurrentBetaName( Self, mempchName, (1024 * 32) ); pchName = Helpers.MemoryToString( mempchName ); return returnValue; @@ -241,8 +236,7 @@ namespace Steamworks #endregion internal uint GetAppInstallDir( AppId appID, out string pchFolder ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchFolder = memory; + using var mempchFolder = Helpers.TakeMemory(); var returnValue = _GetAppInstallDir( Self, appID, mempchFolder, (1024 * 32) ); pchFolder = Helpers.MemoryToString( mempchFolder ); return returnValue; @@ -333,8 +327,7 @@ namespace Steamworks #endregion internal int GetLaunchCommandLine( out string pszCommandLine ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempszCommandLine = memory; + using var mempszCommandLine = Helpers.TakeMemory(); var returnValue = _GetLaunchCommandLine( Self, mempszCommandLine, (1024 * 32) ); pszCommandLine = Helpers.MemoryToString( mempszCommandLine ); return returnValue; @@ -352,5 +345,29 @@ namespace Steamworks return returnValue; } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamApps_BIsTimedTrial", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _BIsTimedTrial( IntPtr self, ref uint punSecondsAllowed, ref uint punSecondsPlayed ); + + #endregion + internal bool BIsTimedTrial( ref uint punSecondsAllowed, ref uint punSecondsPlayed ) + { + var returnValue = _BIsTimedTrial( Self, ref punSecondsAllowed, ref punSecondsPlayed ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamApps_SetDlcContext", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _SetDlcContext( IntPtr self, AppId nAppID ); + + #endregion + internal bool SetDlcContext( AppId nAppID ) + { + var returnValue = _SetDlcContext( Self, nAppID ); + return returnValue; + } + } } diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamClient.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamClient.cs index 7b8e94540..418f5dab6 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamClient.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamClient.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamClient : SteamInterface + internal unsafe class ISteamClient : SteamInterface { internal ISteamClient( bool IsGameServer ) diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamController.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamController.cs index 4e26a1581..d93dccffe 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamController.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamController.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamController : SteamInterface + internal unsafe class ISteamController : SteamInterface { internal ISteamController( bool IsGameServer ) @@ -15,9 +15,9 @@ namespace Steamworks SetupInterface( IsGameServer ); } - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamController_v007", CallingConvention = Platform.CC)] - internal static extern IntPtr SteamAPI_SteamController_v007(); - public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamController_v007(); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamController_v008", CallingConvention = Platform.CC)] + internal static extern IntPtr SteamAPI_SteamController_v008(); + public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamController_v008(); #region FunctionMeta diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamFriends.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamFriends.cs index d70f37c2b..b8e5cd443 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamFriends.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamFriends.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamFriends : SteamInterface + internal unsafe class ISteamFriends : SteamInterface { internal ISteamFriends( bool IsGameServer ) @@ -840,5 +840,72 @@ namespace Steamworks _ActivateGameOverlayRemotePlayTogetherInviteDialog( Self, steamIDLobby ); } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamFriends_RegisterProtocolInOverlayBrowser", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _RegisterProtocolInOverlayBrowser( IntPtr self, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchProtocol ); + + #endregion + internal bool RegisterProtocolInOverlayBrowser( [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchProtocol ) + { + var returnValue = _RegisterProtocolInOverlayBrowser( Self, pchProtocol ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamFriends_ActivateGameOverlayInviteDialogConnectString", CallingConvention = Platform.CC)] + private static extern void _ActivateGameOverlayInviteDialogConnectString( IntPtr self, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchConnectString ); + + #endregion + internal void ActivateGameOverlayInviteDialogConnectString( [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchConnectString ) + { + _ActivateGameOverlayInviteDialogConnectString( Self, pchConnectString ); + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamFriends_RequestEquippedProfileItems", CallingConvention = Platform.CC)] + private static extern SteamAPICall_t _RequestEquippedProfileItems( IntPtr self, SteamId steamID ); + + #endregion + internal CallResult RequestEquippedProfileItems( SteamId steamID ) + { + var returnValue = _RequestEquippedProfileItems( Self, steamID ); + return new CallResult( returnValue, IsServer ); + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamFriends_BHasEquippedProfileItem", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _BHasEquippedProfileItem( IntPtr self, SteamId steamID, CommunityProfileItemType itemType ); + + #endregion + internal bool BHasEquippedProfileItem( SteamId steamID, CommunityProfileItemType itemType ) + { + var returnValue = _BHasEquippedProfileItem( Self, steamID, itemType ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamFriends_GetProfileItemPropertyString", CallingConvention = Platform.CC)] + private static extern Utf8StringPointer _GetProfileItemPropertyString( IntPtr self, SteamId steamID, CommunityProfileItemType itemType, CommunityProfileItemProperty prop ); + + #endregion + internal string GetProfileItemPropertyString( SteamId steamID, CommunityProfileItemType itemType, CommunityProfileItemProperty prop ) + { + var returnValue = _GetProfileItemPropertyString( Self, steamID, itemType, prop ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamFriends_GetProfileItemPropertyUint", CallingConvention = Platform.CC)] + private static extern uint _GetProfileItemPropertyUint( IntPtr self, SteamId steamID, CommunityProfileItemType itemType, CommunityProfileItemProperty prop ); + + #endregion + internal uint GetProfileItemPropertyUint( SteamId steamID, CommunityProfileItemType itemType, CommunityProfileItemProperty prop ) + { + var returnValue = _GetProfileItemPropertyUint( Self, steamID, itemType, prop ); + return returnValue; + } + } } diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamGameSearch.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamGameSearch.cs index bad5eb31b..3c70b84f7 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamGameSearch.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamGameSearch.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamGameSearch : SteamInterface + internal unsafe class ISteamGameSearch : SteamInterface { internal ISteamGameSearch( bool IsGameServer ) @@ -82,8 +82,7 @@ namespace Steamworks #endregion internal GameSearchErrorCode_t RetrieveConnectionDetails( SteamId steamIDHost, out string pchConnectionDetails ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchConnectionDetails = memory; + using var mempchConnectionDetails = Helpers.TakeMemory(); var returnValue = _RetrieveConnectionDetails( Self, steamIDHost, mempchConnectionDetails, (1024 * 32) ); pchConnectionDetails = Helpers.MemoryToString( mempchConnectionDetails ); return returnValue; diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamGameServer.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamGameServer.cs index 719af098a..b80a6953c 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamGameServer.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamGameServer.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamGameServer : SteamInterface + internal unsafe class ISteamGameServer : SteamInterface { internal ISteamGameServer( bool IsGameServer ) @@ -15,9 +15,9 @@ namespace Steamworks SetupInterface( IsGameServer ); } - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamGameServer_v013", CallingConvention = Platform.CC)] - internal static extern IntPtr SteamAPI_SteamGameServer_v013(); - public override IntPtr GetServerInterfacePointer() => SteamAPI_SteamGameServer_v013(); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamGameServer_v015", CallingConvention = Platform.CC)] + internal static extern IntPtr SteamAPI_SteamGameServer_v015(); + public override IntPtr GetServerInterfacePointer() => SteamAPI_SteamGameServer_v015(); #region FunctionMeta @@ -258,58 +258,23 @@ namespace Steamworks } #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamGameServer_SendUserConnectAndAuthenticate", CallingConvention = Platform.CC)] - [return: MarshalAs( UnmanagedType.I1 )] - private static extern bool _SendUserConnectAndAuthenticate( IntPtr self, uint unIPClient, IntPtr pvAuthBlob, uint cubAuthBlobSize, ref SteamId pSteamIDUser ); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamGameServer_SetAdvertiseServerActive", CallingConvention = Platform.CC)] + private static extern void _SetAdvertiseServerActive( IntPtr self, [MarshalAs( UnmanagedType.U1 )] bool bActive ); #endregion - internal bool SendUserConnectAndAuthenticate( uint unIPClient, IntPtr pvAuthBlob, uint cubAuthBlobSize, ref SteamId pSteamIDUser ) + internal void SetAdvertiseServerActive( [MarshalAs( UnmanagedType.U1 )] bool bActive ) { - var returnValue = _SendUserConnectAndAuthenticate( Self, unIPClient, pvAuthBlob, cubAuthBlobSize, ref pSteamIDUser ); - return returnValue; - } - - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamGameServer_CreateUnauthenticatedUserConnection", CallingConvention = Platform.CC)] - private static extern SteamId _CreateUnauthenticatedUserConnection( IntPtr self ); - - #endregion - internal SteamId CreateUnauthenticatedUserConnection() - { - var returnValue = _CreateUnauthenticatedUserConnection( Self ); - return returnValue; - } - - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamGameServer_SendUserDisconnect", CallingConvention = Platform.CC)] - private static extern void _SendUserDisconnect( IntPtr self, SteamId steamIDUser ); - - #endregion - internal void SendUserDisconnect( SteamId steamIDUser ) - { - _SendUserDisconnect( Self, steamIDUser ); - } - - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamGameServer_BUpdateUserData", CallingConvention = Platform.CC)] - [return: MarshalAs( UnmanagedType.I1 )] - private static extern bool _BUpdateUserData( IntPtr self, SteamId steamIDUser, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchPlayerName, uint uScore ); - - #endregion - internal bool BUpdateUserData( SteamId steamIDUser, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchPlayerName, uint uScore ) - { - var returnValue = _BUpdateUserData( Self, steamIDUser, pchPlayerName, uScore ); - return returnValue; + _SetAdvertiseServerActive( Self, bActive ); } #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamGameServer_GetAuthSessionTicket", CallingConvention = Platform.CC)] - private static extern HAuthTicket _GetAuthSessionTicket( IntPtr self, IntPtr pTicket, int cbMaxTicket, ref uint pcbTicket ); + private static extern HAuthTicket _GetAuthSessionTicket( IntPtr self, IntPtr pTicket, int cbMaxTicket, ref uint pcbTicket, ref NetIdentity pSnid ); #endregion - internal HAuthTicket GetAuthSessionTicket( IntPtr pTicket, int cbMaxTicket, ref uint pcbTicket ) + internal HAuthTicket GetAuthSessionTicket( IntPtr pTicket, int cbMaxTicket, ref uint pcbTicket, ref NetIdentity pSnid ) { - var returnValue = _GetAuthSessionTicket( Self, pTicket, cbMaxTicket, ref pcbTicket ); + var returnValue = _GetAuthSessionTicket( Self, pTicket, cbMaxTicket, ref pcbTicket, ref pSnid ); return returnValue; } @@ -422,36 +387,6 @@ namespace Steamworks return returnValue; } - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamGameServer_EnableHeartbeats", CallingConvention = Platform.CC)] - private static extern void _EnableHeartbeats( IntPtr self, [MarshalAs( UnmanagedType.U1 )] bool bActive ); - - #endregion - internal void EnableHeartbeats( [MarshalAs( UnmanagedType.U1 )] bool bActive ) - { - _EnableHeartbeats( Self, bActive ); - } - - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamGameServer_SetHeartbeatInterval", CallingConvention = Platform.CC)] - private static extern void _SetHeartbeatInterval( IntPtr self, int iHeartbeatInterval ); - - #endregion - internal void SetHeartbeatInterval( int iHeartbeatInterval ) - { - _SetHeartbeatInterval( Self, iHeartbeatInterval ); - } - - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamGameServer_ForceHeartbeat", CallingConvention = Platform.CC)] - private static extern void _ForceHeartbeat( IntPtr self ); - - #endregion - internal void ForceHeartbeat() - { - _ForceHeartbeat( Self ); - } - #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamGameServer_AssociateWithClan", CallingConvention = Platform.CC)] private static extern SteamAPICall_t _AssociateWithClan( IntPtr self, SteamId steamIDClan ); @@ -474,5 +409,50 @@ namespace Steamworks return new CallResult( returnValue, IsServer ); } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamGameServer_SendUserConnectAndAuthenticate_DEPRECATED", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _SendUserConnectAndAuthenticate_DEPRECATED( IntPtr self, uint unIPClient, IntPtr pvAuthBlob, uint cubAuthBlobSize, ref SteamId pSteamIDUser ); + + #endregion + internal bool SendUserConnectAndAuthenticate_DEPRECATED( uint unIPClient, IntPtr pvAuthBlob, uint cubAuthBlobSize, ref SteamId pSteamIDUser ) + { + var returnValue = _SendUserConnectAndAuthenticate_DEPRECATED( Self, unIPClient, pvAuthBlob, cubAuthBlobSize, ref pSteamIDUser ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamGameServer_CreateUnauthenticatedUserConnection", CallingConvention = Platform.CC)] + private static extern SteamId _CreateUnauthenticatedUserConnection( IntPtr self ); + + #endregion + internal SteamId CreateUnauthenticatedUserConnection() + { + var returnValue = _CreateUnauthenticatedUserConnection( Self ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamGameServer_SendUserDisconnect_DEPRECATED", CallingConvention = Platform.CC)] + private static extern void _SendUserDisconnect_DEPRECATED( IntPtr self, SteamId steamIDUser ); + + #endregion + internal void SendUserDisconnect_DEPRECATED( SteamId steamIDUser ) + { + _SendUserDisconnect_DEPRECATED( Self, steamIDUser ); + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamGameServer_BUpdateUserData", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _BUpdateUserData( IntPtr self, SteamId steamIDUser, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchPlayerName, uint uScore ); + + #endregion + internal bool BUpdateUserData( SteamId steamIDUser, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchPlayerName, uint uScore ) + { + var returnValue = _BUpdateUserData( Self, steamIDUser, pchPlayerName, uScore ); + return returnValue; + } + } } diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamGameServerStats.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamGameServerStats.cs index fee875705..de293cbe5 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamGameServerStats.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamGameServerStats.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamGameServerStats : SteamInterface + internal unsafe class ISteamGameServerStats : SteamInterface { internal ISteamGameServerStats( bool IsGameServer ) diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamHTMLSurface.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamHTMLSurface.cs index e9bc08dd9..38c6b462e 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamHTMLSurface.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamHTMLSurface.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamHTMLSurface : SteamInterface + internal unsafe class ISteamHTMLSurface : SteamInterface { internal ISteamHTMLSurface( bool IsGameServer ) diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamHTTP.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamHTTP.cs index 3d1d7b148..599054b5c 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamHTTP.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamHTTP.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamHTTP : SteamInterface + internal unsafe class ISteamHTTP : SteamInterface { internal ISteamHTTP( bool IsGameServer ) diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamInput.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamInput.cs index 5a1efd81f..3d1ab5227 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamInput.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamInput.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamInput : SteamInterface + internal unsafe class ISteamInput : SteamInterface { internal ISteamInput( bool IsGameServer ) @@ -15,20 +15,20 @@ namespace Steamworks SetupInterface( IsGameServer ); } - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamInput_v001", CallingConvention = Platform.CC)] - internal static extern IntPtr SteamAPI_SteamInput_v001(); - public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamInput_v001(); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamInput_v006", CallingConvention = Platform.CC)] + internal static extern IntPtr SteamAPI_SteamInput_v006(); + public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamInput_v006(); #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_Init", CallingConvention = Platform.CC)] [return: MarshalAs( UnmanagedType.I1 )] - private static extern bool _Init( IntPtr self ); + private static extern bool _Init( IntPtr self, [MarshalAs( UnmanagedType.U1 )] bool bExplicitlyCallRunFrame ); #endregion - internal bool Init() + internal bool Init( [MarshalAs( UnmanagedType.U1 )] bool bExplicitlyCallRunFrame ) { - var returnValue = _Init( Self ); + var returnValue = _Init( Self, bExplicitlyCallRunFrame ); return returnValue; } @@ -45,13 +45,49 @@ namespace Steamworks } #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_RunFrame", CallingConvention = Platform.CC)] - private static extern void _RunFrame( IntPtr self ); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_SetInputActionManifestFilePath", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _SetInputActionManifestFilePath( IntPtr self, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchInputActionManifestAbsolutePath ); #endregion - internal void RunFrame() + internal bool SetInputActionManifestFilePath( [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchInputActionManifestAbsolutePath ) { - _RunFrame( Self ); + var returnValue = _SetInputActionManifestFilePath( Self, pchInputActionManifestAbsolutePath ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_RunFrame", CallingConvention = Platform.CC)] + private static extern void _RunFrame( IntPtr self, [MarshalAs( UnmanagedType.U1 )] bool bReservedValue ); + + #endregion + internal void RunFrame( [MarshalAs( UnmanagedType.U1 )] bool bReservedValue ) + { + _RunFrame( Self, bReservedValue ); + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_BWaitForData", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _BWaitForData( IntPtr self, [MarshalAs( UnmanagedType.U1 )] bool bWaitForever, uint unTimeout ); + + #endregion + internal bool BWaitForData( [MarshalAs( UnmanagedType.U1 )] bool bWaitForever, uint unTimeout ) + { + var returnValue = _BWaitForData( Self, bWaitForever, unTimeout ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_BNewDataAvailable", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _BNewDataAvailable( IntPtr self ); + + #endregion + internal bool BNewDataAvailable() + { + var returnValue = _BNewDataAvailable( Self ); + return returnValue; } #region FunctionMeta @@ -65,6 +101,16 @@ namespace Steamworks return returnValue; } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_EnableDeviceCallbacks", CallingConvention = Platform.CC)] + private static extern void _EnableDeviceCallbacks( IntPtr self ); + + #endregion + internal void EnableDeviceCallbacks() + { + _EnableDeviceCallbacks( Self ); + } + #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_GetActionSetHandle", CallingConvention = Platform.CC)] private static extern InputActionSetHandle_t _GetActionSetHandle( IntPtr self, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pszActionSetName ); @@ -171,6 +217,17 @@ namespace Steamworks return returnValue; } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_GetStringForDigitalActionName", CallingConvention = Platform.CC)] + private static extern Utf8StringPointer _GetStringForDigitalActionName( IntPtr self, InputDigitalActionHandle_t eActionHandle ); + + #endregion + internal string GetStringForDigitalActionName( InputDigitalActionHandle_t eActionHandle ) + { + var returnValue = _GetStringForDigitalActionName( Self, eActionHandle ); + return returnValue; + } + #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_GetAnalogActionHandle", CallingConvention = Platform.CC)] private static extern InputAnalogActionHandle_t _GetAnalogActionHandle( IntPtr self, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pszActionName ); @@ -205,13 +262,35 @@ namespace Steamworks } #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_GetGlyphForActionOrigin", CallingConvention = Platform.CC)] - private static extern Utf8StringPointer _GetGlyphForActionOrigin( IntPtr self, InputActionOrigin eOrigin ); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_GetGlyphPNGForActionOrigin", CallingConvention = Platform.CC)] + private static extern Utf8StringPointer _GetGlyphPNGForActionOrigin( IntPtr self, InputActionOrigin eOrigin, GlyphSize eSize, uint unFlags ); #endregion - internal string GetGlyphForActionOrigin( InputActionOrigin eOrigin ) + internal string GetGlyphPNGForActionOrigin( InputActionOrigin eOrigin, GlyphSize eSize, uint unFlags ) { - var returnValue = _GetGlyphForActionOrigin( Self, eOrigin ); + var returnValue = _GetGlyphPNGForActionOrigin( Self, eOrigin, eSize, unFlags ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_GetGlyphSVGForActionOrigin", CallingConvention = Platform.CC)] + private static extern Utf8StringPointer _GetGlyphSVGForActionOrigin( IntPtr self, InputActionOrigin eOrigin, uint unFlags ); + + #endregion + internal string GetGlyphSVGForActionOrigin( InputActionOrigin eOrigin, uint unFlags ) + { + var returnValue = _GetGlyphSVGForActionOrigin( Self, eOrigin, unFlags ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_GetGlyphForActionOrigin_Legacy", CallingConvention = Platform.CC)] + private static extern Utf8StringPointer _GetGlyphForActionOrigin_Legacy( IntPtr self, InputActionOrigin eOrigin ); + + #endregion + internal string GetGlyphForActionOrigin_Legacy( InputActionOrigin eOrigin ) + { + var returnValue = _GetGlyphForActionOrigin_Legacy( Self, eOrigin ); return returnValue; } @@ -226,6 +305,17 @@ namespace Steamworks return returnValue; } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_GetStringForAnalogActionName", CallingConvention = Platform.CC)] + private static extern Utf8StringPointer _GetStringForAnalogActionName( IntPtr self, InputAnalogActionHandle_t eActionHandle ); + + #endregion + internal string GetStringForAnalogActionName( InputAnalogActionHandle_t eActionHandle ) + { + var returnValue = _GetStringForAnalogActionName( Self, eActionHandle ); + return returnValue; + } + #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_StopAnalogActionMomentum", CallingConvention = Platform.CC)] private static extern void _StopAnalogActionMomentum( IntPtr self, InputHandle_t inputHandle, InputAnalogActionHandle_t eAction ); @@ -257,6 +347,26 @@ namespace Steamworks _TriggerVibration( Self, inputHandle, usLeftSpeed, usRightSpeed ); } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_TriggerVibrationExtended", CallingConvention = Platform.CC)] + private static extern void _TriggerVibrationExtended( IntPtr self, InputHandle_t inputHandle, ushort usLeftSpeed, ushort usRightSpeed, ushort usLeftTriggerSpeed, ushort usRightTriggerSpeed ); + + #endregion + internal void TriggerVibrationExtended( InputHandle_t inputHandle, ushort usLeftSpeed, ushort usRightSpeed, ushort usLeftTriggerSpeed, ushort usRightTriggerSpeed ) + { + _TriggerVibrationExtended( Self, inputHandle, usLeftSpeed, usRightSpeed, usLeftTriggerSpeed, usRightTriggerSpeed ); + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_TriggerSimpleHapticEvent", CallingConvention = Platform.CC)] + private static extern void _TriggerSimpleHapticEvent( IntPtr self, InputHandle_t inputHandle, ControllerHapticLocation eHapticLocation, byte nIntensity, char nGainDB, byte nOtherIntensity, char nOtherGainDB ); + + #endregion + internal void TriggerSimpleHapticEvent( InputHandle_t inputHandle, ControllerHapticLocation eHapticLocation, byte nIntensity, char nGainDB, byte nOtherIntensity, char nOtherGainDB ) + { + _TriggerSimpleHapticEvent( Self, inputHandle, eHapticLocation, nIntensity, nGainDB, nOtherIntensity, nOtherGainDB ); + } + #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_SetLEDColor", CallingConvention = Platform.CC)] private static extern void _SetLEDColor( IntPtr self, InputHandle_t inputHandle, byte nColorR, byte nColorG, byte nColorB, uint nFlags ); @@ -268,23 +378,23 @@ namespace Steamworks } #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_TriggerHapticPulse", CallingConvention = Platform.CC)] - private static extern void _TriggerHapticPulse( IntPtr self, InputHandle_t inputHandle, SteamControllerPad eTargetPad, ushort usDurationMicroSec ); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_Legacy_TriggerHapticPulse", CallingConvention = Platform.CC)] + private static extern void _Legacy_TriggerHapticPulse( IntPtr self, InputHandle_t inputHandle, SteamControllerPad eTargetPad, ushort usDurationMicroSec ); #endregion - internal void TriggerHapticPulse( InputHandle_t inputHandle, SteamControllerPad eTargetPad, ushort usDurationMicroSec ) + internal void Legacy_TriggerHapticPulse( InputHandle_t inputHandle, SteamControllerPad eTargetPad, ushort usDurationMicroSec ) { - _TriggerHapticPulse( Self, inputHandle, eTargetPad, usDurationMicroSec ); + _Legacy_TriggerHapticPulse( Self, inputHandle, eTargetPad, usDurationMicroSec ); } #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_TriggerRepeatedHapticPulse", CallingConvention = Platform.CC)] - private static extern void _TriggerRepeatedHapticPulse( IntPtr self, InputHandle_t inputHandle, SteamControllerPad eTargetPad, ushort usDurationMicroSec, ushort usOffMicroSec, ushort unRepeat, uint nFlags ); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_Legacy_TriggerRepeatedHapticPulse", CallingConvention = Platform.CC)] + private static extern void _Legacy_TriggerRepeatedHapticPulse( IntPtr self, InputHandle_t inputHandle, SteamControllerPad eTargetPad, ushort usDurationMicroSec, ushort usOffMicroSec, ushort unRepeat, uint nFlags ); #endregion - internal void TriggerRepeatedHapticPulse( InputHandle_t inputHandle, SteamControllerPad eTargetPad, ushort usDurationMicroSec, ushort usOffMicroSec, ushort unRepeat, uint nFlags ) + internal void Legacy_TriggerRepeatedHapticPulse( InputHandle_t inputHandle, SteamControllerPad eTargetPad, ushort usDurationMicroSec, ushort usOffMicroSec, ushort unRepeat, uint nFlags ) { - _TriggerRepeatedHapticPulse( Self, inputHandle, eTargetPad, usDurationMicroSec, usOffMicroSec, unRepeat, nFlags ); + _Legacy_TriggerRepeatedHapticPulse( Self, inputHandle, eTargetPad, usDurationMicroSec, usOffMicroSec, unRepeat, nFlags ); } #region FunctionMeta @@ -399,5 +509,16 @@ namespace Steamworks return returnValue; } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInput_GetSessionInputConfigurationSettings", CallingConvention = Platform.CC)] + private static extern ushort _GetSessionInputConfigurationSettings( IntPtr self ); + + #endregion + internal ushort GetSessionInputConfigurationSettings() + { + var returnValue = _GetSessionInputConfigurationSettings( Self ); + return returnValue; + } + } } diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamInventory.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamInventory.cs index b8c24ac7d..5b4d20160 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamInventory.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamInventory.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamInventory : SteamInterface + internal unsafe class ISteamInventory : SteamInterface { internal ISteamInventory( bool IsGameServer ) @@ -28,9 +28,6 @@ namespace Steamworks private static extern Result _GetResultStatus( IntPtr self, SteamInventoryResult_t resultHandle ); #endregion - /// - /// Find out the status of an asynchronous inventory result handle. - /// internal Result GetResultStatus( SteamInventoryResult_t resultHandle ) { var returnValue = _GetResultStatus( Self, resultHandle ); @@ -43,9 +40,6 @@ namespace Steamworks private static extern bool _GetResultItems( IntPtr self, SteamInventoryResult_t resultHandle, [In,Out] SteamItemDetails_t[]? pOutItemsArray, ref uint punOutItemsArraySize ); #endregion - /// - /// Copies the contents of a result set into a flat array. The specific contents of the result set depend on which query which was used. - /// internal bool GetResultItems( SteamInventoryResult_t resultHandle, [In,Out] SteamItemDetails_t[]? pOutItemsArray, ref uint punOutItemsArraySize ) { var returnValue = _GetResultItems( Self, resultHandle, pOutItemsArray, ref punOutItemsArraySize ); @@ -60,8 +54,7 @@ namespace Steamworks #endregion internal bool GetResultItemProperty( SteamInventoryResult_t resultHandle, uint unItemIndex, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string? pchPropertyName, out string pchValueBuffer, ref uint punValueBufferSizeOut ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchValueBuffer = memory; + using var mempchValueBuffer = Helpers.TakeMemory(); var returnValue = _GetResultItemProperty( Self, resultHandle, unItemIndex, pchPropertyName, mempchValueBuffer, ref punValueBufferSizeOut ); pchValueBuffer = Helpers.MemoryToString( mempchValueBuffer ); return returnValue; @@ -72,9 +65,6 @@ namespace Steamworks private static extern uint _GetResultTimestamp( IntPtr self, SteamInventoryResult_t resultHandle ); #endregion - /// - /// Returns the server time at which the result was generated. Compare against the value of IClientUtils::GetServerRealTime() to determine age. - /// internal uint GetResultTimestamp( SteamInventoryResult_t resultHandle ) { var returnValue = _GetResultTimestamp( Self, resultHandle ); @@ -87,9 +77,6 @@ namespace Steamworks private static extern bool _CheckResultSteamID( IntPtr self, SteamInventoryResult_t resultHandle, SteamId steamIDExpected ); #endregion - /// - /// Returns true if the result belongs to the target steam ID or false if the result does not. This is important when using DeserializeResult to verify that a remote player is not pretending to have a different users inventory. - /// internal bool CheckResultSteamID( SteamInventoryResult_t resultHandle, SteamId steamIDExpected ) { var returnValue = _CheckResultSteamID( Self, resultHandle, steamIDExpected ); @@ -101,9 +88,6 @@ namespace Steamworks private static extern void _DestroyResult( IntPtr self, SteamInventoryResult_t resultHandle ); #endregion - /// - /// Destroys a result handle and frees all associated memory. - /// internal void DestroyResult( SteamInventoryResult_t resultHandle ) { _DestroyResult( Self, resultHandle ); @@ -115,9 +99,6 @@ namespace Steamworks private static extern bool _GetAllItems( IntPtr self, ref SteamInventoryResult_t pResultHandle ); #endregion - /// - /// Captures the entire state of the current users Steam inventory. - /// internal bool GetAllItems( ref SteamInventoryResult_t pResultHandle ) { var returnValue = _GetAllItems( Self, ref pResultHandle ); @@ -130,9 +111,6 @@ namespace Steamworks private static extern bool _GetItemsByID( IntPtr self, ref SteamInventoryResult_t pResultHandle, ref InventoryItemId pInstanceIDs, uint unCountInstanceIDs ); #endregion - /// - /// Captures the state of a subset of the current users Steam inventory identified by an array of item instance IDs. - /// internal bool GetItemsByID( ref SteamInventoryResult_t pResultHandle, ref InventoryItemId pInstanceIDs, uint unCountInstanceIDs ) { var returnValue = _GetItemsByID( Self, ref pResultHandle, ref pInstanceIDs, unCountInstanceIDs ); @@ -181,9 +159,6 @@ namespace Steamworks private static extern bool _GrantPromoItems( IntPtr self, ref SteamInventoryResult_t pResultHandle ); #endregion - /// - /// GrantPromoItems() checks the list of promotional items for which the user may be eligible and grants the items (one time only). - /// internal bool GrantPromoItems( ref SteamInventoryResult_t pResultHandle ) { var returnValue = _GrantPromoItems( Self, ref pResultHandle ); @@ -220,9 +195,6 @@ namespace Steamworks private static extern bool _ConsumeItem( IntPtr self, ref SteamInventoryResult_t pResultHandle, InventoryItemId itemConsume, uint unQuantity ); #endregion - /// - /// ConsumeItem() removes items from the inventory permanently. - /// internal bool ConsumeItem( ref SteamInventoryResult_t pResultHandle, InventoryItemId itemConsume, uint unQuantity ) { var returnValue = _ConsumeItem( Self, ref pResultHandle, itemConsume, unQuantity ); @@ -258,9 +230,6 @@ namespace Steamworks private static extern void _SendItemDropHeartbeat( IntPtr self ); #endregion - /// - /// Deprecated method. Playtime accounting is performed on the Steam servers. - /// internal void SendItemDropHeartbeat() { _SendItemDropHeartbeat( Self ); @@ -272,9 +241,6 @@ namespace Steamworks private static extern bool _TriggerItemDrop( IntPtr self, ref SteamInventoryResult_t pResultHandle, InventoryDefId dropListDefinition ); #endregion - /// - /// Playtime credit must be consumed and turned into item drops by your game. - /// internal bool TriggerItemDrop( ref SteamInventoryResult_t pResultHandle, InventoryDefId dropListDefinition ) { var returnValue = _TriggerItemDrop( Self, ref pResultHandle, dropListDefinition ); @@ -299,9 +265,6 @@ namespace Steamworks private static extern bool _LoadItemDefinitions( IntPtr self ); #endregion - /// - /// LoadItemDefinitions triggers the automatic load and refresh of item definitions. - /// internal bool LoadItemDefinitions() { var returnValue = _LoadItemDefinitions( Self ); @@ -328,8 +291,7 @@ namespace Steamworks #endregion internal bool GetItemDefinitionProperty( InventoryDefId iDefinition, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string? pchPropertyName, out string pchValueBuffer, ref uint punValueBufferSizeOut ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchValueBuffer = memory; + using var mempchValueBuffer = Helpers.TakeMemory(); var returnValue = _GetItemDefinitionProperty( Self, iDefinition, pchPropertyName, mempchValueBuffer, ref punValueBufferSizeOut ); pchValueBuffer = Helpers.MemoryToString( mempchValueBuffer ); return returnValue; @@ -498,5 +460,17 @@ namespace Steamworks return returnValue; } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInventory_InspectItem", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _InspectItem( IntPtr self, ref SteamInventoryResult_t pResultHandle, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchItemToken ); + + #endregion + internal bool InspectItem( ref SteamInventoryResult_t pResultHandle, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchItemToken ) + { + var returnValue = _InspectItem( Self, ref pResultHandle, pchItemToken ); + return returnValue; + } + } } diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmaking.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmaking.cs index 593676092..801d41086 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmaking.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmaking.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamMatchmaking : SteamInterface + internal unsafe class ISteamMatchmaking : SteamInterface { internal ISteamMatchmaking( bool IsGameServer ) @@ -266,10 +266,8 @@ namespace Steamworks #endregion internal bool GetLobbyDataByIndex( SteamId steamIDLobby, int iLobbyData, out string pchKey, out string pchValue ) { - using var memoryKey = Helpers.TakeMemory(); - using var memoryValue = Helpers.TakeMemory(); - IntPtr mempchKey = memoryKey; - IntPtr mempchValue = memoryValue; + using var mempchKey = Helpers.TakeMemory(); + using var mempchValue = Helpers.TakeMemory(); var returnValue = _GetLobbyDataByIndex( Self, steamIDLobby, iLobbyData, mempchKey, (1024 * 32), mempchValue, (1024 * 32) ); pchKey = Helpers.MemoryToString( mempchKey ); pchValue = Helpers.MemoryToString( mempchValue ); diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingPingResponse.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingPingResponse.cs index 9f14a9618..0306b76f6 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingPingResponse.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingPingResponse.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamMatchmakingPingResponse : SteamInterface + internal unsafe class ISteamMatchmakingPingResponse : SteamInterface { internal ISteamMatchmakingPingResponse( bool IsGameServer ) diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingPlayersResponse.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingPlayersResponse.cs index ce4f02aee..a5ed92691 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingPlayersResponse.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingPlayersResponse.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamMatchmakingPlayersResponse : SteamInterface + internal unsafe class ISteamMatchmakingPlayersResponse : SteamInterface { internal ISteamMatchmakingPlayersResponse( bool IsGameServer ) diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingRulesResponse.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingRulesResponse.cs index 7367634d1..78507b9ea 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingRulesResponse.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingRulesResponse.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamMatchmakingRulesResponse : SteamInterface + internal unsafe class ISteamMatchmakingRulesResponse : SteamInterface { internal ISteamMatchmakingRulesResponse( bool IsGameServer ) diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingServerListResponse.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingServerListResponse.cs index 9ab74dc67..8fd0a9e98 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingServerListResponse.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingServerListResponse.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamMatchmakingServerListResponse : SteamInterface + internal unsafe class ISteamMatchmakingServerListResponse : SteamInterface { internal ISteamMatchmakingServerListResponse( bool IsGameServer ) diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingServers.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingServers.cs index bd7bb81ba..3eb098c00 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingServers.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmakingServers.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamMatchmakingServers : SteamInterface + internal unsafe class ISteamMatchmakingServers : SteamInterface { internal ISteamMatchmakingServers( bool IsGameServer ) diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMusic.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMusic.cs index 1ed2f6707..6534a83c0 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMusic.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMusic.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamMusic : SteamInterface + internal unsafe class ISteamMusic : SteamInterface { internal ISteamMusic( bool IsGameServer ) diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMusicRemote.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMusicRemote.cs index 620b7c69f..cf7350288 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMusicRemote.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMusicRemote.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamMusicRemote : SteamInterface + internal unsafe class ISteamMusicRemote : SteamInterface { internal ISteamMusicRemote( bool IsGameServer ) diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworking.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworking.cs index baef24dd3..f14955788 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworking.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworking.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamNetworking : SteamInterface + internal unsafe class ISteamNetworking : SteamInterface { internal ISteamNetworking( bool IsGameServer ) diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingConnectionCustomSignaling.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingConnectionCustomSignaling.cs deleted file mode 100644 index 3379820af..000000000 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingConnectionCustomSignaling.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; -using Steamworks.Data; - - -namespace Steamworks -{ - internal class ISteamNetworkingConnectionCustomSignaling : SteamInterface - { - - internal ISteamNetworkingConnectionCustomSignaling( bool IsGameServer ) - { - SetupInterface( IsGameServer ); - } - - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingConnectionCustomSignaling_SendSignal", CallingConvention = Platform.CC)] - [return: MarshalAs( UnmanagedType.I1 )] - private static extern bool _SendSignal( IntPtr self, Connection hConn, ref ConnectionInfo info, IntPtr pMsg, int cbMsg ); - - #endregion - internal bool SendSignal( Connection hConn, ref ConnectionInfo info, IntPtr pMsg, int cbMsg ) - { - var returnValue = _SendSignal( Self, hConn, ref info, pMsg, cbMsg ); - return returnValue; - } - - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingConnectionCustomSignaling_Release", CallingConvention = Platform.CC)] - private static extern void _Release( IntPtr self ); - - #endregion - internal void Release() - { - _Release( Self ); - } - - } -} diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingCustomSignalingRecvContext.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingCustomSignalingRecvContext.cs deleted file mode 100644 index 670919571..000000000 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingCustomSignalingRecvContext.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; -using Steamworks.Data; - - -namespace Steamworks -{ - internal class ISteamNetworkingCustomSignalingRecvContext : SteamInterface - { - - internal ISteamNetworkingCustomSignalingRecvContext( bool IsGameServer ) - { - SetupInterface( IsGameServer ); - } - - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingCustomSignalingRecvContext_OnConnectRequest", CallingConvention = Platform.CC)] - private static extern IntPtr _OnConnectRequest( IntPtr self, Connection hConn, ref NetIdentity identityPeer ); - - #endregion - internal IntPtr OnConnectRequest( Connection hConn, ref NetIdentity identityPeer ) - { - var returnValue = _OnConnectRequest( Self, hConn, ref identityPeer ); - return returnValue; - } - - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingCustomSignalingRecvContext_SendRejectionSignal", CallingConvention = Platform.CC)] - private static extern void _SendRejectionSignal( IntPtr self, ref NetIdentity identityPeer, IntPtr pMsg, int cbMsg ); - - #endregion - internal void SendRejectionSignal( ref NetIdentity identityPeer, IntPtr pMsg, int cbMsg ) - { - _SendRejectionSignal( Self, ref identityPeer, pMsg, cbMsg ); - } - - } -} diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingFakeUDPPort.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingFakeUDPPort.cs new file mode 100644 index 000000000..776ab8a68 --- /dev/null +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingFakeUDPPort.cs @@ -0,0 +1,61 @@ +using System; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using Steamworks.Data; + + +namespace Steamworks +{ + internal unsafe class ISteamNetworkingFakeUDPPort : SteamInterface + { + + internal ISteamNetworkingFakeUDPPort( bool IsGameServer ) + { + SetupInterface( IsGameServer ); + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingFakeUDPPort_DestroyFakeUDPPort", CallingConvention = Platform.CC)] + private static extern void _DestroyFakeUDPPort( IntPtr self ); + + #endregion + internal void DestroyFakeUDPPort() + { + _DestroyFakeUDPPort( Self ); + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingFakeUDPPort_SendMessageToFakeIP", CallingConvention = Platform.CC)] + private static extern Result _SendMessageToFakeIP( IntPtr self, ref NetAddress remoteAddress, IntPtr pData, uint cbData, int nSendFlags ); + + #endregion + internal Result SendMessageToFakeIP( ref NetAddress remoteAddress, IntPtr pData, uint cbData, int nSendFlags ) + { + var returnValue = _SendMessageToFakeIP( Self, ref remoteAddress, pData, cbData, nSendFlags ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingFakeUDPPort_ReceiveMessages", CallingConvention = Platform.CC)] + private static extern int _ReceiveMessages( IntPtr self, IntPtr ppOutMessages, int nMaxMessages ); + + #endregion + internal int ReceiveMessages( IntPtr ppOutMessages, int nMaxMessages ) + { + var returnValue = _ReceiveMessages( Self, ppOutMessages, nMaxMessages ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingFakeUDPPort_ScheduleCleanup", CallingConvention = Platform.CC)] + private static extern void _ScheduleCleanup( IntPtr self, ref NetAddress remoteAddress ); + + #endregion + internal void ScheduleCleanup( ref NetAddress remoteAddress ) + { + _ScheduleCleanup( Self, ref remoteAddress ); + } + + } +} diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingMessages.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingMessages.cs new file mode 100644 index 000000000..715c34a13 --- /dev/null +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingMessages.cs @@ -0,0 +1,96 @@ +using System; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using Steamworks.Data; + + +namespace Steamworks +{ + internal unsafe class ISteamNetworkingMessages : SteamInterface + { + + internal ISteamNetworkingMessages( bool IsGameServer ) + { + SetupInterface( IsGameServer ); + } + + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingMessages_SteamAPI_v002", CallingConvention = Platform.CC)] + internal static extern IntPtr SteamAPI_SteamNetworkingMessages_SteamAPI_v002(); + public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamNetworkingMessages_SteamAPI_v002(); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamGameServerNetworkingMessages_SteamAPI_v002", CallingConvention = Platform.CC)] + internal static extern IntPtr SteamAPI_SteamGameServerNetworkingMessages_SteamAPI_v002(); + public override IntPtr GetServerInterfacePointer() => SteamAPI_SteamGameServerNetworkingMessages_SteamAPI_v002(); + + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingMessages_SendMessageToUser", CallingConvention = Platform.CC)] + private static extern Result _SendMessageToUser( IntPtr self, ref NetIdentity identityRemote, [In,Out] IntPtr[] pubData, uint cubData, int nSendFlags, int nRemoteChannel ); + + #endregion + internal Result SendMessageToUser( ref NetIdentity identityRemote, [In,Out] IntPtr[] pubData, uint cubData, int nSendFlags, int nRemoteChannel ) + { + var returnValue = _SendMessageToUser( Self, ref identityRemote, pubData, cubData, nSendFlags, nRemoteChannel ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingMessages_ReceiveMessagesOnChannel", CallingConvention = Platform.CC)] + private static extern int _ReceiveMessagesOnChannel( IntPtr self, int nLocalChannel, IntPtr ppOutMessages, int nMaxMessages ); + + #endregion + internal int ReceiveMessagesOnChannel( int nLocalChannel, IntPtr ppOutMessages, int nMaxMessages ) + { + var returnValue = _ReceiveMessagesOnChannel( Self, nLocalChannel, ppOutMessages, nMaxMessages ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingMessages_AcceptSessionWithUser", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _AcceptSessionWithUser( IntPtr self, ref NetIdentity identityRemote ); + + #endregion + internal bool AcceptSessionWithUser( ref NetIdentity identityRemote ) + { + var returnValue = _AcceptSessionWithUser( Self, ref identityRemote ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingMessages_CloseSessionWithUser", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _CloseSessionWithUser( IntPtr self, ref NetIdentity identityRemote ); + + #endregion + internal bool CloseSessionWithUser( ref NetIdentity identityRemote ) + { + var returnValue = _CloseSessionWithUser( Self, ref identityRemote ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingMessages_CloseChannelWithUser", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _CloseChannelWithUser( IntPtr self, ref NetIdentity identityRemote, int nLocalChannel ); + + #endregion + internal bool CloseChannelWithUser( ref NetIdentity identityRemote, int nLocalChannel ) + { + var returnValue = _CloseChannelWithUser( Self, ref identityRemote, nLocalChannel ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingMessages_GetSessionConnectionInfo", CallingConvention = Platform.CC)] + private static extern ConnectionState _GetSessionConnectionInfo( IntPtr self, ref NetIdentity identityRemote, ref ConnectionInfo pConnectionInfo, ref ConnectionStatus pQuickStatus ); + + #endregion + internal ConnectionState GetSessionConnectionInfo( ref NetIdentity identityRemote, ref ConnectionInfo pConnectionInfo, ref ConnectionStatus pQuickStatus ) + { + var returnValue = _GetSessionConnectionInfo( Self, ref identityRemote, ref pConnectionInfo, ref pQuickStatus ); + return returnValue; + } + + } +} diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingSockets.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingSockets.cs index 61190cd7b..becc5d302 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingSockets.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingSockets.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamNetworkingSockets : SteamInterface + internal unsafe class ISteamNetworkingSockets : SteamInterface { internal ISteamNetworkingSockets( bool IsGameServer ) @@ -15,12 +15,12 @@ namespace Steamworks SetupInterface( IsGameServer ); } - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingSockets_v008", CallingConvention = Platform.CC)] - internal static extern IntPtr SteamAPI_SteamNetworkingSockets_v008(); - public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamNetworkingSockets_v008(); - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamGameServerNetworkingSockets_v008", CallingConvention = Platform.CC)] - internal static extern IntPtr SteamAPI_SteamGameServerNetworkingSockets_v008(); - public override IntPtr GetServerInterfacePointer() => SteamAPI_SteamGameServerNetworkingSockets_v008(); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingSockets_SteamAPI_v012", CallingConvention = Platform.CC)] + internal static extern IntPtr SteamAPI_SteamNetworkingSockets_SteamAPI_v012(); + public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamNetworkingSockets_SteamAPI_v012(); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamGameServerNetworkingSockets_SteamAPI_v012", CallingConvention = Platform.CC)] + internal static extern IntPtr SteamAPI_SteamGameServerNetworkingSockets_SteamAPI_v012(); + public override IntPtr GetServerInterfacePointer() => SteamAPI_SteamGameServerNetworkingSockets_SteamAPI_v012(); #region FunctionMeta @@ -47,23 +47,23 @@ namespace Steamworks #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_CreateListenSocketP2P", CallingConvention = Platform.CC)] - private static extern Socket _CreateListenSocketP2P( IntPtr self, int nVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ); + private static extern Socket _CreateListenSocketP2P( IntPtr self, int nLocalVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ); #endregion - internal Socket CreateListenSocketP2P( int nVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ) + internal Socket CreateListenSocketP2P( int nLocalVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ) { - var returnValue = _CreateListenSocketP2P( Self, nVirtualPort, nOptions, pOptions ); + var returnValue = _CreateListenSocketP2P( Self, nLocalVirtualPort, nOptions, pOptions ); return returnValue; } #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_ConnectP2P", CallingConvention = Platform.CC)] - private static extern Connection _ConnectP2P( IntPtr self, ref NetIdentity identityRemote, int nVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ); + private static extern Connection _ConnectP2P( IntPtr self, ref NetIdentity identityRemote, int nRemoteVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ); #endregion - internal Connection ConnectP2P( ref NetIdentity identityRemote, int nVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ) + internal Connection ConnectP2P( ref NetIdentity identityRemote, int nRemoteVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ) { - var returnValue = _ConnectP2P( Self, ref identityRemote, nVirtualPort, nOptions, pOptions ); + var returnValue = _ConnectP2P( Self, ref identityRemote, nRemoteVirtualPort, nOptions, pOptions ); return returnValue; } @@ -143,8 +143,7 @@ namespace Steamworks #endregion internal bool GetConnectionName( Connection hPeer, out string pszName ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempszName = memory; + using var mempszName = Helpers.TakeMemory(); var returnValue = _GetConnectionName( Self, hPeer, mempszName, (1024 * 32) ); pszName = Helpers.MemoryToString( mempszName ); return returnValue; @@ -163,12 +162,12 @@ namespace Steamworks #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_SendMessages", CallingConvention = Platform.CC)] - private static extern void _SendMessages( IntPtr self, int nMessages, ref NetMsg pMessages, [In,Out] long[] pOutMessageNumberOrResult ); + private static extern void _SendMessages( IntPtr self, int nMessages, NetMsg** pMessages, long* pOutMessageNumberOrResult ); #endregion - internal void SendMessages( int nMessages, ref NetMsg pMessages, [In,Out] long[] pOutMessageNumberOrResult ) + internal void SendMessages( int nMessages, NetMsg** pMessages, long* pOutMessageNumberOrResult ) { - _SendMessages( Self, nMessages, ref pMessages, pOutMessageNumberOrResult ); + _SendMessages( Self, nMessages, pMessages, pOutMessageNumberOrResult ); } #region FunctionMeta @@ -206,14 +205,13 @@ namespace Steamworks } #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_GetQuickConnectionStatus", CallingConvention = Platform.CC)] - [return: MarshalAs( UnmanagedType.I1 )] - private static extern bool _GetQuickConnectionStatus( IntPtr self, Connection hConn, ref SteamNetworkingQuickConnectionStatus pStats ); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_GetConnectionRealTimeStatus", CallingConvention = Platform.CC)] + private static extern Result _GetConnectionRealTimeStatus( IntPtr self, Connection hConn, ref ConnectionStatus pStatus, int nLanes, [In,Out] ConnectionLaneStatus[]? pLanes ); #endregion - internal bool GetQuickConnectionStatus( Connection hConn, ref SteamNetworkingQuickConnectionStatus pStats ) + internal Result GetConnectionRealTimeStatus( Connection hConn, ref ConnectionStatus pStatus, int nLanes, [In,Out] ConnectionLaneStatus[]? pLanes ) { - var returnValue = _GetQuickConnectionStatus( Self, hConn, ref pStats ); + var returnValue = _GetConnectionRealTimeStatus( Self, hConn, ref pStatus, nLanes, pLanes ); return returnValue; } @@ -224,8 +222,7 @@ namespace Steamworks #endregion internal int GetDetailedConnectionStatus( Connection hConn, out string pszBuf ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempszBuf = memory; + using var mempszBuf = Helpers.TakeMemory(); var returnValue = _GetDetailedConnectionStatus( Self, hConn, mempszBuf, (1024 * 32) ); pszBuf = Helpers.MemoryToString( mempszBuf ); return returnValue; @@ -255,6 +252,17 @@ namespace Steamworks return returnValue; } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_ConfigureConnectionLanes", CallingConvention = Platform.CC)] + private static extern Result _ConfigureConnectionLanes( IntPtr self, Connection hConn, int nNumLanes, [In,Out] int[] pLanePriorities, [In,Out] ushort[] pLaneWeights ); + + #endregion + internal Result ConfigureConnectionLanes( Connection hConn, int nNumLanes, [In,Out] int[] pLanePriorities, [In,Out] ushort[] pLaneWeights ) + { + var returnValue = _ConfigureConnectionLanes( Self, hConn, nNumLanes, pLanePriorities, pLaneWeights ); + return returnValue; + } + #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_GetIdentity", CallingConvention = Platform.CC)] [return: MarshalAs( UnmanagedType.I1 )] @@ -349,23 +357,23 @@ namespace Steamworks #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_FindRelayAuthTicketForServer", CallingConvention = Platform.CC)] - private static extern int _FindRelayAuthTicketForServer( IntPtr self, ref NetIdentity identityGameServer, int nVirtualPort, [In,Out] SteamDatagramRelayAuthTicket[] pOutParsedTicket ); + private static extern int _FindRelayAuthTicketForServer( IntPtr self, ref NetIdentity identityGameServer, int nRemoteVirtualPort, [In,Out] SteamDatagramRelayAuthTicket[] pOutParsedTicket ); #endregion - internal int FindRelayAuthTicketForServer( ref NetIdentity identityGameServer, int nVirtualPort, [In,Out] SteamDatagramRelayAuthTicket[] pOutParsedTicket ) + internal int FindRelayAuthTicketForServer( ref NetIdentity identityGameServer, int nRemoteVirtualPort, [In,Out] SteamDatagramRelayAuthTicket[] pOutParsedTicket ) { - var returnValue = _FindRelayAuthTicketForServer( Self, ref identityGameServer, nVirtualPort, pOutParsedTicket ); + var returnValue = _FindRelayAuthTicketForServer( Self, ref identityGameServer, nRemoteVirtualPort, pOutParsedTicket ); return returnValue; } #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_ConnectToHostedDedicatedServer", CallingConvention = Platform.CC)] - private static extern Connection _ConnectToHostedDedicatedServer( IntPtr self, ref NetIdentity identityTarget, int nVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ); + private static extern Connection _ConnectToHostedDedicatedServer( IntPtr self, ref NetIdentity identityTarget, int nRemoteVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ); #endregion - internal Connection ConnectToHostedDedicatedServer( ref NetIdentity identityTarget, int nVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ) + internal Connection ConnectToHostedDedicatedServer( ref NetIdentity identityTarget, int nRemoteVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ) { - var returnValue = _ConnectToHostedDedicatedServer( Self, ref identityTarget, nVirtualPort, nOptions, pOptions ); + var returnValue = _ConnectToHostedDedicatedServer( Self, ref identityTarget, nRemoteVirtualPort, nOptions, pOptions ); return returnValue; } @@ -404,12 +412,12 @@ namespace Steamworks #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_CreateHostedDedicatedServerListenSocket", CallingConvention = Platform.CC)] - private static extern Socket _CreateHostedDedicatedServerListenSocket( IntPtr self, int nVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ); + private static extern Socket _CreateHostedDedicatedServerListenSocket( IntPtr self, int nLocalVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ); #endregion - internal Socket CreateHostedDedicatedServerListenSocket( int nVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ) + internal Socket CreateHostedDedicatedServerListenSocket( int nLocalVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ) { - var returnValue = _CreateHostedDedicatedServerListenSocket( Self, nVirtualPort, nOptions, pOptions ); + var returnValue = _CreateHostedDedicatedServerListenSocket( Self, nLocalVirtualPort, nOptions, pOptions ); return returnValue; } @@ -426,12 +434,12 @@ namespace Steamworks #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_ConnectP2PCustomSignaling", CallingConvention = Platform.CC)] - private static extern Connection _ConnectP2PCustomSignaling( IntPtr self, IntPtr pSignaling, ref NetIdentity pPeerIdentity, int nOptions, [In,Out] NetKeyValue[] pOptions ); + private static extern Connection _ConnectP2PCustomSignaling( IntPtr self, IntPtr pSignaling, ref NetIdentity pPeerIdentity, int nRemoteVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ); #endregion - internal Connection ConnectP2PCustomSignaling( IntPtr pSignaling, ref NetIdentity pPeerIdentity, int nOptions, [In,Out] NetKeyValue[] pOptions ) + internal Connection ConnectP2PCustomSignaling( IntPtr pSignaling, ref NetIdentity pPeerIdentity, int nRemoteVirtualPort, int nOptions, [In,Out] NetKeyValue[] pOptions ) { - var returnValue = _ConnectP2PCustomSignaling( Self, pSignaling, ref pPeerIdentity, nOptions, pOptions ); + var returnValue = _ConnectP2PCustomSignaling( Self, pSignaling, ref pPeerIdentity, nRemoteVirtualPort, nOptions, pOptions ); return returnValue; } @@ -471,5 +479,80 @@ namespace Steamworks return returnValue; } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_ResetIdentity", CallingConvention = Platform.CC)] + private static extern void _ResetIdentity( IntPtr self, ref NetIdentity pIdentity ); + + #endregion + internal void ResetIdentity( ref NetIdentity pIdentity ) + { + _ResetIdentity( Self, ref pIdentity ); + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_RunCallbacks", CallingConvention = Platform.CC)] + private static extern void _RunCallbacks( IntPtr self ); + + #endregion + internal void RunCallbacks() + { + _RunCallbacks( Self ); + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_BeginAsyncRequestFakeIP", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _BeginAsyncRequestFakeIP( IntPtr self, int nNumPorts ); + + #endregion + internal bool BeginAsyncRequestFakeIP( int nNumPorts ) + { + var returnValue = _BeginAsyncRequestFakeIP( Self, nNumPorts ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_GetFakeIP", CallingConvention = Platform.CC)] + private static extern void _GetFakeIP( IntPtr self, int idxFirstPort, ref SteamNetworkingFakeIPResult_t pInfo ); + + #endregion + internal void GetFakeIP( int idxFirstPort, ref SteamNetworkingFakeIPResult_t pInfo ) + { + _GetFakeIP( Self, idxFirstPort, ref pInfo ); + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_CreateListenSocketP2PFakeIP", CallingConvention = Platform.CC)] + private static extern Socket _CreateListenSocketP2PFakeIP( IntPtr self, int idxFakePort, int nOptions, [In,Out] NetKeyValue[] pOptions ); + + #endregion + internal Socket CreateListenSocketP2PFakeIP( int idxFakePort, int nOptions, [In,Out] NetKeyValue[] pOptions ) + { + var returnValue = _CreateListenSocketP2PFakeIP( Self, idxFakePort, nOptions, pOptions ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_GetRemoteFakeIPForConnection", CallingConvention = Platform.CC)] + private static extern Result _GetRemoteFakeIPForConnection( IntPtr self, Connection hConn, [In,Out] NetAddress[] pOutAddr ); + + #endregion + internal Result GetRemoteFakeIPForConnection( Connection hConn, [In,Out] NetAddress[] pOutAddr ) + { + var returnValue = _GetRemoteFakeIPForConnection( Self, hConn, pOutAddr ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingSockets_CreateFakeUDPPort", CallingConvention = Platform.CC)] + private static extern IntPtr _CreateFakeUDPPort( IntPtr self, int idxFakeServerPort ); + + #endregion + internal IntPtr CreateFakeUDPPort( int idxFakeServerPort ) + { + var returnValue = _CreateFakeUDPPort( Self, idxFakeServerPort ); + return returnValue; + } + } } diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingUtils.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingUtils.cs index 51739641e..50d6d7d34 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingUtils.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingUtils.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamNetworkingUtils : SteamInterface + internal unsafe class ISteamNetworkingUtils : SteamInterface { internal ISteamNetworkingUtils( bool IsGameServer ) @@ -15,20 +15,20 @@ namespace Steamworks SetupInterface( IsGameServer ); } - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingUtils_v003", CallingConvention = Platform.CC)] - internal static extern IntPtr SteamAPI_SteamNetworkingUtils_v003(); - public override IntPtr GetGlobalInterfacePointer() => SteamAPI_SteamNetworkingUtils_v003(); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingUtils_SteamAPI_v004", CallingConvention = Platform.CC)] + internal static extern IntPtr SteamAPI_SteamNetworkingUtils_SteamAPI_v004(); + public override IntPtr GetGlobalInterfacePointer() => SteamAPI_SteamNetworkingUtils_SteamAPI_v004(); #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_AllocateMessage", CallingConvention = Platform.CC)] - private static extern IntPtr _AllocateMessage( IntPtr self, int cbAllocateBuffer ); + private static extern NetMsg* _AllocateMessage( IntPtr self, int cbAllocateBuffer ); #endregion - internal NetMsg AllocateMessage( int cbAllocateBuffer ) + internal NetMsg* AllocateMessage( int cbAllocateBuffer ) { var returnValue = _AllocateMessage( Self, cbAllocateBuffer ); - return returnValue.ToType(); + return returnValue; } #region FunctionMeta @@ -92,8 +92,7 @@ namespace Steamworks #endregion internal void ConvertPingLocationToString( ref NetPingLocation location, out string pszBuf ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempszBuf = memory; + using var mempszBuf = Helpers.TakeMemory(); _ConvertPingLocationToString( Self, ref location, mempszBuf, (1024 * 32) ); pszBuf = Helpers.MemoryToString( mempszBuf ); } @@ -187,6 +186,40 @@ namespace Steamworks _SetDebugOutputFunction( Self, eDetailLevel, pfnFunc ); } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_IsFakeIPv4", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _IsFakeIPv4( IntPtr self, uint nIPv4 ); + + #endregion + internal bool IsFakeIPv4( uint nIPv4 ) + { + var returnValue = _IsFakeIPv4( Self, nIPv4 ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_GetIPv4FakeIPType", CallingConvention = Platform.CC)] + private static extern SteamNetworkingFakeIPType _GetIPv4FakeIPType( IntPtr self, uint nIPv4 ); + + #endregion + internal SteamNetworkingFakeIPType GetIPv4FakeIPType( uint nIPv4 ) + { + var returnValue = _GetIPv4FakeIPType( Self, nIPv4 ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_GetRealIdentityForFakeIP", CallingConvention = Platform.CC)] + private static extern Result _GetRealIdentityForFakeIP( IntPtr self, ref NetAddress fakeIP, [In,Out] NetIdentity[] pOutRealIdentity ); + + #endregion + internal Result GetRealIdentityForFakeIP( ref NetAddress fakeIP, [In,Out] NetIdentity[] pOutRealIdentity ) + { + var returnValue = _GetRealIdentityForFakeIP( Self, ref fakeIP, pOutRealIdentity ); + return returnValue; + } + #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_SetGlobalConfigValueInt32", CallingConvention = Platform.CC)] [return: MarshalAs( UnmanagedType.I1 )] @@ -223,6 +256,18 @@ namespace Steamworks return returnValue; } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_SetGlobalConfigValuePtr", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _SetGlobalConfigValuePtr( IntPtr self, NetConfig eValue, IntPtr val ); + + #endregion + internal bool SetGlobalConfigValuePtr( NetConfig eValue, IntPtr val ) + { + var returnValue = _SetGlobalConfigValuePtr( Self, eValue, val ); + return returnValue; + } + #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_SetConnectionConfigValueInt32", CallingConvention = Platform.CC)] [return: MarshalAs( UnmanagedType.I1 )] @@ -259,6 +304,78 @@ namespace Steamworks return returnValue; } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_SetGlobalCallback_SteamNetConnectionStatusChanged", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _SetGlobalCallback_SteamNetConnectionStatusChanged( IntPtr self, FnSteamNetConnectionStatusChanged fnCallback ); + + #endregion + internal bool SetGlobalCallback_SteamNetConnectionStatusChanged( FnSteamNetConnectionStatusChanged fnCallback ) + { + var returnValue = _SetGlobalCallback_SteamNetConnectionStatusChanged( Self, fnCallback ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_SetGlobalCallback_SteamNetAuthenticationStatusChanged", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _SetGlobalCallback_SteamNetAuthenticationStatusChanged( IntPtr self, FnSteamNetAuthenticationStatusChanged fnCallback ); + + #endregion + internal bool SetGlobalCallback_SteamNetAuthenticationStatusChanged( FnSteamNetAuthenticationStatusChanged fnCallback ) + { + var returnValue = _SetGlobalCallback_SteamNetAuthenticationStatusChanged( Self, fnCallback ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_SetGlobalCallback_SteamRelayNetworkStatusChanged", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _SetGlobalCallback_SteamRelayNetworkStatusChanged( IntPtr self, FnSteamRelayNetworkStatusChanged fnCallback ); + + #endregion + internal bool SetGlobalCallback_SteamRelayNetworkStatusChanged( FnSteamRelayNetworkStatusChanged fnCallback ) + { + var returnValue = _SetGlobalCallback_SteamRelayNetworkStatusChanged( Self, fnCallback ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_SetGlobalCallback_FakeIPResult", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _SetGlobalCallback_FakeIPResult( IntPtr self, FnSteamNetworkingFakeIPResult fnCallback ); + + #endregion + internal bool SetGlobalCallback_FakeIPResult( FnSteamNetworkingFakeIPResult fnCallback ) + { + var returnValue = _SetGlobalCallback_FakeIPResult( Self, fnCallback ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_SetGlobalCallback_MessagesSessionRequest", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _SetGlobalCallback_MessagesSessionRequest( IntPtr self, FnSteamNetworkingMessagesSessionRequest fnCallback ); + + #endregion + internal bool SetGlobalCallback_MessagesSessionRequest( FnSteamNetworkingMessagesSessionRequest fnCallback ) + { + var returnValue = _SetGlobalCallback_MessagesSessionRequest( Self, fnCallback ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_SetGlobalCallback_MessagesSessionFailed", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _SetGlobalCallback_MessagesSessionFailed( IntPtr self, FnSteamNetworkingMessagesSessionFailed fnCallback ); + + #endregion + internal bool SetGlobalCallback_MessagesSessionFailed( FnSteamNetworkingMessagesSessionFailed fnCallback ) + { + var returnValue = _SetGlobalCallback_MessagesSessionFailed( Self, fnCallback ); + return returnValue; + } + #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_SetConfigValue", CallingConvention = Platform.CC)] [return: MarshalAs( UnmanagedType.I1 )] @@ -296,24 +413,23 @@ namespace Steamworks #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_GetConfigValueInfo", CallingConvention = Platform.CC)] - [return: MarshalAs( UnmanagedType.I1 )] - private static extern bool _GetConfigValueInfo( IntPtr self, NetConfig eValue, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pOutName, ref NetConfigType pOutDataType, [In,Out] NetConfigScope[] pOutScope, [In,Out] NetConfig[] pOutNextValue ); + private static extern Utf8StringPointer _GetConfigValueInfo( IntPtr self, NetConfig eValue, ref NetConfigType pOutDataType, [In,Out] NetConfigScope[] pOutScope ); #endregion - internal bool GetConfigValueInfo( NetConfig eValue, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pOutName, ref NetConfigType pOutDataType, [In,Out] NetConfigScope[] pOutScope, [In,Out] NetConfig[] pOutNextValue ) + internal string GetConfigValueInfo( NetConfig eValue, ref NetConfigType pOutDataType, [In,Out] NetConfigScope[] pOutScope ) { - var returnValue = _GetConfigValueInfo( Self, eValue, pOutName, ref pOutDataType, pOutScope, pOutNextValue ); + var returnValue = _GetConfigValueInfo( Self, eValue, ref pOutDataType, pOutScope ); return returnValue; } #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_GetFirstConfigValue", CallingConvention = Platform.CC)] - private static extern NetConfig _GetFirstConfigValue( IntPtr self ); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_IterateGenericEditableConfigValues", CallingConvention = Platform.CC)] + private static extern NetConfig _IterateGenericEditableConfigValues( IntPtr self, NetConfig eCurrent, [MarshalAs( UnmanagedType.U1 )] bool bEnumerateDevVars ); #endregion - internal NetConfig GetFirstConfigValue() + internal NetConfig IterateGenericEditableConfigValues( NetConfig eCurrent, [MarshalAs( UnmanagedType.U1 )] bool bEnumerateDevVars ) { - var returnValue = _GetFirstConfigValue( Self ); + var returnValue = _IterateGenericEditableConfigValues( Self, eCurrent, bEnumerateDevVars ); return returnValue; } @@ -324,8 +440,7 @@ namespace Steamworks #endregion internal void SteamNetworkingIPAddr_ToString( ref NetAddress addr, out string buf, [MarshalAs( UnmanagedType.U1 )] bool bWithPort ) { - using var memory = Helpers.TakeMemory(); - IntPtr membuf = memory; + using var membuf = Helpers.TakeMemory(); _SteamNetworkingIPAddr_ToString( Self, ref addr, membuf, (1024 * 32), bWithPort ); buf = Helpers.MemoryToString( membuf ); } @@ -342,6 +457,17 @@ namespace Steamworks return returnValue; } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_SteamNetworkingIPAddr_GetFakeIPType", CallingConvention = Platform.CC)] + private static extern SteamNetworkingFakeIPType _SteamNetworkingIPAddr_GetFakeIPType( IntPtr self, ref NetAddress addr ); + + #endregion + internal SteamNetworkingFakeIPType SteamNetworkingIPAddr_GetFakeIPType( ref NetAddress addr ) + { + var returnValue = _SteamNetworkingIPAddr_GetFakeIPType( Self, ref addr ); + return returnValue; + } + #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamNetworkingUtils_SteamNetworkingIdentity_ToString", CallingConvention = Platform.CC)] private static extern void _SteamNetworkingIdentity_ToString( IntPtr self, ref NetIdentity identity, IntPtr buf, uint cbBuf ); @@ -349,8 +475,7 @@ namespace Steamworks #endregion internal void SteamNetworkingIdentity_ToString( ref NetIdentity identity, out string buf ) { - using var memory = Helpers.TakeMemory(); - IntPtr membuf = memory; + using var membuf = Helpers.TakeMemory(); _SteamNetworkingIdentity_ToString( Self, ref identity, membuf, (1024 * 32) ); buf = Helpers.MemoryToString( membuf ); } diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamParentalSettings.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamParentalSettings.cs index 3968079e3..56bfb1e61 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamParentalSettings.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamParentalSettings.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamParentalSettings : SteamInterface + internal unsafe class ISteamParentalSettings : SteamInterface { internal ISteamParentalSettings( bool IsGameServer ) diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamParties.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamParties.cs index 6adabea80..e9585ec8b 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamParties.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamParties.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamParties : SteamInterface + internal unsafe class ISteamParties : SteamInterface { internal ISteamParties( bool IsGameServer ) @@ -50,8 +50,7 @@ namespace Steamworks #endregion internal bool GetBeaconDetails( PartyBeaconID_t ulBeaconID, ref SteamId pSteamIDBeaconOwner, ref SteamPartyBeaconLocation_t pLocation, out string pchMetadata ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchMetadata = memory; + using var mempchMetadata = Helpers.TakeMemory(); var returnValue = _GetBeaconDetails( Self, ulBeaconID, ref pSteamIDBeaconOwner, ref pLocation, mempchMetadata, (1024 * 32) ); pchMetadata = Helpers.MemoryToString( mempchMetadata ); return returnValue; @@ -154,8 +153,7 @@ namespace Steamworks #endregion internal bool GetBeaconLocationData( SteamPartyBeaconLocation_t BeaconLocation, SteamPartyBeaconLocationData eData, out string pchDataStringOut ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchDataStringOut = memory; + using var mempchDataStringOut = Helpers.TakeMemory(); var returnValue = _GetBeaconLocationData( Self, BeaconLocation, eData, mempchDataStringOut, (1024 * 32) ); pchDataStringOut = Helpers.MemoryToString( mempchDataStringOut ); return returnValue; diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamRemotePlay.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamRemotePlay.cs index f2d98c20b..6ad42043c 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamRemotePlay.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamRemotePlay.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamRemotePlay : SteamInterface + internal unsafe class ISteamRemotePlay : SteamInterface { internal ISteamRemotePlay( bool IsGameServer ) diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamRemoteStorage.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamRemoteStorage.cs index cc2ee6b36..c1a872115 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamRemoteStorage.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamRemoteStorage.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamRemoteStorage : SteamInterface + internal unsafe class ISteamRemoteStorage : SteamInterface { internal ISteamRemoteStorage( bool IsGameServer ) @@ -15,9 +15,9 @@ namespace Steamworks SetupInterface( IsGameServer ); } - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamRemoteStorage_v014", CallingConvention = Platform.CC)] - internal static extern IntPtr SteamAPI_SteamRemoteStorage_v014(); - public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamRemoteStorage_v014(); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamRemoteStorage_v016", CallingConvention = Platform.CC)] + internal static extern IntPtr SteamAPI_SteamRemoteStorage_v016(); + public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamRemoteStorage_v016(); #region FunctionMeta @@ -375,5 +375,51 @@ namespace Steamworks return new CallResult( returnValue, IsServer ); } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamRemoteStorage_GetLocalFileChangeCount", CallingConvention = Platform.CC)] + private static extern int _GetLocalFileChangeCount( IntPtr self ); + + #endregion + internal int GetLocalFileChangeCount() + { + var returnValue = _GetLocalFileChangeCount( Self ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamRemoteStorage_GetLocalFileChange", CallingConvention = Platform.CC)] + private static extern Utf8StringPointer _GetLocalFileChange( IntPtr self, int iFile, ref RemoteStorageLocalFileChange pEChangeType, ref RemoteStorageFilePathType pEFilePathType ); + + #endregion + internal string GetLocalFileChange( int iFile, ref RemoteStorageLocalFileChange pEChangeType, ref RemoteStorageFilePathType pEFilePathType ) + { + var returnValue = _GetLocalFileChange( Self, iFile, ref pEChangeType, ref pEFilePathType ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamRemoteStorage_BeginFileWriteBatch", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _BeginFileWriteBatch( IntPtr self ); + + #endregion + internal bool BeginFileWriteBatch() + { + var returnValue = _BeginFileWriteBatch( Self ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamRemoteStorage_EndFileWriteBatch", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _EndFileWriteBatch( IntPtr self ); + + #endregion + internal bool EndFileWriteBatch() + { + var returnValue = _EndFileWriteBatch( Self ); + return returnValue; + } + } } diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamScreenshots.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamScreenshots.cs index c076519a8..ff9380a3f 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamScreenshots.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamScreenshots.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamScreenshots : SteamInterface + internal unsafe class ISteamScreenshots : SteamInterface { internal ISteamScreenshots( bool IsGameServer ) diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamTV.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamTV.cs deleted file mode 100644 index 1a8a6badb..000000000 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamTV.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; -using Steamworks.Data; - - -namespace Steamworks -{ - internal class ISteamTV : SteamInterface - { - - internal ISteamTV( bool IsGameServer ) - { - SetupInterface( IsGameServer ); - } - - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamTV_v001", CallingConvention = Platform.CC)] - internal static extern IntPtr SteamAPI_SteamTV_v001(); - public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamTV_v001(); - - - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamTV_IsBroadcasting", CallingConvention = Platform.CC)] - [return: MarshalAs( UnmanagedType.I1 )] - private static extern bool _IsBroadcasting( IntPtr self, ref int pnNumViewers ); - - #endregion - internal bool IsBroadcasting( ref int pnNumViewers ) - { - var returnValue = _IsBroadcasting( Self, ref pnNumViewers ); - return returnValue; - } - - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamTV_AddBroadcastGameData", CallingConvention = Platform.CC)] - private static extern void _AddBroadcastGameData( IntPtr self, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchKey, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchValue ); - - #endregion - internal void AddBroadcastGameData( [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchKey, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchValue ) - { - _AddBroadcastGameData( Self, pchKey, pchValue ); - } - - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamTV_RemoveBroadcastGameData", CallingConvention = Platform.CC)] - private static extern void _RemoveBroadcastGameData( IntPtr self, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchKey ); - - #endregion - internal void RemoveBroadcastGameData( [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchKey ) - { - _RemoveBroadcastGameData( Self, pchKey ); - } - - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamTV_AddTimelineMarker", CallingConvention = Platform.CC)] - private static extern void _AddTimelineMarker( IntPtr self, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchTemplateName, [MarshalAs( UnmanagedType.U1 )] bool bPersistent, byte nColorR, byte nColorG, byte nColorB ); - - #endregion - internal void AddTimelineMarker( [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchTemplateName, [MarshalAs( UnmanagedType.U1 )] bool bPersistent, byte nColorR, byte nColorG, byte nColorB ) - { - _AddTimelineMarker( Self, pchTemplateName, bPersistent, nColorR, nColorG, nColorB ); - } - - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamTV_RemoveTimelineMarker", CallingConvention = Platform.CC)] - private static extern void _RemoveTimelineMarker( IntPtr self ); - - #endregion - internal void RemoveTimelineMarker() - { - _RemoveTimelineMarker( Self ); - } - - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamTV_AddRegion", CallingConvention = Platform.CC)] - private static extern uint _AddRegion( IntPtr self, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchElementName, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchTimelineDataSection, ref SteamTVRegion_t pSteamTVRegion, SteamTVRegionBehavior eSteamTVRegionBehavior ); - - #endregion - internal uint AddRegion( [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchElementName, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchTimelineDataSection, ref SteamTVRegion_t pSteamTVRegion, SteamTVRegionBehavior eSteamTVRegionBehavior ) - { - var returnValue = _AddRegion( Self, pchElementName, pchTimelineDataSection, ref pSteamTVRegion, eSteamTVRegionBehavior ); - return returnValue; - } - - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamTV_RemoveRegion", CallingConvention = Platform.CC)] - private static extern void _RemoveRegion( IntPtr self, uint unRegionHandle ); - - #endregion - internal void RemoveRegion( uint unRegionHandle ) - { - _RemoveRegion( Self, unRegionHandle ); - } - - } -} diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUGC.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUGC.cs index dbe560c05..758020c64 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUGC.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUGC.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamUGC : SteamInterface + internal unsafe class ISteamUGC : SteamInterface { internal ISteamUGC( bool IsGameServer ) @@ -15,12 +15,12 @@ namespace Steamworks SetupInterface( IsGameServer ); } - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamUGC_v014", CallingConvention = Platform.CC)] - internal static extern IntPtr SteamAPI_SteamUGC_v014(); - public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamUGC_v014(); - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamGameServerUGC_v014", CallingConvention = Platform.CC)] - internal static extern IntPtr SteamAPI_SteamGameServerUGC_v014(); - public override IntPtr GetServerInterfacePointer() => SteamAPI_SteamGameServerUGC_v014(); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamUGC_v017", CallingConvention = Platform.CC)] + internal static extern IntPtr SteamAPI_SteamUGC_v017(); + public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamUGC_v017(); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamGameServerUGC_v017", CallingConvention = Platform.CC)] + internal static extern IntPtr SteamAPI_SteamGameServerUGC_v017(); + public override IntPtr GetServerInterfacePointer() => SteamAPI_SteamGameServerUGC_v017(); #region FunctionMeta @@ -90,6 +90,45 @@ namespace Steamworks return returnValue; } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUGC_GetQueryUGCNumTags", CallingConvention = Platform.CC)] + private static extern uint _GetQueryUGCNumTags( IntPtr self, UGCQueryHandle_t handle, uint index ); + + #endregion + internal uint GetQueryUGCNumTags( UGCQueryHandle_t handle, uint index ) + { + var returnValue = _GetQueryUGCNumTags( Self, handle, index ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUGC_GetQueryUGCTag", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _GetQueryUGCTag( IntPtr self, UGCQueryHandle_t handle, uint index, uint indexTag, IntPtr pchValue, uint cchValueSize ); + + #endregion + internal bool GetQueryUGCTag( UGCQueryHandle_t handle, uint index, uint indexTag, out string pchValue ) + { + using var mempchValue = Helpers.TakeMemory(); + var returnValue = _GetQueryUGCTag( Self, handle, index, indexTag, mempchValue, (1024 * 32) ); + pchValue = Helpers.MemoryToString( mempchValue ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUGC_GetQueryUGCTagDisplayName", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _GetQueryUGCTagDisplayName( IntPtr self, UGCQueryHandle_t handle, uint index, uint indexTag, IntPtr pchValue, uint cchValueSize ); + + #endregion + internal bool GetQueryUGCTagDisplayName( UGCQueryHandle_t handle, uint index, uint indexTag, out string pchValue ) + { + using var mempchValue = Helpers.TakeMemory(); + var returnValue = _GetQueryUGCTagDisplayName( Self, handle, index, indexTag, mempchValue, (1024 * 32) ); + pchValue = Helpers.MemoryToString( mempchValue ); + return returnValue; + } + #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUGC_GetQueryUGCPreviewURL", CallingConvention = Platform.CC)] [return: MarshalAs( UnmanagedType.I1 )] @@ -98,8 +137,7 @@ namespace Steamworks #endregion internal bool GetQueryUGCPreviewURL( UGCQueryHandle_t handle, uint index, out string pchURL ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchURL = memory; + using var mempchURL = Helpers.TakeMemory(); var returnValue = _GetQueryUGCPreviewURL( Self, handle, index, mempchURL, (1024 * 32) ); pchURL = Helpers.MemoryToString( mempchURL ); return returnValue; @@ -113,8 +151,7 @@ namespace Steamworks #endregion internal bool GetQueryUGCMetadata( UGCQueryHandle_t handle, uint index, out string pchMetadata ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchMetadata = memory; + using var mempchMetadata = Helpers.TakeMemory(); var returnValue = _GetQueryUGCMetadata( Self, handle, index, mempchMetadata, (1024 * 32) ); pchMetadata = Helpers.MemoryToString( mempchMetadata ); return returnValue; @@ -163,10 +200,8 @@ namespace Steamworks #endregion internal bool GetQueryUGCAdditionalPreview( UGCQueryHandle_t handle, uint index, uint previewIndex, out string pchURLOrVideoID, out string pchOriginalFileName, ref ItemPreviewType pPreviewType ) { - using var memoryUrlOrId = Helpers.TakeMemory(); - using var memoryFileName = Helpers.TakeMemory(); - IntPtr mempchURLOrVideoID = memoryUrlOrId; - IntPtr mempchOriginalFileName = memoryFileName; + using var mempchURLOrVideoID = Helpers.TakeMemory(); + using var mempchOriginalFileName = Helpers.TakeMemory(); var returnValue = _GetQueryUGCAdditionalPreview( Self, handle, index, previewIndex, mempchURLOrVideoID, (1024 * 32), mempchOriginalFileName, (1024 * 32), ref pPreviewType ); pchURLOrVideoID = Helpers.MemoryToString( mempchURLOrVideoID ); pchOriginalFileName = Helpers.MemoryToString( mempchOriginalFileName ); @@ -192,10 +227,8 @@ namespace Steamworks #endregion internal bool GetQueryUGCKeyValueTag( UGCQueryHandle_t handle, uint index, uint keyValueTagIndex, out string pchKey, out string pchValue ) { - using var memoryKey = Helpers.TakeMemory(); - using var memoryValue = Helpers.TakeMemory(); - IntPtr mempchKey = memoryKey; - IntPtr mempchValue = memoryValue; + using var mempchKey = Helpers.TakeMemory(); + using var mempchValue = Helpers.TakeMemory(); var returnValue = _GetQueryUGCKeyValueTag( Self, handle, index, keyValueTagIndex, mempchKey, (1024 * 32), mempchValue, (1024 * 32) ); pchKey = Helpers.MemoryToString( mempchKey ); pchValue = Helpers.MemoryToString( mempchValue ); @@ -210,13 +243,23 @@ namespace Steamworks #endregion internal bool GetQueryUGCKeyValueTag( UGCQueryHandle_t handle, uint index, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchKey, out string pchValue ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchValue = memory; + using var mempchValue = Helpers.TakeMemory(); var returnValue = _GetQueryUGCKeyValueTag( Self, handle, index, pchKey, mempchValue, (1024 * 32) ); pchValue = Helpers.MemoryToString( mempchValue ); return returnValue; } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUGC_GetQueryUGCContentDescriptors", CallingConvention = Platform.CC)] + private static extern uint _GetQueryUGCContentDescriptors( IntPtr self, UGCQueryHandle_t handle, uint index, [In,Out] UGCContentDescriptorID[] pvecDescriptors, uint cMaxEntries ); + + #endregion + internal uint GetQueryUGCContentDescriptors( UGCQueryHandle_t handle, uint index, [In,Out] UGCContentDescriptorID[] pvecDescriptors, uint cMaxEntries ) + { + var returnValue = _GetQueryUGCContentDescriptors( Self, handle, index, pvecDescriptors, cMaxEntries ); + return returnValue; + } + #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUGC_ReleaseQueryUGCRequest", CallingConvention = Platform.CC)] [return: MarshalAs( UnmanagedType.I1 )] @@ -433,6 +476,30 @@ namespace Steamworks return returnValue; } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUGC_SetTimeCreatedDateRange", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _SetTimeCreatedDateRange( IntPtr self, UGCQueryHandle_t handle, RTime32 rtStart, RTime32 rtEnd ); + + #endregion + internal bool SetTimeCreatedDateRange( UGCQueryHandle_t handle, RTime32 rtStart, RTime32 rtEnd ) + { + var returnValue = _SetTimeCreatedDateRange( Self, handle, rtStart, rtEnd ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUGC_SetTimeUpdatedDateRange", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _SetTimeUpdatedDateRange( IntPtr self, UGCQueryHandle_t handle, RTime32 rtStart, RTime32 rtEnd ); + + #endregion + internal bool SetTimeUpdatedDateRange( UGCQueryHandle_t handle, RTime32 rtStart, RTime32 rtEnd ) + { + var returnValue = _SetTimeUpdatedDateRange( Self, handle, rtStart, rtEnd ); + return returnValue; + } + #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUGC_AddRequiredKeyValueTag", CallingConvention = Platform.CC)] [return: MarshalAs( UnmanagedType.I1 )] @@ -671,6 +738,30 @@ namespace Steamworks return returnValue; } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUGC_AddContentDescriptor", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _AddContentDescriptor( IntPtr self, UGCUpdateHandle_t handle, UGCContentDescriptorID descid ); + + #endregion + internal bool AddContentDescriptor( UGCUpdateHandle_t handle, UGCContentDescriptorID descid ) + { + var returnValue = _AddContentDescriptor( Self, handle, descid ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUGC_RemoveContentDescriptor", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _RemoveContentDescriptor( IntPtr self, UGCUpdateHandle_t handle, UGCContentDescriptorID descid ); + + #endregion + internal bool RemoveContentDescriptor( UGCUpdateHandle_t handle, UGCContentDescriptorID descid ) + { + var returnValue = _RemoveContentDescriptor( Self, handle, descid ); + return returnValue; + } + #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUGC_SubmitItemUpdate", CallingConvention = Platform.CC)] private static extern SteamAPICall_t _SubmitItemUpdate( IntPtr self, UGCUpdateHandle_t handle, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchChangeNote ); @@ -800,8 +891,7 @@ namespace Steamworks #endregion internal bool GetItemInstallInfo( PublishedFileId nPublishedFileID, ref ulong punSizeOnDisk, out string pchFolder, ref uint punTimeStamp ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchFolder = memory; + using var mempchFolder = Helpers.TakeMemory(); var returnValue = _GetItemInstallInfo( Self, nPublishedFileID, ref punSizeOnDisk, mempchFolder, (1024 * 32), ref punTimeStamp ); pchFolder = Helpers.MemoryToString( mempchFolder ); return returnValue; @@ -952,5 +1042,28 @@ namespace Steamworks return new CallResult( returnValue, IsServer ); } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUGC_ShowWorkshopEULA", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _ShowWorkshopEULA( IntPtr self ); + + #endregion + internal bool ShowWorkshopEULA() + { + var returnValue = _ShowWorkshopEULA( Self ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUGC_GetWorkshopEULAStatus", CallingConvention = Platform.CC)] + private static extern SteamAPICall_t _GetWorkshopEULAStatus( IntPtr self ); + + #endregion + internal CallResult GetWorkshopEULAStatus() + { + var returnValue = _GetWorkshopEULAStatus( Self ); + return new CallResult( returnValue, IsServer ); + } + } } diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUser.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUser.cs index 77624f5f8..18147edf6 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUser.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUser.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamUser : SteamInterface + internal unsafe class ISteamUser : SteamInterface { internal ISteamUser( bool IsGameServer ) @@ -15,9 +15,9 @@ namespace Steamworks SetupInterface( IsGameServer ); } - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamUser_v020", CallingConvention = Platform.CC)] - internal static extern IntPtr SteamAPI_SteamUser_v020(); - public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamUser_v020(); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamUser_v023", CallingConvention = Platform.CC)] + internal static extern IntPtr SteamAPI_SteamUser_v023(); + public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamUser_v023(); #region FunctionMeta @@ -55,24 +55,24 @@ namespace Steamworks } #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUser_InitiateGameConnection", CallingConvention = Platform.CC)] - private static extern int _InitiateGameConnection( IntPtr self, IntPtr pAuthBlob, int cbMaxAuthBlob, SteamId steamIDGameServer, uint unIPServer, ushort usPortServer, [MarshalAs( UnmanagedType.U1 )] bool bSecure ); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUser_InitiateGameConnection_DEPRECATED", CallingConvention = Platform.CC)] + private static extern int _InitiateGameConnection_DEPRECATED( IntPtr self, IntPtr pAuthBlob, int cbMaxAuthBlob, SteamId steamIDGameServer, uint unIPServer, ushort usPortServer, [MarshalAs( UnmanagedType.U1 )] bool bSecure ); #endregion - internal int InitiateGameConnection( IntPtr pAuthBlob, int cbMaxAuthBlob, SteamId steamIDGameServer, uint unIPServer, ushort usPortServer, [MarshalAs( UnmanagedType.U1 )] bool bSecure ) + internal int InitiateGameConnection_DEPRECATED( IntPtr pAuthBlob, int cbMaxAuthBlob, SteamId steamIDGameServer, uint unIPServer, ushort usPortServer, [MarshalAs( UnmanagedType.U1 )] bool bSecure ) { - var returnValue = _InitiateGameConnection( Self, pAuthBlob, cbMaxAuthBlob, steamIDGameServer, unIPServer, usPortServer, bSecure ); + var returnValue = _InitiateGameConnection_DEPRECATED( Self, pAuthBlob, cbMaxAuthBlob, steamIDGameServer, unIPServer, usPortServer, bSecure ); return returnValue; } #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUser_TerminateGameConnection", CallingConvention = Platform.CC)] - private static extern void _TerminateGameConnection( IntPtr self, uint unIPServer, ushort usPortServer ); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUser_TerminateGameConnection_DEPRECATED", CallingConvention = Platform.CC)] + private static extern void _TerminateGameConnection_DEPRECATED( IntPtr self, uint unIPServer, ushort usPortServer ); #endregion - internal void TerminateGameConnection( uint unIPServer, ushort usPortServer ) + internal void TerminateGameConnection_DEPRECATED( uint unIPServer, ushort usPortServer ) { - _TerminateGameConnection( Self, unIPServer, usPortServer ); + _TerminateGameConnection_DEPRECATED( Self, unIPServer, usPortServer ); } #region FunctionMeta @@ -93,8 +93,7 @@ namespace Steamworks #endregion internal bool GetUserDataFolder( out string pchBuffer ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchBuffer = memory; + using var mempchBuffer = Helpers.TakeMemory(); var returnValue = _GetUserDataFolder( Self, mempchBuffer, (1024 * 32) ); pchBuffer = Helpers.MemoryToString( mempchBuffer ); return returnValue; @@ -166,12 +165,23 @@ namespace Steamworks #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUser_GetAuthSessionTicket", CallingConvention = Platform.CC)] - private static extern HAuthTicket _GetAuthSessionTicket( IntPtr self, IntPtr pTicket, int cbMaxTicket, ref uint pcbTicket ); + private static extern HAuthTicket _GetAuthSessionTicket( IntPtr self, IntPtr pTicket, int cbMaxTicket, ref uint pcbTicket, ref NetIdentity pSteamNetworkingIdentity ); #endregion - internal HAuthTicket GetAuthSessionTicket( IntPtr pTicket, int cbMaxTicket, ref uint pcbTicket ) + internal HAuthTicket GetAuthSessionTicket( IntPtr pTicket, int cbMaxTicket, ref uint pcbTicket, ref NetIdentity pSteamNetworkingIdentity ) { - var returnValue = _GetAuthSessionTicket( Self, pTicket, cbMaxTicket, ref pcbTicket ); + var returnValue = _GetAuthSessionTicket( Self, pTicket, cbMaxTicket, ref pcbTicket, ref pSteamNetworkingIdentity ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUser_GetAuthTicketForWebApi", CallingConvention = Platform.CC)] + private static extern HAuthTicket _GetAuthTicketForWebApi( IntPtr self, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchIdentity ); + + #endregion + internal HAuthTicket GetAuthTicketForWebApi( [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchIdentity ) + { + var returnValue = _GetAuthTicketForWebApi( Self, pchIdentity ); return returnValue; } @@ -365,5 +375,17 @@ namespace Steamworks return new CallResult( returnValue, IsServer ); } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUser_BSetDurationControlOnlineState", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _BSetDurationControlOnlineState( IntPtr self, DurationControlOnlineState eNewState ); + + #endregion + internal bool BSetDurationControlOnlineState( DurationControlOnlineState eNewState ) + { + var returnValue = _BSetDurationControlOnlineState( Self, eNewState ); + return returnValue; + } + } } diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUserStats.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUserStats.cs index 69d46180b..ab98ca638 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUserStats.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUserStats.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamUserStats : SteamInterface + internal unsafe class ISteamUserStats : SteamInterface { internal ISteamUserStats( bool IsGameServer ) @@ -15,9 +15,9 @@ namespace Steamworks SetupInterface( IsGameServer ); } - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamUserStats_v011", CallingConvention = Platform.CC)] - internal static extern IntPtr SteamAPI_SteamUserStats_v011(); - public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamUserStats_v011(); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamUserStats_v012", CallingConvention = Platform.CC)] + internal static extern IntPtr SteamAPI_SteamUserStats_v012(); + public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamUserStats_v012(); #region FunctionMeta @@ -361,9 +361,6 @@ namespace Steamworks private static extern SteamAPICall_t _DownloadLeaderboardEntriesForUsers( IntPtr self, SteamLeaderboard_t hSteamLeaderboard, [In,Out] SteamId[] prgUsers, int cUsers ); #endregion - /// - /// Downloads leaderboard entries for an arbitrary set of users - ELeaderboardDataRequest is k_ELeaderboardDataRequestUsers - /// internal CallResult DownloadLeaderboardEntriesForUsers( SteamLeaderboard_t hSteamLeaderboard, [In,Out] SteamId[] prgUsers, int cUsers ) { var returnValue = _DownloadLeaderboardEntriesForUsers( Self, hSteamLeaderboard, prgUsers, cUsers ); @@ -433,8 +430,7 @@ namespace Steamworks #endregion internal int GetMostAchievedAchievementInfo( out string pchName, ref float pflPercent, [MarshalAs( UnmanagedType.U1 )] ref bool pbAchieved ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchName = memory; + using var mempchName = Helpers.TakeMemory(); var returnValue = _GetMostAchievedAchievementInfo( Self, mempchName, (1024 * 32), ref pflPercent, ref pbAchieved ); pchName = Helpers.MemoryToString( mempchName ); return returnValue; @@ -447,8 +443,7 @@ namespace Steamworks #endregion internal int GetNextMostAchievedAchievementInfo( int iIteratorPrevious, out string pchName, ref float pflPercent, [MarshalAs( UnmanagedType.U1 )] ref bool pbAchieved ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchName = memory; + using var mempchName = Helpers.TakeMemory(); var returnValue = _GetNextMostAchievedAchievementInfo( Self, iIteratorPrevious, mempchName, (1024 * 32), ref pflPercent, ref pbAchieved ); pchName = Helpers.MemoryToString( mempchName ); return returnValue; @@ -523,5 +518,29 @@ namespace Steamworks return returnValue; } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUserStats_GetAchievementProgressLimitsInt32", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _GetAchievementProgressLimits( IntPtr self, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchName, ref int pnMinProgress, ref int pnMaxProgress ); + + #endregion + internal bool GetAchievementProgressLimits( [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchName, ref int pnMinProgress, ref int pnMaxProgress ) + { + var returnValue = _GetAchievementProgressLimits( Self, pchName, ref pnMinProgress, ref pnMaxProgress ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUserStats_GetAchievementProgressLimitsFloat", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _GetAchievementProgressLimits( IntPtr self, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchName, ref float pfMinProgress, ref float pfMaxProgress ); + + #endregion + internal bool GetAchievementProgressLimits( [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchName, ref float pfMinProgress, ref float pfMaxProgress ) + { + var returnValue = _GetAchievementProgressLimits( Self, pchName, ref pfMinProgress, ref pfMaxProgress ); + return returnValue; + } + } } diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUtils.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUtils.cs index aae959ae4..f0a4cf544 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUtils.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUtils.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamUtils : SteamInterface + internal unsafe class ISteamUtils : SteamInterface { internal ISteamUtils( bool IsGameServer ) @@ -15,12 +15,12 @@ namespace Steamworks SetupInterface( IsGameServer ); } - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamUtils_v009", CallingConvention = Platform.CC)] - internal static extern IntPtr SteamAPI_SteamUtils_v009(); - public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamUtils_v009(); - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamGameServerUtils_v009", CallingConvention = Platform.CC)] - internal static extern IntPtr SteamAPI_SteamGameServerUtils_v009(); - public override IntPtr GetServerInterfacePointer() => SteamAPI_SteamGameServerUtils_v009(); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamUtils_v010", CallingConvention = Platform.CC)] + internal static extern IntPtr SteamAPI_SteamUtils_v010(); + public override IntPtr GetUserInterfacePointer() => SteamAPI_SteamUtils_v010(); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamGameServerUtils_v010", CallingConvention = Platform.CC)] + internal static extern IntPtr SteamAPI_SteamGameServerUtils_v010(); + public override IntPtr GetServerInterfacePointer() => SteamAPI_SteamGameServerUtils_v010(); #region FunctionMeta @@ -102,18 +102,6 @@ namespace Steamworks return returnValue; } - #region FunctionMeta - [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUtils_GetCSERIPPort", CallingConvention = Platform.CC)] - [return: MarshalAs( UnmanagedType.I1 )] - private static extern bool _GetCSERIPPort( IntPtr self, ref uint unIP, ref ushort usPort ); - - #endregion - internal bool GetCSERIPPort( ref uint unIP, ref ushort usPort ) - { - var returnValue = _GetCSERIPPort( Self, ref unIP, ref usPort ); - return returnValue; - } - #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUtils_GetCurrentBatteryPower", CallingConvention = Platform.CC)] private static extern byte _GetCurrentBatteryPower( IntPtr self ); @@ -268,8 +256,7 @@ namespace Steamworks #endregion internal bool GetEnteredGamepadTextInput( out string pchText ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchText = memory; + using var mempchText = Helpers.TakeMemory(); var returnValue = _GetEnteredGamepadTextInput( Self, mempchText, (1024 * 32) ); pchText = Helpers.MemoryToString( mempchText ); return returnValue; @@ -367,25 +354,24 @@ namespace Steamworks #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUtils_InitFilterText", CallingConvention = Platform.CC)] [return: MarshalAs( UnmanagedType.I1 )] - private static extern bool _InitFilterText( IntPtr self ); + private static extern bool _InitFilterText( IntPtr self, uint unFilterOptions ); #endregion - internal bool InitFilterText() + internal bool InitFilterText( uint unFilterOptions ) { - var returnValue = _InitFilterText( Self ); + var returnValue = _InitFilterText( Self, unFilterOptions ); return returnValue; } #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUtils_FilterText", CallingConvention = Platform.CC)] - private static extern int _FilterText( IntPtr self, IntPtr pchOutFilteredText, uint nByteSizeOutFilteredText, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchInputMessage, [MarshalAs( UnmanagedType.U1 )] bool bLegalOnly ); + private static extern int _FilterText( IntPtr self, TextFilteringContext eContext, SteamId sourceSteamID, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchInputMessage, IntPtr pchOutFilteredText, uint nByteSizeOutFilteredText ); #endregion - internal int FilterText( out string pchOutFilteredText, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchInputMessage, [MarshalAs( UnmanagedType.U1 )] bool bLegalOnly ) + internal int FilterText( TextFilteringContext eContext, SteamId sourceSteamID, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchInputMessage, out string pchOutFilteredText ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchOutFilteredText = memory; - var returnValue = _FilterText( Self, mempchOutFilteredText, (1024 * 32), pchInputMessage, bLegalOnly ); + using var mempchOutFilteredText = Helpers.TakeMemory(); + var returnValue = _FilterText( Self, eContext, sourceSteamID, pchInputMessage, mempchOutFilteredText, (1024 * 32) ); pchOutFilteredText = Helpers.MemoryToString( mempchOutFilteredText ); return returnValue; } @@ -401,5 +387,51 @@ namespace Steamworks return returnValue; } + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUtils_IsSteamRunningOnSteamDeck", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _IsSteamRunningOnSteamDeck( IntPtr self ); + + #endregion + internal bool IsSteamRunningOnSteamDeck() + { + var returnValue = _IsSteamRunningOnSteamDeck( Self ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUtils_ShowFloatingGamepadTextInput", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _ShowFloatingGamepadTextInput( IntPtr self, TextInputMode eKeyboardMode, int nTextFieldXPosition, int nTextFieldYPosition, int nTextFieldWidth, int nTextFieldHeight ); + + #endregion + internal bool ShowFloatingGamepadTextInput( TextInputMode eKeyboardMode, int nTextFieldXPosition, int nTextFieldYPosition, int nTextFieldWidth, int nTextFieldHeight ) + { + var returnValue = _ShowFloatingGamepadTextInput( Self, eKeyboardMode, nTextFieldXPosition, nTextFieldYPosition, nTextFieldWidth, nTextFieldHeight ); + return returnValue; + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUtils_SetGameLauncherMode", CallingConvention = Platform.CC)] + private static extern void _SetGameLauncherMode( IntPtr self, [MarshalAs( UnmanagedType.U1 )] bool bLauncherMode ); + + #endregion + internal void SetGameLauncherMode( [MarshalAs( UnmanagedType.U1 )] bool bLauncherMode ) + { + _SetGameLauncherMode( Self, bLauncherMode ); + } + + #region FunctionMeta + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamUtils_DismissFloatingGamepadTextInput", CallingConvention = Platform.CC)] + [return: MarshalAs( UnmanagedType.I1 )] + private static extern bool _DismissFloatingGamepadTextInput( IntPtr self ); + + #endregion + internal bool DismissFloatingGamepadTextInput() + { + var returnValue = _DismissFloatingGamepadTextInput( Self ); + return returnValue; + } + } } diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamVideo.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamVideo.cs index c6c55105e..bd4d029a8 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamVideo.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamVideo.cs @@ -7,7 +7,7 @@ using Steamworks.Data; namespace Steamworks { - internal class ISteamVideo : SteamInterface + internal unsafe class ISteamVideo : SteamInterface { internal ISteamVideo( bool IsGameServer ) @@ -60,8 +60,7 @@ namespace Steamworks #endregion internal bool GetOPFStringForApp( AppId unVideoAppID, out string pchBuffer, ref int pnBufferSize ) { - using var memory = Helpers.TakeMemory(); - IntPtr mempchBuffer = memory; + using var mempchBuffer = Helpers.TakeMemory(); var returnValue = _GetOPFStringForApp( Self, unVideoAppID, mempchBuffer, ref pnBufferSize ); pchBuffer = Helpers.MemoryToString( mempchBuffer ); return returnValue; diff --git a/Libraries/Facepunch.Steamworks/Generated/SteamCallbacks.cs b/Libraries/Facepunch.Steamworks/Generated/SteamCallbacks.cs index fbc2d590a..883114778 100644 --- a/Libraries/Facepunch.Steamworks/Generated/SteamCallbacks.cs +++ b/Libraries/Facepunch.Steamworks/Generated/SteamCallbacks.cs @@ -206,6 +206,22 @@ namespace Steamworks.Data #endregion } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct GetTicketForWebApiResponse_t : ICallbackData + { + internal uint AuthTicket; // m_hAuthTicket HAuthTicket + internal Result Result; // m_eResult EResult + internal int Ticket; // m_cubTicket int + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2560)] // m_rgubTicket + internal byte[] GubTicket; // m_rgubTicket uint8 [2560] + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(GetTicketForWebApiResponse_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.GetTicketForWebApiResponse; + #endregion + } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] internal struct PersonaStateChange_t : ICallbackData { @@ -223,6 +239,9 @@ namespace Steamworks.Data internal struct GameOverlayActivated_t : ICallbackData { internal byte Active; // m_bActive uint8 + [MarshalAs(UnmanagedType.I1)] + internal bool UserInitiated; // m_bUserInitiated bool + internal AppId AppID; // m_nAppID AppId_t #region SteamCallback public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(GameOverlayActivated_t) ); @@ -473,6 +492,55 @@ namespace Steamworks.Data #endregion } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct OverlayBrowserProtocolNavigation_t : ICallbackData + { + internal string RgchURIUTF8() => System.Text.Encoding.UTF8.GetString( RgchURI, 0, System.Array.IndexOf( RgchURI, 0 ) ); + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1024)] // byte[] rgchURI + internal byte[] RgchURI; // rgchURI char [1024] + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(OverlayBrowserProtocolNavigation_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.OverlayBrowserProtocolNavigation; + #endregion + } + + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct EquippedProfileItemsChanged_t : ICallbackData + { + internal ulong SteamID; // m_steamID CSteamID + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(EquippedProfileItemsChanged_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.EquippedProfileItemsChanged; + #endregion + } + + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPackSize )] + internal struct EquippedProfileItems_t : ICallbackData + { + internal Result Result; // m_eResult EResult + internal ulong SteamID; // m_steamID CSteamID + [MarshalAs(UnmanagedType.I1)] + internal bool HasAnimatedAvatar; // m_bHasAnimatedAvatar bool + [MarshalAs(UnmanagedType.I1)] + internal bool HasAvatarFrame; // m_bHasAvatarFrame bool + [MarshalAs(UnmanagedType.I1)] + internal bool HasProfileModifier; // m_bHasProfileModifier bool + [MarshalAs(UnmanagedType.I1)] + internal bool HasProfileBackground; // m_bHasProfileBackground bool + [MarshalAs(UnmanagedType.I1)] + internal bool HasMiniProfileBackground; // m_bHasMiniProfileBackground bool + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(EquippedProfileItems_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.EquippedProfileItems; + #endregion + } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] internal struct IPCountry_t : ICallbackData { @@ -539,6 +607,7 @@ namespace Steamworks.Data [MarshalAs(UnmanagedType.I1)] internal bool Submitted; // m_bSubmitted bool internal uint SubmittedText; // m_unSubmittedText uint32 + internal AppId AppID; // m_unAppID AppId_t #region SteamCallback public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(GamepadTextInputDismissed_t) ); @@ -547,6 +616,40 @@ namespace Steamworks.Data #endregion } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct AppResumingFromSuspend_t : ICallbackData + { + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(AppResumingFromSuspend_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.AppResumingFromSuspend; + #endregion + } + + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct FloatingGamepadTextInputDismissed_t : ICallbackData + { + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(FloatingGamepadTextInputDismissed_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.FloatingGamepadTextInputDismissed; + #endregion + } + + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct FilterTextDictionaryChanged_t : ICallbackData + { + internal int Language; // m_eLanguage int + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(FilterTextDictionaryChanged_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.FilterTextDictionaryChanged; + #endregion + } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] internal struct FavoritesListChanged_t : ICallbackData { @@ -914,66 +1017,6 @@ namespace Steamworks.Data #endregion } - [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] - internal struct RemoteStorageAppSyncedClient_t : ICallbackData - { - internal AppId AppID; // m_nAppID AppId_t - internal Result Result; // m_eResult EResult - internal int NumDownloads; // m_unNumDownloads int - - #region SteamCallback - public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(RemoteStorageAppSyncedClient_t) ); - public int DataSize => _datasize; - public CallbackType CallbackType => CallbackType.RemoteStorageAppSyncedClient; - #endregion - } - - [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] - internal struct RemoteStorageAppSyncedServer_t : ICallbackData - { - internal AppId AppID; // m_nAppID AppId_t - internal Result Result; // m_eResult EResult - internal int NumUploads; // m_unNumUploads int - - #region SteamCallback - public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(RemoteStorageAppSyncedServer_t) ); - public int DataSize => _datasize; - public CallbackType CallbackType => CallbackType.RemoteStorageAppSyncedServer; - #endregion - } - - [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] - internal struct RemoteStorageAppSyncProgress_t : ICallbackData - { - internal string CurrentFileUTF8() => System.Text.Encoding.UTF8.GetString( CurrentFile, 0, System.Array.IndexOf( CurrentFile, 0 ) ); - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 260)] // byte[] m_rgchCurrentFile - internal byte[] CurrentFile; // m_rgchCurrentFile char [260] - internal AppId AppID; // m_nAppID AppId_t - internal uint BytesTransferredThisChunk; // m_uBytesTransferredThisChunk uint32 - internal double DAppPercentComplete; // m_dAppPercentComplete double - [MarshalAs(UnmanagedType.I1)] - internal bool Uploading; // m_bUploading bool - - #region SteamCallback - public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(RemoteStorageAppSyncProgress_t) ); - public int DataSize => _datasize; - public CallbackType CallbackType => CallbackType.RemoteStorageAppSyncProgress; - #endregion - } - - [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] - internal struct RemoteStorageAppSyncStatusCheck_t : ICallbackData - { - internal AppId AppID; // m_nAppID AppId_t - internal Result Result; // m_eResult EResult - - #region SteamCallback - public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(RemoteStorageAppSyncStatusCheck_t) ); - public int DataSize => _datasize; - public CallbackType CallbackType => CallbackType.RemoteStorageAppSyncStatusCheck; - #endregion - } - [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] internal struct RemoteStorageFileShareResult_t : ICallbackData { @@ -1364,6 +1407,17 @@ namespace Steamworks.Data #endregion } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct RemoteStorageLocalFileChange_t : ICallbackData + { + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(RemoteStorageLocalFileChange_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.RemoteStorageLocalFileChange; + #endregion + } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPackSize )] internal struct UserStatsReceived_t : ICallbackData { @@ -1548,19 +1602,6 @@ namespace Steamworks.Data #endregion } - [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] - internal struct RegisterActivationCodeResponse_t : ICallbackData - { - internal RegisterActivationCodeResult Result; // m_eResult ERegisterActivationCodeResult - internal uint PackageRegistered; // m_unPackageRegistered uint32 - - #region SteamCallback - public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(RegisterActivationCodeResponse_t) ); - public int DataSize => _datasize; - public CallbackType CallbackType => CallbackType.RegisterActivationCodeResponse; - #endregion - } - [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] internal struct NewUrlLaunchParameters_t : ICallbackData { @@ -1605,6 +1646,22 @@ namespace Steamworks.Data #endregion } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct TimedTrialStatus_t : ICallbackData + { + internal AppId AppID; // m_unAppID AppId_t + [MarshalAs(UnmanagedType.I1)] + internal bool IsOffline; // m_bIsOffline bool + internal uint SecondsAllowed; // m_unSecondsAllowed uint32 + internal uint SecondsPlayed; // m_unSecondsPlayed uint32 + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(TimedTrialStatus_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.TimedTrialStatus; + #endregion + } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] internal struct P2PSessionRequest_t : ICallbackData { @@ -1884,6 +1941,66 @@ namespace Steamworks.Data #endregion } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct SteamInputDeviceConnected_t : ICallbackData + { + internal ulong ConnectedDeviceHandle; // m_ulConnectedDeviceHandle InputHandle_t + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(SteamInputDeviceConnected_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.SteamInputDeviceConnected; + #endregion + } + + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct SteamInputDeviceDisconnected_t : ICallbackData + { + internal ulong DisconnectedDeviceHandle; // m_ulDisconnectedDeviceHandle InputHandle_t + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(SteamInputDeviceDisconnected_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.SteamInputDeviceDisconnected; + #endregion + } + + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPackSize )] + internal struct SteamInputConfigurationLoaded_t : ICallbackData + { + internal AppId AppID; // m_unAppID AppId_t + internal ulong DeviceHandle; // m_ulDeviceHandle InputHandle_t + internal ulong MappingCreator; // m_ulMappingCreator CSteamID + internal uint MajorRevision; // m_unMajorRevision uint32 + internal uint MinorRevision; // m_unMinorRevision uint32 + [MarshalAs(UnmanagedType.I1)] + internal bool UsesSteamInputAPI; // m_bUsesSteamInputAPI bool + [MarshalAs(UnmanagedType.I1)] + internal bool UsesGamepadAPI; // m_bUsesGamepadAPI bool + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(SteamInputConfigurationLoaded_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.SteamInputConfigurationLoaded; + #endregion + } + + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct SteamInputGamepadSlotChange_t : ICallbackData + { + internal AppId AppID; // m_unAppID AppId_t + internal ulong DeviceHandle; // m_ulDeviceHandle InputHandle_t + internal InputType DeviceType; // m_eDeviceType ESteamInputType + internal int OldGamepadSlot; // m_nOldGamepadSlot int + internal int NewGamepadSlot; // m_nNewGamepadSlot int + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(SteamInputGamepadSlotChange_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.SteamInputGamepadSlotChange; + #endregion + } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] internal struct SteamUGCQueryCompleted_t : ICallbackData { @@ -2134,10 +2251,42 @@ namespace Steamworks.Data #endregion } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct UserSubscribedItemsListChanged_t : ICallbackData + { + internal AppId AppID; // m_nAppID AppId_t + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(UserSubscribedItemsListChanged_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.UserSubscribedItemsListChanged; + #endregion + } + + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct WorkshopEULAStatus_t : ICallbackData + { + internal Result Result; // m_eResult EResult + internal AppId AppID; // m_nAppID AppId_t + internal uint Version; // m_unVersion uint32 + internal uint TAction; // m_rtAction RTime32 + [MarshalAs(UnmanagedType.I1)] + internal bool Accepted; // m_bAccepted bool + [MarshalAs(UnmanagedType.I1)] + internal bool NeedsAction; // m_bNeedsAction bool + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(WorkshopEULAStatus_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.WorkshopEULAStatus; + #endregion + } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] internal struct SteamAppInstalled_t : ICallbackData { internal AppId AppID; // m_nAppID AppId_t + internal int InstallFolderIndex; // m_iInstallFolderIndex int #region SteamCallback public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(SteamAppInstalled_t) ); @@ -2150,6 +2299,7 @@ namespace Steamworks.Data internal struct SteamAppUninstalled_t : ICallbackData { internal AppId AppID; // m_nAppID AppId_t + internal int InstallFolderIndex; // m_iInstallFolderIndex int #region SteamCallback public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(SteamAppUninstalled_t) ); @@ -2611,31 +2761,6 @@ namespace Steamworks.Data #endregion } - [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] - internal struct BroadcastUploadStart_t : ICallbackData - { - [MarshalAs(UnmanagedType.I1)] - internal bool IsRTMP; // m_bIsRTMP bool - - #region SteamCallback - public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(BroadcastUploadStart_t) ); - public int DataSize => _datasize; - public CallbackType CallbackType => CallbackType.BroadcastUploadStart; - #endregion - } - - [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] - internal struct BroadcastUploadStop_t : ICallbackData - { - internal BroadcastUploadResult Result; // m_eResult EBroadcastUploadResult - - #region SteamCallback - public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(BroadcastUploadStop_t) ); - public int DataSize => _datasize; - public CallbackType CallbackType => CallbackType.BroadcastUploadStop; - #endregion - } - [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] internal struct SteamParentalSettingsChanged_t : ICallbackData { @@ -2671,6 +2796,44 @@ namespace Steamworks.Data #endregion } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct SteamRemotePlayTogetherGuestInvite_t : ICallbackData + { + internal string ConnectURLUTF8() => System.Text.Encoding.UTF8.GetString( ConnectURL, 0, System.Array.IndexOf( ConnectURL, 0 ) ); + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1024)] // byte[] m_szConnectURL + internal byte[] ConnectURL; // m_szConnectURL char [1024] + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(SteamRemotePlayTogetherGuestInvite_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.SteamRemotePlayTogetherGuestInvite; + #endregion + } + + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct SteamNetworkingMessagesSessionRequest_t : ICallbackData + { + internal NetIdentity DentityRemote; // m_identityRemote SteamNetworkingIdentity + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(SteamNetworkingMessagesSessionRequest_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.SteamNetworkingMessagesSessionRequest; + #endregion + } + + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct SteamNetworkingMessagesSessionFailed_t : ICallbackData + { + internal ConnectionInfo Nfo; // m_info SteamNetConnectionInfo_t + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(SteamNetworkingMessagesSessionFailed_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.SteamNetworkingMessagesSessionFailed; + #endregion + } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] internal struct SteamNetConnectionStatusChangedCallback_t : ICallbackData { @@ -2906,4 +3069,20 @@ namespace Steamworks.Data #endregion } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct SteamNetworkingFakeIPResult_t : ICallbackData + { + internal Result Result; // m_eResult EResult + internal NetIdentity Dentity; // m_identity SteamNetworkingIdentity + internal uint IP; // m_unIP uint32 + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8, ArraySubType = UnmanagedType.U2)] + internal ushort[] Ports; // m_unPorts uint16 [8] + + #region SteamCallback + public static int _datasize = System.Runtime.InteropServices.Marshal.SizeOf( typeof(SteamNetworkingFakeIPResult_t) ); + public int DataSize => _datasize; + public CallbackType CallbackType => CallbackType.SteamNetworkingFakeIPResult; + #endregion + } + } diff --git a/Libraries/Facepunch.Steamworks/Generated/SteamConstants.cs b/Libraries/Facepunch.Steamworks/Generated/SteamConstants.cs index 521b00d28..fcdd12f1c 100644 --- a/Libraries/Facepunch.Steamworks/Generated/SteamConstants.cs +++ b/Libraries/Facepunch.Steamworks/Generated/SteamConstants.cs @@ -8,22 +8,9 @@ namespace Steamworks.Data { internal static class Defines { - internal static readonly int k_cubSaltSize = 8; - internal static readonly GID_t k_GIDNil = 0xffffffffffffffff; - internal static readonly GID_t k_TxnIDNil = k_GIDNil; - internal static readonly GID_t k_TxnIDUnknown = 0; - internal static readonly JobID_t k_JobIDNil = 0xffffffffffffffff; - internal static readonly PackageId_t k_uPackageIdInvalid = 0xFFFFFFFF; - internal static readonly BundleId_t k_uBundleIdInvalid = 0; internal static readonly AppId k_uAppIdInvalid = 0x0; - internal static readonly AssetClassId_t k_ulAssetClassIdInvalid = 0x0; - internal static readonly PhysicalItemId_t k_uPhysicalItemIdInvalid = 0x0; internal static readonly DepotId_t k_uDepotIdInvalid = 0x0; - internal static readonly CellID_t k_uCellIDInvalid = 0xFFFFFFFF; internal static readonly SteamAPICall_t k_uAPICallInvalid = 0x0; - internal static readonly PartnerId_t k_uPartnerIdInvalid = 0; - internal static readonly ManifestId_t k_uManifestIdInvalid = 0; - internal static readonly SiteId_t k_ulSiteIdInvalid = 0; internal static readonly PartyBeaconID_t k_ulPartyBeaconIdInvalid = 0; internal static readonly HAuthTicket k_HAuthTicketInvalid = 0; internal static readonly uint k_unSteamAccountIDMask = 0xFFFFFFFF; @@ -77,6 +64,13 @@ namespace Steamworks.Data internal static readonly int k_cchMaxSteamNetworkingErrMsg = 1024; internal static readonly int k_cchSteamNetworkingMaxConnectionCloseReason = 128; internal static readonly int k_cchSteamNetworkingMaxConnectionDescription = 128; + internal static readonly int k_cchSteamNetworkingMaxConnectionAppName = 32; + internal static readonly int k_nSteamNetworkConnectionInfoFlags_Unauthenticated = 1; + internal static readonly int k_nSteamNetworkConnectionInfoFlags_Unencrypted = 2; + internal static readonly int k_nSteamNetworkConnectionInfoFlags_LoopbackBuffers = 4; + internal static readonly int k_nSteamNetworkConnectionInfoFlags_Fast = 8; + internal static readonly int k_nSteamNetworkConnectionInfoFlags_Relayed = 16; + internal static readonly int k_nSteamNetworkConnectionInfoFlags_DualWifi = 32; internal static readonly int k_cbMaxSteamNetworkingSocketsMessageSizeSend = 512 * 1024; internal static readonly int k_nSteamNetworkingSend_Unreliable = 0; internal static readonly int k_nSteamNetworkingSend_NoNagle = 1; @@ -86,19 +80,23 @@ namespace Steamworks.Data internal static readonly int k_nSteamNetworkingSend_Reliable = 8; internal static readonly int k_nSteamNetworkingSend_ReliableNoNagle = k_nSteamNetworkingSend_Reliable | k_nSteamNetworkingSend_NoNagle; internal static readonly int k_nSteamNetworkingSend_UseCurrentThread = 16; + internal static readonly int k_nSteamNetworkingSend_AutoRestartBrokenSession = 32; internal static readonly int k_cchMaxSteamNetworkingPingLocationString = 1024; internal static readonly int k_nSteamNetworkingPing_Failed = - 1; internal static readonly int k_nSteamNetworkingPing_Unknown = - 2; + internal static readonly int k_nSteamNetworkingConfig_P2P_Transport_ICE_Enable_Default = - 1; + internal static readonly int k_nSteamNetworkingConfig_P2P_Transport_ICE_Enable_Disable = 0; + internal static readonly int k_nSteamNetworkingConfig_P2P_Transport_ICE_Enable_Relay = 1; + internal static readonly int k_nSteamNetworkingConfig_P2P_Transport_ICE_Enable_Private = 2; + internal static readonly int k_nSteamNetworkingConfig_P2P_Transport_ICE_Enable_Public = 4; + internal static readonly int k_nSteamNetworkingConfig_P2P_Transport_ICE_Enable_All = 0x7fffffff; internal static readonly SteamNetworkingPOPID k_SteamDatagramPOPID_dev = ( ( uint ) 'd' << 16 ) | ( ( uint ) 'e' << 8 ) | ( uint ) 'v'; - internal static readonly uint k_unServerFlagNone = 0x00; - internal static readonly uint k_unServerFlagActive = 0x01; - internal static readonly uint k_unServerFlagSecure = 0x02; - internal static readonly uint k_unServerFlagDedicated = 0x04; - internal static readonly uint k_unServerFlagLinux = 0x08; - internal static readonly uint k_unServerFlagPassworded = 0x10; - internal static readonly uint k_unServerFlagPrivate = 0x20; + internal static readonly ushort STEAMGAMESERVER_QUERY_PORT_SHARED = 0xffff; + internal static readonly ushort MASTERSERVERUPDATERPORT_USEGAMESOCKETSHARE = STEAMGAMESERVER_QUERY_PORT_SHARED; internal static readonly uint k_cbSteamDatagramMaxSerializedTicket = 512; internal static readonly uint k_cbMaxSteamDatagramGameCoordinatorServerLoginAppData = 2048; internal static readonly uint k_cbMaxSteamDatagramGameCoordinatorServerLoginSerialized = 4096; + internal static readonly int k_cbSteamNetworkingSocketsFakeUDPPortRecommendedMTU = 1200; + internal static readonly int k_cbSteamNetworkingSocketsFakeUDPPortMaxMessageSize = 4096; } } diff --git a/Libraries/Facepunch.Steamworks/Generated/SteamEnums.cs b/Libraries/Facepunch.Steamworks/Generated/SteamEnums.cs index 253ef88d2..0dc3329ac 100644 --- a/Libraries/Facepunch.Steamworks/Generated/SteamEnums.cs +++ b/Libraries/Facepunch.Steamworks/Generated/SteamEnums.cs @@ -148,6 +148,18 @@ namespace Steamworks CantRemoveItem = 113, AccountDeleted = 114, ExistingUserCancelledLicense = 115, + CommunityCooldown = 116, + NoLauncherSpecified = 117, + MustAgreeToSSA = 118, + LauncherMigrated = 119, + SteamRealmMismatch = 120, + InvalidSignature = 121, + ParseFailure = 122, + NoVerifiedPhone = 123, + InsufficientBattery = 124, + ChargerRequired = 125, + CachedCredentialInvalid = 126, + K_EResultPhoneNumberIsVOIP = 127, } // @@ -222,6 +234,7 @@ namespace Steamworks AuthTicketInvalidAlreadyUsed = 7, AuthTicketInvalid = 8, PublisherIssuedBan = 9, + AuthTicketNetworkIdentityFailure = 10, } // @@ -253,88 +266,6 @@ namespace Steamworks Max = 11, } - // - // EAppReleaseState - // - internal enum AppReleaseState : int - { - Unknown = 0, - Unavailable = 1, - Prerelease = 2, - PreloadOnly = 3, - Released = 4, - } - - // - // EAppOwnershipFlags - // - internal enum AppOwnershipFlags : int - { - None = 0, - OwnsLicense = 1, - FreeLicense = 2, - RegionRestricted = 4, - LowViolence = 8, - InvalidPlatform = 16, - SharedLicense = 32, - FreeWeekend = 64, - RetailLicense = 128, - LicenseLocked = 256, - LicensePending = 512, - LicenseExpired = 1024, - LicensePermanent = 2048, - LicenseRecurring = 4096, - LicenseCanceled = 8192, - AutoGrant = 16384, - PendingGift = 32768, - RentalNotActivated = 65536, - Rental = 131072, - SiteLicense = 262144, - LegacyFreeSub = 524288, - InvalidOSType = 1048576, - } - - // - // EAppType - // - internal enum AppType : int - { - Invalid = 0, - Game = 1, - Application = 2, - Tool = 4, - Demo = 8, - Media_DEPRECATED = 16, - DLC = 32, - Guide = 64, - Driver = 128, - Config = 256, - Hardware = 512, - Franchise = 1024, - Video = 2048, - Plugin = 4096, - MusicAlbum = 8192, - Series = 16384, - Comic_UNUSED = 32768, - Beta = 65536, - Shortcut = 1073741824, - DepotOnly = -2147483648, - } - - // - // ESteamUserStatType - // - internal enum SteamUserStatType : int - { - INVALID = 0, - INT = 1, - FLOAT = 2, - AVGRATE = 3, - ACHIEVEMENTS = 4, - GROUPACHIEVEMENTS = 5, - MAX = 6, - } - // // EChatEntryType // @@ -384,24 +315,12 @@ namespace Steamworks InstanceFlagMMSLobby = 131072, } - // - // EMarketingMessageFlags - // - internal enum MarketingMessageFlags : int - { - None = 0, - HighPriority = 1, - PlatformWindows = 2, - PlatformMac = 4, - PlatformLinux = 8, - PlatformRestrictions = 14, - } - // // ENotificationPosition // public enum NotificationPosition : int { + Invalid = -1, TopLeft = 0, TopRight = 1, BottomLeft = 2, @@ -439,70 +358,6 @@ namespace Steamworks AudioInitFailed = 23, } - // - // ELaunchOptionType - // - internal enum LaunchOptionType : int - { - None = 0, - Default = 1, - SafeMode = 2, - Multiplayer = 3, - Config = 4, - OpenVR = 5, - Server = 6, - Editor = 7, - Manual = 8, - Benchmark = 9, - Option1 = 10, - Option2 = 11, - Option3 = 12, - OculusVR = 13, - OpenVROverlay = 14, - OSVR = 15, - Dialog = 1000, - } - - // - // EVRHMDType - // - internal enum VRHMDType : int - { - MDType_None = -1, - MDType_Unknown = 0, - MDType_HTC_Dev = 1, - MDType_HTC_VivePre = 2, - MDType_HTC_Vive = 3, - MDType_HTC_VivePro = 4, - MDType_HTC_ViveCosmos = 5, - MDType_HTC_Unknown = 20, - MDType_Oculus_DK1 = 21, - MDType_Oculus_DK2 = 22, - MDType_Oculus_Rift = 23, - MDType_Oculus_RiftS = 24, - MDType_Oculus_Quest = 25, - MDType_Oculus_Unknown = 40, - MDType_Acer_Unknown = 50, - MDType_Acer_WindowsMR = 51, - MDType_Dell_Unknown = 60, - MDType_Dell_Visor = 61, - MDType_Lenovo_Unknown = 70, - MDType_Lenovo_Explorer = 71, - MDType_HP_Unknown = 80, - MDType_HP_WindowsMR = 81, - MDType_HP_Reverb = 82, - MDType_Samsung_Unknown = 90, - MDType_Samsung_Odyssey = 91, - MDType_Unannounced_Unknown = 100, - MDType_Unannounced_WindowsMR = 101, - MDType_vridge = 110, - MDType_Huawei_Unknown = 120, - MDType_Huawei_VR2 = 121, - MDType_Huawei_EndOfRange = 129, - mdType_Valve_Unknown = 130, - mdType_Valve_Index = 131, - } - // // EMarketNotAllowedReasonFlags // @@ -555,6 +410,17 @@ namespace Steamworks ExitSoon_Night = 7, } + // + // EDurationControlOnlineState + // + internal enum DurationControlOnlineState : int + { + Invalid = 0, + Offline = 1, + Online = 2, + OnlineHighPri = 3, + } + // // EGameSearchErrorCode_t // @@ -672,7 +538,7 @@ namespace Steamworks // // EOverlayToStoreFlag // - internal enum OverlayToStoreFlag : int + public enum OverlayToStoreFlag : int { None = 0, AddToCart = 1, @@ -688,6 +554,37 @@ namespace Steamworks Modal = 1, } + // + // ECommunityProfileItemType + // + internal enum CommunityProfileItemType : int + { + AnimatedAvatar = 0, + AvatarFrame = 1, + ProfileModifier = 2, + ProfileBackground = 3, + MiniProfileBackground = 4, + } + + // + // ECommunityProfileItemProperty + // + internal enum CommunityProfileItemProperty : int + { + ImageSmall = 0, + ImageLarge = 1, + InternalName = 2, + Title = 3, + Description = 4, + AppID = 5, + TypeID = 6, + Class = 7, + MovieWebM = 8, + MovieMP4 = 9, + MovieWebMSmall = 10, + MovieMP4Small = 11, + } + // // EPersonaChange // @@ -740,6 +637,28 @@ namespace Steamworks MultipleLines = 1, } + // + // EFloatingGamepadTextInputMode + // + public enum TextInputMode : int + { + SingleLine = 0, + MultipleLines = 1, + Email = 2, + Numeric = 3, + } + + // + // ETextFilteringContext + // + public enum TextFilteringContext : int + { + Unknown = 0, + GameContent = 1, + Chat = 2, + Name = 3, + } + // // ECheckFileSignature // @@ -937,6 +856,26 @@ namespace Steamworks lose = 2, } + // + // ERemoteStorageLocalFileChange + // + internal enum RemoteStorageLocalFileChange : int + { + Invalid = 0, + FileUpdated = 1, + FileDeleted = 2, + } + + // + // ERemoteStorageFilePathType + // + internal enum RemoteStorageFilePathType : int + { + Invalid = 0, + Absolute = 1, + APIFilename = 2, + } + // // ELeaderboardDataRequest // @@ -964,28 +903,16 @@ namespace Steamworks ForceUpdate = 2, } - // - // ERegisterActivationCodeResult - // - internal enum RegisterActivationCodeResult : int - { - ResultOK = 0, - ResultFail = 1, - ResultAlreadyRegistered = 2, - ResultTimeout = 3, - AlreadyOwned = 4, - } - // // EP2PSessionError // public enum P2PSessionError : int { None = 0, - NotRunningApp = 1, NoRightsToApp = 2, - DestinationNotLoggedIn = 3, Timeout = 4, + NotRunningApp_DELETED = 1, + DestinationNotLoggedIn_DELETED = 3, Max = 5, } @@ -1067,6 +994,7 @@ namespace Steamworks Code304NotModified = 304, Code305UseProxy = 305, Code307TemporaryRedirect = 307, + Code308PermanentRedirect = 308, Code400BadRequest = 400, Code401Unauthorized = 401, Code402PaymentRequired = 402, @@ -1087,6 +1015,7 @@ namespace Steamworks Code417ExpectationFailed = 417, Code4xxUnknown = 418, Code429TooManyRequests = 429, + Code444ConnectionClosed = 444, Code500InternalServerError = 500, Code501NotImplemented = 501, Code502BadGateway = 502, @@ -1268,11 +1197,11 @@ namespace Steamworks XBoxOne_DPad_West = 140, XBoxOne_DPad_East = 141, XBoxOne_DPad_Move = 142, - XBoxOne_Reserved1 = 143, - XBoxOne_Reserved2 = 144, - XBoxOne_Reserved3 = 145, - XBoxOne_Reserved4 = 146, - XBoxOne_Reserved5 = 147, + XBoxOne_LeftGrip_Lower = 143, + XBoxOne_LeftGrip_Upper = 144, + XBoxOne_RightGrip_Lower = 145, + XBoxOne_RightGrip_Upper = 146, + XBoxOne_Share = 147, XBoxOne_Reserved6 = 148, XBoxOne_Reserved7 = 149, XBoxOne_Reserved8 = 150, @@ -1373,17 +1302,165 @@ namespace Steamworks Switch_LeftGrip_Upper = 245, Switch_RightGrip_Lower = 246, Switch_RightGrip_Upper = 247, - Switch_Reserved11 = 248, - Switch_Reserved12 = 249, - Switch_Reserved13 = 250, - Switch_Reserved14 = 251, + Switch_JoyConButton_N = 248, + Switch_JoyConButton_E = 249, + Switch_JoyConButton_S = 250, + Switch_JoyConButton_W = 251, Switch_Reserved15 = 252, Switch_Reserved16 = 253, Switch_Reserved17 = 254, Switch_Reserved18 = 255, Switch_Reserved19 = 256, Switch_Reserved20 = 257, - Count = 258, + PS5_X = 258, + PS5_Circle = 259, + PS5_Triangle = 260, + PS5_Square = 261, + PS5_LeftBumper = 262, + PS5_RightBumper = 263, + PS5_Option = 264, + PS5_Create = 265, + PS5_Mute = 266, + PS5_LeftPad_Touch = 267, + PS5_LeftPad_Swipe = 268, + PS5_LeftPad_Click = 269, + PS5_LeftPad_DPadNorth = 270, + PS5_LeftPad_DPadSouth = 271, + PS5_LeftPad_DPadWest = 272, + PS5_LeftPad_DPadEast = 273, + PS5_RightPad_Touch = 274, + PS5_RightPad_Swipe = 275, + PS5_RightPad_Click = 276, + PS5_RightPad_DPadNorth = 277, + PS5_RightPad_DPadSouth = 278, + PS5_RightPad_DPadWest = 279, + PS5_RightPad_DPadEast = 280, + PS5_CenterPad_Touch = 281, + PS5_CenterPad_Swipe = 282, + PS5_CenterPad_Click = 283, + PS5_CenterPad_DPadNorth = 284, + PS5_CenterPad_DPadSouth = 285, + PS5_CenterPad_DPadWest = 286, + PS5_CenterPad_DPadEast = 287, + PS5_LeftTrigger_Pull = 288, + PS5_LeftTrigger_Click = 289, + PS5_RightTrigger_Pull = 290, + PS5_RightTrigger_Click = 291, + PS5_LeftStick_Move = 292, + PS5_LeftStick_Click = 293, + PS5_LeftStick_DPadNorth = 294, + PS5_LeftStick_DPadSouth = 295, + PS5_LeftStick_DPadWest = 296, + PS5_LeftStick_DPadEast = 297, + PS5_RightStick_Move = 298, + PS5_RightStick_Click = 299, + PS5_RightStick_DPadNorth = 300, + PS5_RightStick_DPadSouth = 301, + PS5_RightStick_DPadWest = 302, + PS5_RightStick_DPadEast = 303, + PS5_DPad_North = 304, + PS5_DPad_South = 305, + PS5_DPad_West = 306, + PS5_DPad_East = 307, + PS5_Gyro_Move = 308, + PS5_Gyro_Pitch = 309, + PS5_Gyro_Yaw = 310, + PS5_Gyro_Roll = 311, + PS5_DPad_Move = 312, + PS5_LeftGrip = 313, + PS5_RightGrip = 314, + PS5_LeftFn = 315, + PS5_RightFn = 316, + PS5_Reserved5 = 317, + PS5_Reserved6 = 318, + PS5_Reserved7 = 319, + PS5_Reserved8 = 320, + PS5_Reserved9 = 321, + PS5_Reserved10 = 322, + PS5_Reserved11 = 323, + PS5_Reserved12 = 324, + PS5_Reserved13 = 325, + PS5_Reserved14 = 326, + PS5_Reserved15 = 327, + PS5_Reserved16 = 328, + PS5_Reserved17 = 329, + PS5_Reserved18 = 330, + PS5_Reserved19 = 331, + PS5_Reserved20 = 332, + SteamDeck_A = 333, + SteamDeck_B = 334, + SteamDeck_X = 335, + SteamDeck_Y = 336, + SteamDeck_L1 = 337, + SteamDeck_R1 = 338, + SteamDeck_Menu = 339, + SteamDeck_View = 340, + SteamDeck_LeftPad_Touch = 341, + SteamDeck_LeftPad_Swipe = 342, + SteamDeck_LeftPad_Click = 343, + SteamDeck_LeftPad_DPadNorth = 344, + SteamDeck_LeftPad_DPadSouth = 345, + SteamDeck_LeftPad_DPadWest = 346, + SteamDeck_LeftPad_DPadEast = 347, + SteamDeck_RightPad_Touch = 348, + SteamDeck_RightPad_Swipe = 349, + SteamDeck_RightPad_Click = 350, + SteamDeck_RightPad_DPadNorth = 351, + SteamDeck_RightPad_DPadSouth = 352, + SteamDeck_RightPad_DPadWest = 353, + SteamDeck_RightPad_DPadEast = 354, + SteamDeck_L2_SoftPull = 355, + SteamDeck_L2 = 356, + SteamDeck_R2_SoftPull = 357, + SteamDeck_R2 = 358, + SteamDeck_LeftStick_Move = 359, + SteamDeck_L3 = 360, + SteamDeck_LeftStick_DPadNorth = 361, + SteamDeck_LeftStick_DPadSouth = 362, + SteamDeck_LeftStick_DPadWest = 363, + SteamDeck_LeftStick_DPadEast = 364, + SteamDeck_LeftStick_Touch = 365, + SteamDeck_RightStick_Move = 366, + SteamDeck_R3 = 367, + SteamDeck_RightStick_DPadNorth = 368, + SteamDeck_RightStick_DPadSouth = 369, + SteamDeck_RightStick_DPadWest = 370, + SteamDeck_RightStick_DPadEast = 371, + SteamDeck_RightStick_Touch = 372, + SteamDeck_L4 = 373, + SteamDeck_R4 = 374, + SteamDeck_L5 = 375, + SteamDeck_R5 = 376, + SteamDeck_DPad_Move = 377, + SteamDeck_DPad_North = 378, + SteamDeck_DPad_South = 379, + SteamDeck_DPad_West = 380, + SteamDeck_DPad_East = 381, + SteamDeck_Gyro_Move = 382, + SteamDeck_Gyro_Pitch = 383, + SteamDeck_Gyro_Yaw = 384, + SteamDeck_Gyro_Roll = 385, + SteamDeck_Reserved1 = 386, + SteamDeck_Reserved2 = 387, + SteamDeck_Reserved3 = 388, + SteamDeck_Reserved4 = 389, + SteamDeck_Reserved5 = 390, + SteamDeck_Reserved6 = 391, + SteamDeck_Reserved7 = 392, + SteamDeck_Reserved8 = 393, + SteamDeck_Reserved9 = 394, + SteamDeck_Reserved10 = 395, + SteamDeck_Reserved11 = 396, + SteamDeck_Reserved12 = 397, + SteamDeck_Reserved13 = 398, + SteamDeck_Reserved14 = 399, + SteamDeck_Reserved15 = 400, + SteamDeck_Reserved16 = 401, + SteamDeck_Reserved17 = 402, + SteamDeck_Reserved18 = 403, + SteamDeck_Reserved19 = 404, + SteamDeck_Reserved20 = 405, + Count = 406, MaximumPossibleValue = 32767, } @@ -1432,6 +1509,26 @@ namespace Steamworks Right = 1, } + // + // EControllerHapticLocation + // + internal enum ControllerHapticLocation : int + { + Left = 1, + Right = 2, + Both = 3, + } + + // + // EControllerHapticType + // + internal enum ControllerHapticType : int + { + Off = 0, + Tick = 1, + Click = 2, + } + // // ESteamInputType // @@ -1450,10 +1547,24 @@ namespace Steamworks SwitchProController = 10, MobileTouch = 11, PS3Controller = 12, - Count = 13, + PS5Controller = 13, + SteamDeckController = 14, + Count = 15, MaximumPossibleValue = 255, } + // + // ESteamInputConfigurationEnableType + // + internal enum SteamInputConfigurationEnableType : int + { + None = 0, + Playstation = 1, + Xbox = 2, + Generic = 4, + Switch = 8, + } + // // ESteamInputLEDFlag // @@ -1463,6 +1574,38 @@ namespace Steamworks RestoreUserDefault = 1, } + // + // ESteamInputGlyphSize + // + public enum GlyphSize : int + { + Small = 0, + Medium = 1, + Large = 2, + Count = 3, + } + + // + // ESteamInputGlyphStyle + // + internal enum SteamInputGlyphStyle : int + { + Knockout = 0, + Light = 1, + Dark = 2, + NeutralColorABXY = 16, + SolidABXY = 32, + } + + // + // ESteamInputActionEventType + // + internal enum SteamInputActionEventType : int + { + DigitalAction = 0, + AnalogAction = 1, + } + // // EControllerActionOrigin // @@ -1713,7 +1856,148 @@ namespace Steamworks XBoxOne_DPad_Move = 242, XBox360_DPad_Move = 243, Switch_DPad_Move = 244, - Count = 245, + PS5_X = 245, + PS5_Circle = 246, + PS5_Triangle = 247, + PS5_Square = 248, + PS5_LeftBumper = 249, + PS5_RightBumper = 250, + PS5_Option = 251, + PS5_Create = 252, + PS5_Mute = 253, + PS5_LeftPad_Touch = 254, + PS5_LeftPad_Swipe = 255, + PS5_LeftPad_Click = 256, + PS5_LeftPad_DPadNorth = 257, + PS5_LeftPad_DPadSouth = 258, + PS5_LeftPad_DPadWest = 259, + PS5_LeftPad_DPadEast = 260, + PS5_RightPad_Touch = 261, + PS5_RightPad_Swipe = 262, + PS5_RightPad_Click = 263, + PS5_RightPad_DPadNorth = 264, + PS5_RightPad_DPadSouth = 265, + PS5_RightPad_DPadWest = 266, + PS5_RightPad_DPadEast = 267, + PS5_CenterPad_Touch = 268, + PS5_CenterPad_Swipe = 269, + PS5_CenterPad_Click = 270, + PS5_CenterPad_DPadNorth = 271, + PS5_CenterPad_DPadSouth = 272, + PS5_CenterPad_DPadWest = 273, + PS5_CenterPad_DPadEast = 274, + PS5_LeftTrigger_Pull = 275, + PS5_LeftTrigger_Click = 276, + PS5_RightTrigger_Pull = 277, + PS5_RightTrigger_Click = 278, + PS5_LeftStick_Move = 279, + PS5_LeftStick_Click = 280, + PS5_LeftStick_DPadNorth = 281, + PS5_LeftStick_DPadSouth = 282, + PS5_LeftStick_DPadWest = 283, + PS5_LeftStick_DPadEast = 284, + PS5_RightStick_Move = 285, + PS5_RightStick_Click = 286, + PS5_RightStick_DPadNorth = 287, + PS5_RightStick_DPadSouth = 288, + PS5_RightStick_DPadWest = 289, + PS5_RightStick_DPadEast = 290, + PS5_DPad_Move = 291, + PS5_DPad_North = 292, + PS5_DPad_South = 293, + PS5_DPad_West = 294, + PS5_DPad_East = 295, + PS5_Gyro_Move = 296, + PS5_Gyro_Pitch = 297, + PS5_Gyro_Yaw = 298, + PS5_Gyro_Roll = 299, + XBoxOne_LeftGrip_Lower = 300, + XBoxOne_LeftGrip_Upper = 301, + XBoxOne_RightGrip_Lower = 302, + XBoxOne_RightGrip_Upper = 303, + XBoxOne_Share = 304, + SteamDeck_A = 305, + SteamDeck_B = 306, + SteamDeck_X = 307, + SteamDeck_Y = 308, + SteamDeck_L1 = 309, + SteamDeck_R1 = 310, + SteamDeck_Menu = 311, + SteamDeck_View = 312, + SteamDeck_LeftPad_Touch = 313, + SteamDeck_LeftPad_Swipe = 314, + SteamDeck_LeftPad_Click = 315, + SteamDeck_LeftPad_DPadNorth = 316, + SteamDeck_LeftPad_DPadSouth = 317, + SteamDeck_LeftPad_DPadWest = 318, + SteamDeck_LeftPad_DPadEast = 319, + SteamDeck_RightPad_Touch = 320, + SteamDeck_RightPad_Swipe = 321, + SteamDeck_RightPad_Click = 322, + SteamDeck_RightPad_DPadNorth = 323, + SteamDeck_RightPad_DPadSouth = 324, + SteamDeck_RightPad_DPadWest = 325, + SteamDeck_RightPad_DPadEast = 326, + SteamDeck_L2_SoftPull = 327, + SteamDeck_L2 = 328, + SteamDeck_R2_SoftPull = 329, + SteamDeck_R2 = 330, + SteamDeck_LeftStick_Move = 331, + SteamDeck_L3 = 332, + SteamDeck_LeftStick_DPadNorth = 333, + SteamDeck_LeftStick_DPadSouth = 334, + SteamDeck_LeftStick_DPadWest = 335, + SteamDeck_LeftStick_DPadEast = 336, + SteamDeck_LeftStick_Touch = 337, + SteamDeck_RightStick_Move = 338, + SteamDeck_R3 = 339, + SteamDeck_RightStick_DPadNorth = 340, + SteamDeck_RightStick_DPadSouth = 341, + SteamDeck_RightStick_DPadWest = 342, + SteamDeck_RightStick_DPadEast = 343, + SteamDeck_RightStick_Touch = 344, + SteamDeck_L4 = 345, + SteamDeck_R4 = 346, + SteamDeck_L5 = 347, + SteamDeck_R5 = 348, + SteamDeck_DPad_Move = 349, + SteamDeck_DPad_North = 350, + SteamDeck_DPad_South = 351, + SteamDeck_DPad_West = 352, + SteamDeck_DPad_East = 353, + SteamDeck_Gyro_Move = 354, + SteamDeck_Gyro_Pitch = 355, + SteamDeck_Gyro_Yaw = 356, + SteamDeck_Gyro_Roll = 357, + SteamDeck_Reserved1 = 358, + SteamDeck_Reserved2 = 359, + SteamDeck_Reserved3 = 360, + SteamDeck_Reserved4 = 361, + SteamDeck_Reserved5 = 362, + SteamDeck_Reserved6 = 363, + SteamDeck_Reserved7 = 364, + SteamDeck_Reserved8 = 365, + SteamDeck_Reserved9 = 366, + SteamDeck_Reserved10 = 367, + SteamDeck_Reserved11 = 368, + SteamDeck_Reserved12 = 369, + SteamDeck_Reserved13 = 370, + SteamDeck_Reserved14 = 371, + SteamDeck_Reserved15 = 372, + SteamDeck_Reserved16 = 373, + SteamDeck_Reserved17 = 374, + SteamDeck_Reserved18 = 375, + SteamDeck_Reserved19 = 376, + SteamDeck_Reserved20 = 377, + Switch_JoyConButton_N = 378, + Switch_JoyConButton_E = 379, + Switch_JoyConButton_S = 380, + Switch_JoyConButton_W = 381, + PS5_LeftGrip = 382, + PS5_RightGrip = 383, + PS5_LeftFn = 384, + PS5_RightFn = 385, + Count = 386, MaximumPossibleValue = 32767, } @@ -1801,6 +2085,7 @@ namespace Steamworks RankedByLifetimeAveragePlaytime = 16, RankedByPlaytimeSessionsTrend = 17, RankedByLifetimePlaytimeSessions = 18, + RankedByLastUpdatedDate = 19, } // @@ -1853,7 +2138,7 @@ namespace Steamworks // // EItemPreviewType // - internal enum ItemPreviewType : int + public enum ItemPreviewType : int { Image = 0, YouTubeVideo = 1, @@ -1863,6 +2148,18 @@ namespace Steamworks ReservedMax = 255, } + // + // EUGCContentDescriptorID + // + internal enum UGCContentDescriptorID : int + { + NudityOrSexualContent = 1, + FrequentViolenceOrGore = 2, + AdultOnlySexualContent = 3, + GratuitousSexualContent = 4, + AnyMatureContent = 5, + } + // // ESteamItemFlags // @@ -1873,17 +2170,6 @@ namespace Steamworks Consumed = 512, } - // - // ESteamTVRegionBehavior - // - internal enum SteamTVRegionBehavior : int - { - Invalid = -1, - Hover = 0, - ClickPopup = 1, - ClickSurroundingRegion = 2, - } - // // EParentalFeature // @@ -1903,7 +2189,8 @@ namespace Steamworks Library = 11, Test = 12, SiteLicense = 13, - Max = 14, + KioskMode = 14, + Max = 15, } // @@ -1943,6 +2230,8 @@ namespace Steamworks Invalid = 0, SteamID = 16, XboxPairwiseID = 17, + SonyPSN = 18, + GoogleStadia = 19, IPAddress = 1, GenericString = 2, GenericBytes = 3, @@ -1950,6 +2239,17 @@ namespace Steamworks Force32bit = 2147483647, } + // + // ESteamNetworkingFakeIPType + // + internal enum SteamNetworkingFakeIPType : int + { + Invalid = 0, + NotFake = 1, + GlobalIPv4 = 2, + LocalIPv4 = 3, + } + // // ESteamNetworkingConnectionState // @@ -1984,22 +2284,24 @@ namespace Steamworks Local_HostedServerPrimaryRelay = 3003, Local_NetworkConfig = 3004, Local_Rights = 3005, + Local_P2P_ICE_NoPublicAddresses = 3006, Local_Max = 3999, Remote_Min = 4000, Remote_Timeout = 4001, Remote_BadCrypt = 4002, Remote_BadCert = 4003, - Remote_NotLoggedIn = 4004, - Remote_NotRunningApp = 4005, Remote_BadProtocolVersion = 4006, + Remote_P2P_ICE_NoPublicAddresses = 4007, Remote_Max = 4999, Misc_Min = 5000, Misc_Generic = 5001, Misc_InternalError = 5002, Misc_Timeout = 5003, - Misc_RelayConnectivity = 5004, Misc_SteamConnectivity = 5005, Misc_NoRelaySessionsToClient = 5006, + Misc_P2P_Rendezvous = 5008, + Misc_P2P_NAT_Firewall = 5009, + Misc_PeerSentNoConnection = 5010, Misc_Max = 5999, } @@ -2023,7 +2325,7 @@ namespace Steamworks Int64 = 2, Float = 3, String = 4, - FunctionPtr = 5, + Ptr = 5, } // @@ -2032,6 +2334,21 @@ namespace Steamworks internal enum NetConfig : int { Invalid = 0, + TimeoutInitial = 24, + TimeoutConnected = 25, + SendBufferSize = 9, + ConnectionUserData = 40, + SendRateMin = 10, + SendRateMax = 11, + NagleTime = 12, + IP_AllowWithoutAuth = 23, + MTU_PacketSize = 32, + MTU_DataSize = 33, + Unencrypted = 34, + SymmetricConnect = 37, + LocalVirtualPort = 38, + DualWifi_Enable = 39, + EnableDiagnosticsUI = 46, FakePacketLoss_Send = 2, FakePacketLoss_Recv = 3, FakePacketLag_Send = 4, @@ -2042,17 +2359,26 @@ namespace Steamworks FakePacketDup_Send = 26, FakePacketDup_Recv = 27, FakePacketDup_TimeMax = 28, - TimeoutInitial = 24, - TimeoutConnected = 25, - SendBufferSize = 9, - SendRateMin = 10, - SendRateMax = 11, - NagleTime = 12, - IP_AllowWithoutAuth = 23, - MTU_PacketSize = 32, - MTU_DataSize = 33, - Unencrypted = 34, - EnumerateDevVars = 35, + PacketTraceMaxBytes = 41, + FakeRateLimit_Send_Rate = 42, + FakeRateLimit_Send_Burst = 43, + FakeRateLimit_Recv_Rate = 44, + FakeRateLimit_Recv_Burst = 45, + Callback_ConnectionStatusChanged = 201, + Callback_AuthStatusChanged = 202, + Callback_RelayNetworkStatusChanged = 203, + Callback_MessagesSessionRequest = 204, + Callback_MessagesSessionFailed = 205, + Callback_CreateConnectionSignaling = 206, + Callback_FakeIPResult = 207, + P2P_STUN_ServerList = 103, + P2P_Transport_ICE_Enable = 104, + P2P_Transport_ICE_Penalty = 105, + P2P_Transport_SDR_Penalty = 106, + P2P_TURN_ServerList = 107, + P2P_TURN_UserList = 108, + P2P_TURN_PassList = 109, + P2P_Transport_ICE_Implementation = 110, SDRClient_ConsecutitivePingTimeoutsFailInitial = 19, SDRClient_ConsecutitivePingTimeoutsFail = 20, SDRClient_MinPingsBeforePingAccurate = 21, @@ -2067,6 +2393,7 @@ namespace Steamworks LogLevel_PacketGaps = 16, LogLevel_P2PRendezvous = 17, LogLevel_SDRRelayPings = 18, + DELETED_EnumerateDevVars = 35, } // diff --git a/Libraries/Facepunch.Steamworks/Generated/SteamStructFunctions.cs b/Libraries/Facepunch.Steamworks/Generated/SteamStructFunctions.cs index 3f802a060..0ce830172 100644 --- a/Libraries/Facepunch.Steamworks/Generated/SteamStructFunctions.cs +++ b/Libraries/Facepunch.Steamworks/Generated/SteamStructFunctions.cs @@ -88,6 +88,25 @@ namespace Steamworks.Data } + internal partial struct NetKeyValue + { + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingConfigValue_t_SetInt32", CallingConvention = Platform.CC)] + internal static extern void InternalSetInt32( ref NetKeyValue self, NetConfig eVal, int data ); + + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingConfigValue_t_SetInt64", CallingConvention = Platform.CC)] + internal static extern void InternalSetInt64( ref NetKeyValue self, NetConfig eVal, long data ); + + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingConfigValue_t_SetFloat", CallingConvention = Platform.CC)] + internal static extern void InternalSetFloat( ref NetKeyValue self, NetConfig eVal, float data ); + + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingConfigValue_t_SetPtr", CallingConvention = Platform.CC)] + internal static extern void InternalSetPtr( ref NetKeyValue self, NetConfig eVal, IntPtr data ); + + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingConfigValue_t_SetString", CallingConvention = Platform.CC)] + internal static extern void InternalSetString( ref NetKeyValue self, NetConfig eVal, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string data ); + + } + public partial struct NetIdentity { [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingIdentity_Clear", CallingConvention = Platform.CC)] @@ -116,12 +135,37 @@ namespace Steamworks.Data [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingIdentity_GetXboxPairwiseID", CallingConvention = Platform.CC)] internal static extern Utf8StringPointer InternalGetXboxPairwiseID( ref NetIdentity self ); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingIdentity_SetPSNID", CallingConvention = Platform.CC)] + internal static extern void InternalSetPSNID( ref NetIdentity self, ulong id ); + + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingIdentity_GetPSNID", CallingConvention = Platform.CC)] + internal static extern ulong InternalGetPSNID( ref NetIdentity self ); + + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingIdentity_SetStadiaID", CallingConvention = Platform.CC)] + internal static extern void InternalSetStadiaID( ref NetIdentity self, ulong id ); + + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingIdentity_GetStadiaID", CallingConvention = Platform.CC)] + internal static extern ulong InternalGetStadiaID( ref NetIdentity self ); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingIdentity_SetIPAddr", CallingConvention = Platform.CC)] internal static extern void InternalSetIPAddr( ref NetIdentity self, ref NetAddress addr ); [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingIdentity_GetIPAddr", CallingConvention = Platform.CC)] internal static extern IntPtr InternalGetIPAddr( ref NetIdentity self ); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingIdentity_SetIPv4Addr", CallingConvention = Platform.CC)] + internal static extern void InternalSetIPv4Addr( ref NetIdentity self, uint nIPv4, ushort nPort ); + + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingIdentity_GetIPv4", CallingConvention = Platform.CC)] + internal static extern uint InternalGetIPv4( ref NetIdentity self ); + + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingIdentity_GetFakeIPType", CallingConvention = Platform.CC)] + internal static extern SteamNetworkingFakeIPType InternalGetFakeIPType( ref NetIdentity self ); + + [return: MarshalAs( UnmanagedType.I1 )] + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingIdentity_IsFakeIP", CallingConvention = Platform.CC)] + internal static extern bool InternalIsFakeIP( ref NetIdentity self ); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingIdentity_SetLocalHost", CallingConvention = Platform.CC)] internal static extern void InternalSetLocalHost( ref NetIdentity self ); @@ -196,6 +240,13 @@ namespace Steamworks.Data [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingIPAddr_IsEqualTo", CallingConvention = Platform.CC)] internal static extern bool InternalIsEqualTo( ref NetAddress self, ref NetAddress x ); + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingIPAddr_GetFakeIPType", CallingConvention = Platform.CC)] + internal static extern SteamNetworkingFakeIPType InternalGetFakeIPType( ref NetAddress self ); + + [return: MarshalAs( UnmanagedType.I1 )] + [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_SteamNetworkingIPAddr_IsFakeIP", CallingConvention = Platform.CC)] + internal static extern bool InternalIsFakeIP( ref NetAddress self ); + } internal partial struct NetMsg diff --git a/Libraries/Facepunch.Steamworks/Generated/SteamStructs.cs b/Libraries/Facepunch.Steamworks/Generated/SteamStructs.cs index e3bc0a785..8724de68a 100644 --- a/Libraries/Facepunch.Steamworks/Generated/SteamStructs.cs +++ b/Libraries/Facepunch.Steamworks/Generated/SteamStructs.cs @@ -17,14 +17,6 @@ namespace Steamworks.Data } - [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] - internal struct FriendSessionStateInfo_t - { - internal uint IOnlineSessionInstances; // m_uiOnlineSessionInstances uint32 - internal byte IPublishedToFriendsSessionInstance; // m_uiPublishedToFriendsSessionInstance uint8 - - } - [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] internal partial struct servernetadr_t { @@ -113,6 +105,39 @@ namespace Steamworks.Data } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct InputMotionDataV2_t + { + internal float DriftCorrectedQuatX; // driftCorrectedQuatX float + internal float DriftCorrectedQuatY; // driftCorrectedQuatY float + internal float DriftCorrectedQuatZ; // driftCorrectedQuatZ float + internal float DriftCorrectedQuatW; // driftCorrectedQuatW float + internal float SensorFusionQuatX; // sensorFusionQuatX float + internal float SensorFusionQuatY; // sensorFusionQuatY float + internal float SensorFusionQuatZ; // sensorFusionQuatZ float + internal float SensorFusionQuatW; // sensorFusionQuatW float + internal float DeferredSensorFusionQuatX; // deferredSensorFusionQuatX float + internal float DeferredSensorFusionQuatY; // deferredSensorFusionQuatY float + internal float DeferredSensorFusionQuatZ; // deferredSensorFusionQuatZ float + internal float DeferredSensorFusionQuatW; // deferredSensorFusionQuatW float + internal float GravityX; // gravityX float + internal float GravityY; // gravityY float + internal float GravityZ; // gravityZ float + internal float DegreesPerSecondX; // degreesPerSecondX float + internal float DegreesPerSecondY; // degreesPerSecondY float + internal float DegreesPerSecondZ; // degreesPerSecondZ float + + } + + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + internal struct SteamInputActionEvent_t + { + internal ulong ControllerHandle; // controllerHandle InputHandle_t + internal SteamInputActionEventType EEventType; // eEventType ESteamInputActionEventType + // internal SteamInputActionEvent_t.AnalogAction_t AnalogAction; // analogAction SteamInputActionEvent_t::AnalogAction_t + + } + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] internal struct SteamUGCDetails_t { @@ -168,37 +193,6 @@ namespace Steamworks.Data } - [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] - internal struct SteamTVRegion_t - { - internal uint UnMinX; // unMinX uint32 - internal uint UnMinY; // unMinY uint32 - internal uint UnMaxX; // unMaxX uint32 - internal uint UnMaxY; // unMaxY uint32 - - } - - [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] - internal struct SteamNetworkingQuickConnectionStatus - { - internal ConnectionState State; // m_eState ESteamNetworkingConnectionState - internal int Ping; // m_nPing int - internal float ConnectionQualityLocal; // m_flConnectionQualityLocal float - internal float ConnectionQualityRemote; // m_flConnectionQualityRemote float - internal float OutPacketsPerSec; // m_flOutPacketsPerSec float - internal float OutBytesPerSec; // m_flOutBytesPerSec float - internal float InPacketsPerSec; // m_flInPacketsPerSec float - internal float InBytesPerSec; // m_flInBytesPerSec float - internal int SendRateBytesPerSecond; // m_nSendRateBytesPerSecond int - internal int CbPendingUnreliable; // m_cbPendingUnreliable int - internal int CbPendingReliable; // m_cbPendingReliable int - internal int CbSentUnackedReliable; // m_cbSentUnackedReliable int - internal long EcQueueTime; // m_usecQueueTime SteamNetworkingMicroseconds - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16, ArraySubType = UnmanagedType.U4)] - internal uint[] Reserved; // reserved uint32 [16] - - } - [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] internal partial struct SteamDatagramHostedAddress { diff --git a/Libraries/Facepunch.Steamworks/Generated/SteamTypes.cs b/Libraries/Facepunch.Steamworks/Generated/SteamTypes.cs index 44f5be6e3..5f5085f04 100644 --- a/Libraries/Facepunch.Steamworks/Generated/SteamTypes.cs +++ b/Libraries/Facepunch.Steamworks/Generated/SteamTypes.cs @@ -6,118 +6,6 @@ using System.Threading.Tasks; namespace Steamworks.Data { - internal struct GID_t : IEquatable, IComparable - { - // Name: GID_t, Type: unsigned long long - public ulong Value; - - public static implicit operator GID_t( ulong value ) => new GID_t(){ Value = value }; - public static implicit operator ulong( GID_t value ) => value.Value; - public override string ToString() => Value.ToString(); - public override int GetHashCode() => Value.GetHashCode(); - public override bool Equals( object p ) => this.Equals( (GID_t) p ); - public bool Equals( GID_t p ) => p.Value == Value; - public static bool operator ==( GID_t a, GID_t b ) => a.Equals( b ); - public static bool operator !=( GID_t a, GID_t b ) => !a.Equals( b ); - public int CompareTo( GID_t other ) => Value.CompareTo( other.Value ); - } - - internal struct JobID_t : IEquatable, IComparable - { - // Name: JobID_t, Type: unsigned long long - public ulong Value; - - public static implicit operator JobID_t( ulong value ) => new JobID_t(){ Value = value }; - public static implicit operator ulong( JobID_t value ) => value.Value; - public override string ToString() => Value.ToString(); - public override int GetHashCode() => Value.GetHashCode(); - public override bool Equals( object p ) => this.Equals( (JobID_t) p ); - public bool Equals( JobID_t p ) => p.Value == Value; - public static bool operator ==( JobID_t a, JobID_t b ) => a.Equals( b ); - public static bool operator !=( JobID_t a, JobID_t b ) => !a.Equals( b ); - public int CompareTo( JobID_t other ) => Value.CompareTo( other.Value ); - } - - internal struct TxnID_t : IEquatable, IComparable - { - // Name: TxnID_t, Type: unsigned long long - public ulong Value; - - public static implicit operator TxnID_t( ulong value ) => new TxnID_t(){ Value = value }; - public static implicit operator ulong( TxnID_t value ) => value.Value; - public override string ToString() => Value.ToString(); - public override int GetHashCode() => Value.GetHashCode(); - public override bool Equals( object p ) => this.Equals( (TxnID_t) p ); - public bool Equals( TxnID_t p ) => p.Value == Value; - public static bool operator ==( TxnID_t a, TxnID_t b ) => a.Equals( b ); - public static bool operator !=( TxnID_t a, TxnID_t b ) => !a.Equals( b ); - public int CompareTo( TxnID_t other ) => Value.CompareTo( other.Value ); - } - - internal struct PackageId_t : IEquatable, IComparable - { - // Name: PackageId_t, Type: unsigned int - public uint Value; - - public static implicit operator PackageId_t( uint value ) => new PackageId_t(){ Value = value }; - public static implicit operator uint( PackageId_t value ) => value.Value; - public override string ToString() => Value.ToString(); - public override int GetHashCode() => Value.GetHashCode(); - public override bool Equals( object p ) => this.Equals( (PackageId_t) p ); - public bool Equals( PackageId_t p ) => p.Value == Value; - public static bool operator ==( PackageId_t a, PackageId_t b ) => a.Equals( b ); - public static bool operator !=( PackageId_t a, PackageId_t b ) => !a.Equals( b ); - public int CompareTo( PackageId_t other ) => Value.CompareTo( other.Value ); - } - - internal struct BundleId_t : IEquatable, IComparable - { - // Name: BundleId_t, Type: unsigned int - public uint Value; - - public static implicit operator BundleId_t( uint value ) => new BundleId_t(){ Value = value }; - public static implicit operator uint( BundleId_t value ) => value.Value; - public override string ToString() => Value.ToString(); - public override int GetHashCode() => Value.GetHashCode(); - public override bool Equals( object p ) => this.Equals( (BundleId_t) p ); - public bool Equals( BundleId_t p ) => p.Value == Value; - public static bool operator ==( BundleId_t a, BundleId_t b ) => a.Equals( b ); - public static bool operator !=( BundleId_t a, BundleId_t b ) => !a.Equals( b ); - public int CompareTo( BundleId_t other ) => Value.CompareTo( other.Value ); - } - - internal struct AssetClassId_t : IEquatable, IComparable - { - // Name: AssetClassId_t, Type: unsigned long long - public ulong Value; - - public static implicit operator AssetClassId_t( ulong value ) => new AssetClassId_t(){ Value = value }; - public static implicit operator ulong( AssetClassId_t value ) => value.Value; - public override string ToString() => Value.ToString(); - public override int GetHashCode() => Value.GetHashCode(); - public override bool Equals( object p ) => this.Equals( (AssetClassId_t) p ); - public bool Equals( AssetClassId_t p ) => p.Value == Value; - public static bool operator ==( AssetClassId_t a, AssetClassId_t b ) => a.Equals( b ); - public static bool operator !=( AssetClassId_t a, AssetClassId_t b ) => !a.Equals( b ); - public int CompareTo( AssetClassId_t other ) => Value.CompareTo( other.Value ); - } - - internal struct PhysicalItemId_t : IEquatable, IComparable - { - // Name: PhysicalItemId_t, Type: unsigned int - public uint Value; - - public static implicit operator PhysicalItemId_t( uint value ) => new PhysicalItemId_t(){ Value = value }; - public static implicit operator uint( PhysicalItemId_t value ) => value.Value; - public override string ToString() => Value.ToString(); - public override int GetHashCode() => Value.GetHashCode(); - public override bool Equals( object p ) => this.Equals( (PhysicalItemId_t) p ); - public bool Equals( PhysicalItemId_t p ) => p.Value == Value; - public static bool operator ==( PhysicalItemId_t a, PhysicalItemId_t b ) => a.Equals( b ); - public static bool operator !=( PhysicalItemId_t a, PhysicalItemId_t b ) => !a.Equals( b ); - public int CompareTo( PhysicalItemId_t other ) => Value.CompareTo( other.Value ); - } - internal struct DepotId_t : IEquatable, IComparable { // Name: DepotId_t, Type: unsigned int @@ -150,22 +38,6 @@ namespace Steamworks.Data public int CompareTo( RTime32 other ) => Value.CompareTo( other.Value ); } - internal struct CellID_t : IEquatable, IComparable - { - // Name: CellID_t, Type: unsigned int - public uint Value; - - public static implicit operator CellID_t( uint value ) => new CellID_t(){ Value = value }; - public static implicit operator uint( CellID_t value ) => value.Value; - public override string ToString() => Value.ToString(); - public override int GetHashCode() => Value.GetHashCode(); - public override bool Equals( object p ) => this.Equals( (CellID_t) p ); - public bool Equals( CellID_t p ) => p.Value == Value; - public static bool operator ==( CellID_t a, CellID_t b ) => a.Equals( b ); - public static bool operator !=( CellID_t a, CellID_t b ) => !a.Equals( b ); - public int CompareTo( CellID_t other ) => Value.CompareTo( other.Value ); - } - internal struct SteamAPICall_t : IEquatable, IComparable { // Name: SteamAPICall_t, Type: unsigned long long @@ -198,54 +70,6 @@ namespace Steamworks.Data public int CompareTo( AccountID_t other ) => Value.CompareTo( other.Value ); } - internal struct PartnerId_t : IEquatable, IComparable - { - // Name: PartnerId_t, Type: unsigned int - public uint Value; - - public static implicit operator PartnerId_t( uint value ) => new PartnerId_t(){ Value = value }; - public static implicit operator uint( PartnerId_t value ) => value.Value; - public override string ToString() => Value.ToString(); - public override int GetHashCode() => Value.GetHashCode(); - public override bool Equals( object p ) => this.Equals( (PartnerId_t) p ); - public bool Equals( PartnerId_t p ) => p.Value == Value; - public static bool operator ==( PartnerId_t a, PartnerId_t b ) => a.Equals( b ); - public static bool operator !=( PartnerId_t a, PartnerId_t b ) => !a.Equals( b ); - public int CompareTo( PartnerId_t other ) => Value.CompareTo( other.Value ); - } - - internal struct ManifestId_t : IEquatable, IComparable - { - // Name: ManifestId_t, Type: unsigned long long - public ulong Value; - - public static implicit operator ManifestId_t( ulong value ) => new ManifestId_t(){ Value = value }; - public static implicit operator ulong( ManifestId_t value ) => value.Value; - public override string ToString() => Value.ToString(); - public override int GetHashCode() => Value.GetHashCode(); - public override bool Equals( object p ) => this.Equals( (ManifestId_t) p ); - public bool Equals( ManifestId_t p ) => p.Value == Value; - public static bool operator ==( ManifestId_t a, ManifestId_t b ) => a.Equals( b ); - public static bool operator !=( ManifestId_t a, ManifestId_t b ) => !a.Equals( b ); - public int CompareTo( ManifestId_t other ) => Value.CompareTo( other.Value ); - } - - internal struct SiteId_t : IEquatable, IComparable - { - // Name: SiteId_t, Type: unsigned long long - public ulong Value; - - public static implicit operator SiteId_t( ulong value ) => new SiteId_t(){ Value = value }; - public static implicit operator ulong( SiteId_t value ) => value.Value; - public override string ToString() => Value.ToString(); - public override int GetHashCode() => Value.GetHashCode(); - public override bool Equals( object p ) => this.Equals( (SiteId_t) p ); - public bool Equals( SiteId_t p ) => p.Value == Value; - public static bool operator ==( SiteId_t a, SiteId_t b ) => a.Equals( b ); - public static bool operator !=( SiteId_t a, SiteId_t b ) => !a.Equals( b ); - public int CompareTo( SiteId_t other ) => Value.CompareTo( other.Value ); - } - internal struct PartyBeaconID_t : IEquatable, IComparable { // Name: PartyBeaconID_t, Type: unsigned long long @@ -278,22 +102,6 @@ namespace Steamworks.Data public int CompareTo( HAuthTicket other ) => Value.CompareTo( other.Value ); } - internal struct BREAKPAD_HANDLE : IEquatable, IComparable - { - // Name: BREAKPAD_HANDLE, Type: void * - public IntPtr Value; - - public static implicit operator BREAKPAD_HANDLE( IntPtr value ) => new BREAKPAD_HANDLE(){ Value = value }; - public static implicit operator IntPtr( BREAKPAD_HANDLE value ) => value.Value; - public override string ToString() => Value.ToString(); - public override int GetHashCode() => Value.GetHashCode(); - public override bool Equals( object p ) => this.Equals( (BREAKPAD_HANDLE) p ); - public bool Equals( BREAKPAD_HANDLE p ) => p.Value == Value; - public static bool operator ==( BREAKPAD_HANDLE a, BREAKPAD_HANDLE b ) => a.Equals( b ); - public static bool operator !=( BREAKPAD_HANDLE a, BREAKPAD_HANDLE b ) => !a.Equals( b ); - public int CompareTo( BREAKPAD_HANDLE other ) => Value.ToInt64().CompareTo( other.Value.ToInt64() ); - } - internal struct HSteamPipe : IEquatable, IComparable { // Name: HSteamPipe, Type: int diff --git a/Libraries/Facepunch.Steamworks/Networking/BroadcastBufferManager.cs b/Libraries/Facepunch.Steamworks/Networking/BroadcastBufferManager.cs new file mode 100644 index 000000000..ba0ccc2b9 --- /dev/null +++ b/Libraries/Facepunch.Steamworks/Networking/BroadcastBufferManager.cs @@ -0,0 +1,282 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; +using Steamworks.Data; + +namespace Steamworks +{ + internal static unsafe class BufferManager + { + private sealed class ReferenceCounter + { + public IntPtr Pointer { get; private set; } + public int Size { get; private set; } + private int _count; + + public void Set( IntPtr ptr, int size, int referenceCount ) + { + if ( ptr == IntPtr.Zero ) + throw new ArgumentNullException( nameof( ptr ) ); + if ( size <= 0 ) + throw new ArgumentOutOfRangeException( nameof( size ) ); + if ( referenceCount <= 0 ) + throw new ArgumentOutOfRangeException( nameof( referenceCount ) ); + + Pointer = ptr; + Size = size; + + var prevCount = Interlocked.Exchange(ref _count, referenceCount); + if (prevCount != 0) + { +#if DEBUG + SteamNetworkingUtils.LogDebugMessage( NetDebugOutput.Warning, $"{nameof( BufferManager )} set reference count when current count was not 0" ); +#endif + } + } + + public bool Decrement() + { + var newCount = Interlocked.Decrement( ref _count ); + if ( newCount < 0 ) + { + SteamNetworkingUtils.LogDebugMessage( NetDebugOutput.Bug, $"Prevented double free of {nameof(BufferManager)} pointer" ); + return false; + } + + return newCount == 0; + } + } + + [UnmanagedFunctionPointer( CallingConvention.Cdecl )] + private delegate void FreeFn( NetMsg* msg ); + + private static readonly Stack ReferenceCounterPool = + new Stack( 1024 ); + + private static readonly Dictionary> BufferPools = + new Dictionary>(); + + private static readonly Dictionary ReferenceCounters = + new Dictionary( 1024 ); + + private static readonly FreeFn FreeFunctionPin = new FreeFn( Free ); + + public static readonly IntPtr FreeFunctionPointer = Marshal.GetFunctionPointerForDelegate( FreeFunctionPin ); + + public static IntPtr Get( int size, int referenceCount ) + { + const int maxSize = 16 * 1024 * 1024; + if ( size < 0 || size > maxSize ) + throw new ArgumentOutOfRangeException( nameof( size ) ); + if ( referenceCount <= 0 ) + throw new ArgumentOutOfRangeException( nameof( referenceCount ) ); + + AllocateBuffer( size, out var ptr, out var actualSize ); + var counter = AllocateReferenceCounter( ptr, actualSize, referenceCount ); + +#if DEBUG + SteamNetworkingUtils.LogDebugMessage( NetDebugOutput.Verbose, + $"{nameof( BufferManager )} allocated {ptr.ToInt64():X8} (size={size}, actualSize={actualSize}) with {referenceCount} references" ); +#endif + + lock ( ReferenceCounters ) + { + ReferenceCounters.Add( ptr, counter ); + } + + return ptr; + } + + [MonoPInvokeCallback] + private static void Free( NetMsg* msg ) + { + var ptr = msg->DataPtr; + + lock ( ReferenceCounters ) + { + if ( !ReferenceCounters.TryGetValue( ptr, out var counter ) ) + { + SteamNetworkingUtils.LogDebugMessage( NetDebugOutput.Bug, $"Attempt to free pointer not tracked by {nameof(BufferManager)}: {ptr.ToInt64():X8}" ); + return; + } + +#if DEBUG + SteamNetworkingUtils.LogDebugMessage( NetDebugOutput.Verbose, $"{nameof( BufferManager )} decrementing reference count of {ptr.ToInt64():X8}" ); +#endif + + if ( counter.Decrement() ) + { +#if DEBUG + SteamNetworkingUtils.LogDebugMessage( NetDebugOutput.Verbose, $"{nameof( BufferManager )} freeing {ptr.ToInt64():X8} as it is now unreferenced" ); + + if ( ptr != counter.Pointer ) + { + SteamNetworkingUtils.LogDebugMessage( NetDebugOutput.Bug, + $"{nameof( BufferManager )} freed pointer ({ptr.ToInt64():X8}) does not match counter pointer ({counter.Pointer.ToInt64():X8})" ); + } + + var bucketSize = GetBucketSize( counter.Size ); + if ( counter.Size != bucketSize ) + { + SteamNetworkingUtils.LogDebugMessage( NetDebugOutput.Bug, + $"{nameof( BufferManager )} freed pointer size ({counter.Size}) does not match bucket size ({bucketSize})" ); + } +#endif + + ReferenceCounters.Remove( ptr ); + FreeBuffer( ptr, counter.Size ); + FreeReferenceCounter( counter ); + } + } + } + + private static ReferenceCounter AllocateReferenceCounter( IntPtr ptr, int size, int referenceCount ) + { + lock ( ReferenceCounterPool ) + { + var counter = ReferenceCounterPool.Count > 0 + ? ReferenceCounterPool.Pop() + : new ReferenceCounter(); + + counter.Set( ptr, size, referenceCount ); + return counter; + } + } + + private static void FreeReferenceCounter( ReferenceCounter counter ) + { + if ( counter == null ) + throw new ArgumentNullException( nameof( counter ) ); + + lock ( ReferenceCounterPool ) + { + if ( ReferenceCounterPool.Count >= 1024 ) + { + // we don't want to keep a ton of these lying around - let it GC if we have too many + return; + } + + ReferenceCounterPool.Push( counter ); + } + } + + private static void AllocateBuffer( int minimumSize, out IntPtr ptr, out int size ) + { + var bucketSize = GetBucketSize( minimumSize ); + + if ( bucketSize <= 0 ) + { + // not bucketed, no pooling for this size + ptr = Marshal.AllocHGlobal( minimumSize ); + size = minimumSize; + +#if DEBUG + SteamNetworkingUtils.LogDebugMessage( NetDebugOutput.Verbose, + $"{nameof( BufferManager )} allocated unpooled pointer {ptr.ToInt64():X8} (size={size})" ); +#endif + return; + } + + lock ( BufferPools ) + { + if ( !BufferPools.TryGetValue( bucketSize, out var bucketPool ) || bucketPool.Count == 0 ) + { + // nothing pooled yet, but we can pool this size + ptr = Marshal.AllocHGlobal( bucketSize ); + size = bucketSize; + +#if DEBUG + SteamNetworkingUtils.LogDebugMessage( NetDebugOutput.Verbose, + $"{nameof( BufferManager )} allocated new poolable pointer {ptr.ToInt64():X8} (size={size})" ); +#endif + return; + } + + ptr = bucketPool.Pop(); + size = bucketSize; +#if DEBUG + SteamNetworkingUtils.LogDebugMessage( NetDebugOutput.Verbose, + $"{nameof( BufferManager )} allocated pointer from pool {ptr.ToInt64():X8} (size={size})" ); +#endif + } + } + + private static void FreeBuffer( IntPtr ptr, int size ) + { + var bucketSize = GetBucketSize( size ); + var bucketLimit = GetBucketLimit( size ); + + if ( bucketSize <= 0 || bucketLimit <= 0 ) + { + // not bucketed, no pooling for this size + Marshal.FreeHGlobal( ptr ); + +#if DEBUG + SteamNetworkingUtils.LogDebugMessage( NetDebugOutput.Verbose, + $"{nameof( BufferManager )} freed unpooled pointer {ptr.ToInt64():X8} (size={size})" ); +#endif + return; + } + + lock ( BufferPools ) + { + if ( !BufferPools.TryGetValue( bucketSize, out var bucketPool ) ) + { + bucketPool = new Stack( bucketLimit ); + BufferPools.Add( bucketSize, bucketPool ); + } + + if ( bucketPool.Count >= bucketLimit ) + { + // pool overflow, get rid + Marshal.FreeHGlobal( ptr ); + +#if DEBUG + SteamNetworkingUtils.LogDebugMessage( NetDebugOutput.Verbose, + $"{nameof( BufferManager )} pool overflow, freed pooled pointer {ptr.ToInt64():X8} (size={size})" ); +#endif + return; + } + + bucketPool.Push( ptr ); + +#if DEBUG + SteamNetworkingUtils.LogDebugMessage( NetDebugOutput.Verbose, + $"{nameof( BufferManager )} returned pointer to pool {ptr.ToInt64():X8} (size={size})" ); +#endif + } + } + + private const int Bucket512 = 512; + private const int Bucket1Kb = 1 * 1024; + private const int Bucket4Kb = 4 * 1024; + private const int Bucket16Kb = 16 * 1024; + private const int Bucket64Kb = 64 * 1024; + private const int Bucket256Kb = 256 * 1024; + + private static int GetBucketSize( int size ) + { + if ( size <= Bucket512 ) return Bucket512; + if ( size <= Bucket1Kb ) return Bucket1Kb; + if ( size <= Bucket4Kb ) return Bucket4Kb; + if ( size <= Bucket16Kb ) return Bucket16Kb; + if ( size <= Bucket64Kb ) return Bucket64Kb; + if ( size <= Bucket256Kb ) return Bucket256Kb; + + return -1; + } + + private static int GetBucketLimit( int size ) + { + if ( size <= Bucket512 ) return 1024; + if ( size <= Bucket1Kb ) return 512; + if ( size <= Bucket4Kb ) return 128; + if ( size <= Bucket16Kb ) return 32; + if ( size <= Bucket64Kb ) return 16; + if ( size <= Bucket256Kb ) return 8; + + return -1; + } + } +} diff --git a/Libraries/Facepunch.Steamworks/Networking/Connection.cs b/Libraries/Facepunch.Steamworks/Networking/Connection.cs index 0eb4aafce..d1069a693 100644 --- a/Libraries/Facepunch.Steamworks/Networking/Connection.cs +++ b/Libraries/Facepunch.Steamworks/Networking/Connection.cs @@ -11,13 +11,18 @@ namespace Steamworks.Data /// You can override all the virtual functions to turn it into what you /// want it to do. /// - public struct Connection + public struct Connection : IEquatable { public uint Id { get; set; } + public bool Equals( Connection other ) => Id == other.Id; + public override bool Equals( object obj ) => obj is Connection other && Id == other.Id; + public override int GetHashCode() => Id.GetHashCode(); public override string ToString() => Id.ToString(); public static implicit operator Connection( uint value ) => new Connection() { Id = value }; public static implicit operator uint( Connection value ) => value.Id; + public static bool operator ==( Connection value1, Connection value2 ) => value1.Equals( value2 ); + public static bool operator !=( Connection value1, Connection value2 ) => !value1.Equals( value2 ); /// /// Accept an incoming connection that has been received on a listen socket. @@ -64,21 +69,41 @@ namespace Steamworks.Data /// /// This is the best version to use. /// - public Result SendMessage( IntPtr ptr, int size, SendType sendType = SendType.Reliable ) + public unsafe Result SendMessage( IntPtr ptr, int size, SendType sendType = SendType.Reliable, ushort laneIndex = 0 ) { + if ( ptr == IntPtr.Zero ) + throw new ArgumentNullException( nameof( ptr ) ); + if ( size == 0 ) + throw new ArgumentException( "`size` cannot be zero", nameof( size ) ); + + var copyPtr = BufferManager.Get( size, 1 ); + Buffer.MemoryCopy( (void*)ptr, (void*)copyPtr, size, size ); + + var message = SteamNetworkingUtils.AllocateMessage(); + message->Connection = this; + message->Flags = sendType; + message->DataPtr = copyPtr; + message->DataSize = size; + message->FreeDataPtr = BufferManager.FreeFunctionPointer; + message->IdxLane = laneIndex; + long messageNumber = 0; - return SteamNetworkingSockets.Internal?.SendMessageToConnection( this, ptr, (uint) size, (int)sendType, ref messageNumber ) ?? Result.Fail; + SteamNetworkingSockets.Internal?.SendMessages( 1, &message, &messageNumber ); + + return messageNumber >= 0 + ? Result.OK + : (Result)(-messageNumber); } /// /// Ideally should be using an IntPtr version unless you're being really careful with the byte[] array and /// you're not creating a new one every frame (like using .ToArray()) /// - public unsafe Result SendMessage( byte[] data, SendType sendType = SendType.Reliable ) + public unsafe Result SendMessage( byte[] data, SendType sendType = SendType.Reliable, ushort laneIndex = 0 ) { fixed ( byte* ptr = data ) { - return SendMessage( (IntPtr)ptr, data.Length, sendType ); + return SendMessage( (IntPtr)ptr, data.Length, sendType, laneIndex ); } } @@ -86,21 +111,21 @@ namespace Steamworks.Data /// Ideally should be using an IntPtr version unless you're being really careful with the byte[] array and /// you're not creating a new one every frame (like using .ToArray()) /// - public unsafe Result SendMessage( byte[] data, int offset, int length, SendType sendType = SendType.Reliable ) + public unsafe Result SendMessage( byte[] data, int offset, int length, SendType sendType = SendType.Reliable, ushort laneIndex = 0 ) { fixed ( byte* ptr = data ) { - return SendMessage( (IntPtr)ptr + offset, length, sendType ); + return SendMessage( (IntPtr)ptr + offset, length, sendType, laneIndex ); } } /// /// This creates a ton of garbage - so don't do anything with this beyond testing! /// - public unsafe Result SendMessage( string str, SendType sendType = SendType.Reliable ) + public unsafe Result SendMessage( string str, SendType sendType = SendType.Reliable, ushort laneIndex = 0 ) { var bytes = System.Text.Encoding.UTF8.GetBytes( str ); - return SendMessage( bytes, sendType ); + return SendMessage( bytes, sendType, laneIndex ); } /// @@ -121,5 +146,27 @@ namespace Steamworks.Data return strVal; } + + /// + /// Returns a small set of information about the real-time state of the connection. + /// + public ConnectionStatus QuickStatus() + { + ConnectionStatus connectionStatus = default( ConnectionStatus ); + + SteamNetworkingSockets.Internal?.GetConnectionRealTimeStatus( this, ref connectionStatus, 0, null ); + + return connectionStatus; + } + + /// + /// Configure multiple outbound messages streams ("lanes") on a connection, and + /// control head-of-line blocking between them. + /// + public Result ConfigureConnectionLanes( int[] lanePriorities, ushort[] laneWeights ) + { + return SteamNetworkingSockets.Internal?.ConfigureConnectionLanes( this, lanePriorities.Length, lanePriorities, laneWeights ) + ?? Result.Fail; + } } } diff --git a/Libraries/Facepunch.Steamworks/Networking/ConnectionLaneStatus.cs b/Libraries/Facepunch.Steamworks/Networking/ConnectionLaneStatus.cs new file mode 100644 index 000000000..a109542dc --- /dev/null +++ b/Libraries/Facepunch.Steamworks/Networking/ConnectionLaneStatus.cs @@ -0,0 +1,34 @@ +using System.Runtime.InteropServices; + +namespace Steamworks.Data +{ + /// + /// Describe the status of a connection + /// + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + public struct ConnectionLaneStatus + { + internal int cbPendingUnreliable; // m_cbPendingUnreliable int + internal int cbPendingReliable; // m_cbPendingReliable int + internal int cbSentUnackedReliable; // m_cbSentUnackedReliable int + internal int _reservePad1; // _reservePad1 int + internal long ecQueueTime; // m_usecQueueTime SteamNetworkingMicroseconds + [MarshalAs( UnmanagedType.ByValArray, SizeConst = 10, ArraySubType = UnmanagedType.U4 )] + internal uint[] reserved; // reserved uint32 [10] + + /// + /// Number of bytes unreliable data pending to be sent. This is data that you have recently requested to be sent but has not yet actually been put on the wire. + /// + public int PendingUnreliable => cbPendingUnreliable; + + /// + /// Number of bytes reliable data pending to be sent. This is data that you have recently requested to be sent but has not yet actually been put on the wire. + /// + public int PendingReliable => cbPendingReliable; + + /// + /// Number of bytes of reliable data that has been placed the wire, but for which we have not yet received an acknowledgment, and thus we may have to re-transmit. + /// + public int SentUnackedReliable => cbSentUnackedReliable; + } +} \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/Networking/ConnectionManager.cs b/Libraries/Facepunch.Steamworks/Networking/ConnectionManager.cs index 1ac65e609..3116fa804 100644 --- a/Libraries/Facepunch.Steamworks/Networking/ConnectionManager.cs +++ b/Libraries/Facepunch.Steamworks/Networking/ConnectionManager.cs @@ -1,6 +1,5 @@ using Steamworks.Data; using System; -using System.Runtime.InteropServices; namespace Steamworks { @@ -36,7 +35,10 @@ namespace Steamworks set => Connection.UserData = value; } - public void Close() => Connection.Close(); + public void Close( bool linger = false, int reasonCode = 0, string debugString = "Closing Connection" ) + { + Connection.Close( linger, reasonCode, debugString ); + } public override string ToString() => Connection.ToString(); @@ -44,18 +46,40 @@ namespace Steamworks { ConnectionInfo = info; + // + // Some notes: + // - Update state before the callbacks, in case an exception is thrown + // - ConnectionState.None happens when a connection is destroyed, even if it was already disconnected (ClosedByPeer / ProblemDetectedLocally) + // switch ( info.State ) { case ConnectionState.Connecting: - OnConnecting( info ); + if ( !Connecting && !Connected ) + { + Connecting = true; + + OnConnecting( info ); + } break; case ConnectionState.Connected: - OnConnected( info ); + if ( Connecting && !Connected ) + { + Connecting = false; + Connected = true; + + OnConnected( info ); + } break; case ConnectionState.ClosedByPeer: case ConnectionState.ProblemDetectedLocally: case ConnectionState.None: - OnDisconnected( info ); + if ( Connecting || Connected ) + { + Connecting = false; + Connected = false; + + OnDisconnected( info ); + } break; } } @@ -66,8 +90,6 @@ namespace Steamworks public virtual void OnConnecting( ConnectionInfo info ) { Interface?.OnConnecting( info ); - - Connecting = true; } /// @@ -76,9 +98,6 @@ namespace Steamworks public virtual void OnConnected( ConnectionInfo info ) { Interface?.OnConnected( info ); - - Connected = true; - Connecting = false; } /// @@ -87,52 +106,166 @@ namespace Steamworks public virtual void OnDisconnected( ConnectionInfo info ) { Interface?.OnDisconnected( info ); - - Connected = false; - Connecting = false; } - public void Receive( int bufferSize = 32 ) + public unsafe int Receive( int bufferSize = 32, bool receiveToEnd = true ) { - if (SteamNetworkingSockets.Internal is null) { return; } + if (SteamNetworkingSockets.Internal is null) { return 0; } + + if ( bufferSize < 1 || bufferSize > 256 ) throw new ArgumentOutOfRangeException( nameof( bufferSize ) ); + + int totalProcessed = 0; + NetMsg** messageBuffer = stackalloc NetMsg*[bufferSize]; - int processed = 0; - IntPtr messageBuffer = Marshal.AllocHGlobal( IntPtr.Size * bufferSize ); - - try + while ( true ) { - processed = SteamNetworkingSockets.Internal.ReceiveMessagesOnConnection( Connection, messageBuffer, bufferSize ); + int processed = SteamNetworkingSockets.Internal.ReceiveMessagesOnConnection( Connection, new IntPtr( &messageBuffer[0] ), bufferSize ); + totalProcessed += processed; - for ( int i = 0; i < processed; i++ ) + try { - ReceiveMessage( Marshal.ReadIntPtr( messageBuffer, i * IntPtr.Size ) ); + for ( int i = 0; i < processed; i++ ) + { + ReceiveMessage( ref messageBuffer[i] ); + } + } + catch + { + for ( int i = 0; i < processed; i++ ) + { + if ( messageBuffer[i] != null ) + { + NetMsg.InternalRelease( messageBuffer[i] ); + } + } + + throw; + } + + + // + // Keep going if receiveToEnd and we filled the buffer + // + if ( !receiveToEnd || processed < bufferSize ) + break; + } + + return totalProcessed; + } + + /// + /// Sends a message to multiple connections. + /// + /// The connections to send the message to. + /// The number of connections to send the message to, to allow reusing the connections array. + /// Pointer to the message data. + /// Size of the message data. + /// Flags to control delivery of the message. + /// An optional array to hold the results of sending the messages for each connection. + public unsafe void SendMessages( Connection[] connections, int connectionCount, IntPtr ptr, int size, SendType sendType = SendType.Reliable, Result[]? results = null ) + { + if ( connections == null ) + throw new ArgumentNullException( nameof( connections ) ); + if ( connectionCount < 0 || connectionCount > connections.Length ) + throw new ArgumentException( "`connectionCount` must be between 0 and `connections.Length`", nameof( connectionCount ) ); + if ( results != null && connectionCount > results.Length ) + throw new ArgumentException( "`results` must have at least `connectionCount` entries", nameof( results ) ); + if ( connectionCount > 1024 ) // restricting this because we stack allocate based on this value + throw new ArgumentOutOfRangeException( nameof( connectionCount ) ); + if ( ptr == IntPtr.Zero ) + throw new ArgumentNullException( nameof( ptr ) ); + if ( size == 0 ) + throw new ArgumentException( "`size` cannot be zero", nameof( size ) ); + + if ( SteamNetworkingSockets.Internal is null ) + return; + if ( connectionCount == 0 ) + return; + + // SendMessages does not make a copy of the data. We will need to copy because we don't want to force the caller to keep the pointer valid. + // 1. We don't want a copy per message. They all refer to the same data. This is the benefit of using Broadcast vs. many sends. + // 2. We need to use unmanaged memory. Managed memory may move around and invalidate pointers so it's not an option. + // 3. We'll use a reference counter and custom free() function to release this unmanaged memory. + var copyPtr = BufferManager.Get( size, connectionCount ); + Buffer.MemoryCopy( (void*)ptr, (void*)copyPtr, size, size ); + + var messages = stackalloc NetMsg*[connectionCount]; + var messageNumberOrResults = stackalloc long[results != null ? connectionCount : 0]; + + for ( var i = 0; i < connectionCount; i++ ) + { + messages[i] = SteamNetworkingUtils.AllocateMessage(); + messages[i]->Connection = connections[i]; + messages[i]->Flags = sendType; + messages[i]->DataPtr = copyPtr; + messages[i]->DataSize = size; + messages[i]->FreeDataPtr = BufferManager.FreeFunctionPointer; + } + + SteamNetworkingSockets.Internal.SendMessages( connectionCount, messages, messageNumberOrResults ); + + if (results == null) + return; + + for ( var i = 0; i < connectionCount; i++ ) + { + if ( messageNumberOrResults[i] < 0 ) + { + results[i] = (Result)( -messageNumberOrResults[i] ); + } + else + { + results[i] = Result.OK; } } - finally - { - Marshal.FreeHGlobal( messageBuffer ); - } - - // - // Overwhelmed our buffer, keep going - // - if ( processed == bufferSize ) - Receive( bufferSize ); } - internal unsafe void ReceiveMessage( IntPtr msgPtr ) + /// + /// Ideally should be using an IntPtr version unless you're being really careful with the byte[] array and + /// you're not creating a new one every frame (like using .ToArray()) + /// + public unsafe void SendMessages( Connection[] connections, int connectionCount, byte[] data, SendType sendType = SendType.Reliable, Result[]? results = null ) + { + fixed ( byte* ptr = data ) + { + SendMessages( connections, connectionCount, (IntPtr)ptr, data.Length, sendType, results ); + } + } + + /// + /// Ideally should be using an IntPtr version unless you're being really careful with the byte[] array and + /// you're not creating a new one every frame (like using .ToArray()) + /// + public unsafe void SendMessages( Connection[] connections, int connectionCount, byte[] data, int offset, int length, SendType sendType = SendType.Reliable, Result[]? results = null ) + { + fixed ( byte* ptr = data ) + { + SendMessages( connections, connectionCount, (IntPtr)ptr + offset, length, sendType, results ); + } + } + + /// + /// This creates a ton of garbage - so don't do anything with this beyond testing! + /// + public void SendMessages( Connection[] connections, int connectionCount, string str, SendType sendType = SendType.Reliable, Result[]? results = null ) + { + var bytes = System.Text.Encoding.UTF8.GetBytes( str ); + SendMessages( connections, connectionCount, bytes, sendType, results ); + } + + internal unsafe void ReceiveMessage( ref NetMsg* msg ) { - var msg = Marshal.PtrToStructure( msgPtr ); try { - OnMessage( msg.DataPtr, msg.DataSize, msg.RecvTime, msg.MessageNumber, msg.Channel ); + OnMessage( msg->DataPtr, msg->DataSize, msg->RecvTime, msg->MessageNumber, msg->Channel ); } finally { // // Releases the message // - NetMsg.InternalRelease( (NetMsg*) msgPtr ); + NetMsg.InternalRelease( msg ); + msg = null; } } @@ -141,4 +274,4 @@ namespace Steamworks Interface?.OnMessage( data, size, messageNum, recvTime, channel ); } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/Networking/ConnectionStatus.cs b/Libraries/Facepunch.Steamworks/Networking/ConnectionStatus.cs new file mode 100644 index 000000000..c217055fc --- /dev/null +++ b/Libraries/Facepunch.Steamworks/Networking/ConnectionStatus.cs @@ -0,0 +1,77 @@ +using System.Runtime.InteropServices; + +namespace Steamworks.Data +{ + /// + /// Describe the status of a connection + /// + [StructLayout( LayoutKind.Sequential, Pack = Platform.StructPlatformPackSize )] + public struct ConnectionStatus + { + internal ConnectionState state; // m_eState ESteamNetworkingConnectionState + internal int ping; // m_nPing int + internal float connectionQualityLocal; // m_flConnectionQualityLocal float + internal float connectionQualityRemote; // m_flConnectionQualityRemote float + internal float outPacketsPerSec; // m_flOutPacketsPerSec float + internal float outBytesPerSec; // m_flOutBytesPerSec float + internal float inPacketsPerSec; // m_flInPacketsPerSec float + internal float inBytesPerSec; // m_flInBytesPerSec float + internal int sendRateBytesPerSecond; // m_nSendRateBytesPerSecond int + internal int cbPendingUnreliable; // m_cbPendingUnreliable int + internal int cbPendingReliable; // m_cbPendingReliable int + internal int cbSentUnackedReliable; // m_cbSentUnackedReliable int + internal long ecQueueTime; // m_usecQueueTime SteamNetworkingMicroseconds + [MarshalAs( UnmanagedType.ByValArray, SizeConst = 16, ArraySubType = UnmanagedType.U4 )] + internal uint[] reserved; // reserved uint32 [16] + + /// + /// Current ping (ms) + /// + public int Ping => ping; + + /// + /// Outgoing packets per second + /// + public float OutPacketsPerSec => outPacketsPerSec; + + /// + /// Outgoing bytes per second + /// + public float OutBytesPerSec => outBytesPerSec; + + /// + /// Incoming packets per second + /// + public float InPacketsPerSec => inPacketsPerSec; + + /// + /// Incoming bytes per second + /// + public float InBytesPerSec => inBytesPerSec; + + /// + /// Connection quality measured locally, 0...1 (percentage of packets delivered end-to-end in order). + /// + public float ConnectionQualityLocal => connectionQualityLocal; + + /// + /// Packet delivery success rate as observed from remote host, 0...1 (percentage of packets delivered end-to-end in order). + /// + public float ConnectionQualityRemote => connectionQualityRemote; + + /// + /// Number of bytes unreliable data pending to be sent. This is data that you have recently requested to be sent but has not yet actually been put on the wire. + /// + public int PendingUnreliable => cbPendingUnreliable; + + /// + /// Number of bytes reliable data pending to be sent. This is data that you have recently requested to be sent but has not yet actually been put on the wire. + /// + public int PendingReliable => cbPendingReliable; + + /// + /// Number of bytes of reliable data that has been placed the wire, but for which we have not yet received an acknowledgment, and thus we may have to re-transmit. + /// + public int SentUnackedReliable => cbSentUnackedReliable; + } +} \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/Networking/Delegates.cs b/Libraries/Facepunch.Steamworks/Networking/Delegates.cs new file mode 100644 index 000000000..2ab13e14e --- /dev/null +++ b/Libraries/Facepunch.Steamworks/Networking/Delegates.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Steamworks.Data; + +namespace Steamworks +{ + [UnmanagedFunctionPointer( Platform.CC )] + internal delegate void NetDebugFunc( [In] NetDebugOutput nType, [In] IntPtr pszMsg ); + + [UnmanagedFunctionPointer( Platform.CC )] + internal unsafe delegate void FnSteamNetConnectionStatusChanged( ref SteamNetConnectionStatusChangedCallback_t arg ); + + [UnmanagedFunctionPointer( Platform.CC )] + internal delegate void FnSteamNetAuthenticationStatusChanged( ref SteamNetAuthenticationStatus_t arg ); + + [UnmanagedFunctionPointer( Platform.CC )] + internal delegate void FnSteamRelayNetworkStatusChanged( ref SteamRelayNetworkStatus_t arg ); + + [UnmanagedFunctionPointer( Platform.CC )] + internal delegate void FnSteamNetworkingMessagesSessionRequest( ref SteamNetworkingMessagesSessionRequest_t arg ); + + [UnmanagedFunctionPointer( Platform.CC )] + internal delegate void FnSteamNetworkingMessagesSessionFailed( ref SteamNetworkingMessagesSessionFailed_t arg ); + + [UnmanagedFunctionPointer( Platform.CC )] + internal delegate void FnSteamNetworkingFakeIPResult( ref SteamNetworkingFakeIPResult_t arg ); +} diff --git a/Libraries/Facepunch.Steamworks/Networking/ISocketManager.cs b/Libraries/Facepunch.Steamworks/Networking/ISocketManager.cs index faab31da9..44d67970b 100644 --- a/Libraries/Facepunch.Steamworks/Networking/ISocketManager.cs +++ b/Libraries/Facepunch.Steamworks/Networking/ISocketManager.cs @@ -16,7 +16,7 @@ namespace Steamworks void OnConnected( Connection connection, ConnectionInfo info ); /// - /// Called when the connection leaves + /// Called when the connection leaves. Must call Close on the connection /// void OnDisconnected( Connection connection, ConnectionInfo info ); diff --git a/Libraries/Facepunch.Steamworks/Networking/NetAddress.cs b/Libraries/Facepunch.Steamworks/Networking/NetAddress.cs index 09db1e883..3338382e2 100644 --- a/Libraries/Facepunch.Steamworks/Networking/NetAddress.cs +++ b/Libraries/Facepunch.Steamworks/Networking/NetAddress.cs @@ -110,6 +110,18 @@ namespace Steamworks.Data } } + /// + /// Return true if IP is a fake IPv4 for Steam Datagram Relay + /// + public bool IsFakeIPv4 + { + get + { + NetAddress self = this; + return SteamNetworkingUtils.Internal != null && SteamNetworkingUtils.Internal.IsFakeIPv4( InternalGetIPv4( ref self ) ); + } + } + /// /// Return true if this identity is localhost. (Either IPv6 ::1, or IPv4 127.0.0.1) /// diff --git a/Libraries/Facepunch.Steamworks/Networking/NetDebugFunc.cs b/Libraries/Facepunch.Steamworks/Networking/NetDebugFunc.cs deleted file mode 100644 index ec4cabdec..000000000 --- a/Libraries/Facepunch.Steamworks/Networking/NetDebugFunc.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Steamworks.Data; -using System; -using System.Runtime.InteropServices; - -namespace Steamworks.Data -{ - [UnmanagedFunctionPointer( Platform.CC )] - delegate void NetDebugFunc( [In] NetDebugOutput nType, [In] IntPtr pszMsg ); -} \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/Networking/NetKeyValue.cs b/Libraries/Facepunch.Steamworks/Networking/NetKeyValue.cs index 88bc7ee2c..5ce0661a9 100644 --- a/Libraries/Facepunch.Steamworks/Networking/NetKeyValue.cs +++ b/Libraries/Facepunch.Steamworks/Networking/NetKeyValue.cs @@ -5,7 +5,7 @@ using System.Runtime.InteropServices; namespace Steamworks.Data { [StructLayout( LayoutKind.Explicit, Pack = Platform.StructPlatformPackSize )] - internal struct NetKeyValue + internal partial struct NetKeyValue { [FieldOffset(0)] internal NetConfig Value; // m_eValue ESteamNetworkingConfigValue diff --git a/Libraries/Facepunch.Steamworks/Networking/NetMsg.cs b/Libraries/Facepunch.Steamworks/Networking/NetMsg.cs index 8bd095384..63091cffc 100644 --- a/Libraries/Facepunch.Steamworks/Networking/NetMsg.cs +++ b/Libraries/Facepunch.Steamworks/Networking/NetMsg.cs @@ -1,5 +1,4 @@ -using Steamworks.Data; -using System; +using System; using System.Runtime.InteropServices; namespace Steamworks.Data @@ -17,5 +16,9 @@ namespace Steamworks.Data internal IntPtr FreeDataPtr; internal IntPtr ReleasePtr; internal int Channel; + internal SendType Flags; + internal long UserData; + internal ushort IdxLane; + internal ushort _pad1__; } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/Networking/SocketManager.cs b/Libraries/Facepunch.Steamworks/Networking/SocketManager.cs index 1d9721cf4..4e36a0926 100644 --- a/Libraries/Facepunch.Steamworks/Networking/SocketManager.cs +++ b/Libraries/Facepunch.Steamworks/Networking/SocketManager.cs @@ -16,8 +16,9 @@ namespace Steamworks { public ISocketManager? Interface { get; set; } - public List Connecting = new List(); - public List Connected = new List(); + public HashSet Connecting = new HashSet(); + public HashSet Connected = new HashSet(); + public Socket Socket { get; internal set; } public override string ToString() => Socket.ToString(); @@ -44,17 +45,23 @@ namespace Steamworks public virtual void OnConnectionChanged( Connection connection, ConnectionInfo info ) { + // + // Some notes: + // - Update state before the callbacks, in case an exception is thrown + // - ConnectionState.None happens when a connection is destroyed, even if it was already disconnected (ClosedByPeer / ProblemDetectedLocally) + // switch ( info.State ) { case ConnectionState.Connecting: - if ( !Connecting.Contains( connection ) ) + if ( !Connecting.Contains( connection ) && !Connected.Contains( connection ) ) { Connecting.Add( connection ); + OnConnecting( connection, info ); } break; case ConnectionState.Connected: - if ( !Connected.Contains( connection ) ) + if ( Connecting.Contains( connection ) && !Connected.Contains( connection ) ) { Connecting.Remove( connection ); Connected.Add( connection ); @@ -67,6 +74,9 @@ namespace Steamworks case ConnectionState.None: if ( Connecting.Contains( connection ) || Connected.Contains( connection ) ) { + Connecting.Remove( connection ); + Connected.Remove( connection ); + OnDisconnected( connection, info ); } break; @@ -81,7 +91,6 @@ namespace Steamworks if ( Interface != null ) { Interface.OnConnecting( connection, info ); - return; } else { @@ -104,17 +113,17 @@ namespace Steamworks /// public virtual void OnDisconnected( Connection connection, ConnectionInfo info ) { - SteamNetworkingSockets.Internal?.SetConnectionPollGroup( connection, 0 ); - - connection.Close(); - - Connecting.Remove( connection ); - Connected.Remove( connection ); - - Interface?.OnDisconnected( connection, info ); + if ( Interface != null ) + { + Interface.OnDisconnected( connection, info ); + } + else + { + connection.Close(); + } } - public void Receive( int bufferSize = 32 ) + public int Receive( int bufferSize = 32, bool receiveToEnd = true ) { int processed = 0; IntPtr messageBuffer = Marshal.AllocHGlobal( IntPtr.Size * bufferSize ); @@ -137,8 +146,10 @@ namespace Steamworks // // Overwhelmed our buffer, keep going // - if ( processed == bufferSize ) - Receive( bufferSize ); + if ( receiveToEnd && processed == bufferSize ) + processed += Receive( bufferSize ); + + return processed; } internal unsafe void ReceiveMessage( IntPtr msgPtr ) @@ -162,4 +173,4 @@ namespace Steamworks Interface?.OnMessage( connection, identity, data, size, messageNum, recvTime, channel ); } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamApps.cs b/Libraries/Facepunch.Steamworks/SteamApps.cs index 80af70a5e..80359e884 100644 --- a/Libraries/Facepunch.Steamworks/SteamApps.cs +++ b/Libraries/Facepunch.Steamworks/SteamApps.cs @@ -15,9 +15,12 @@ namespace Steamworks { internal static ISteamApps? Internal => Interface as ISteamApps; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamApps( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; + + return true; } internal static void InstallEvents() @@ -27,41 +30,41 @@ namespace Steamworks } /// - /// posted after the user gains ownership of DLC and that DLC is installed + /// Posted after the user gains ownership of DLC and that DLC is installed. /// public static event Action? OnDlcInstalled; /// - /// posted after the user gains executes a Steam URL with command line or query parameters + /// Posted after the user gains executes a Steam URL with command line or query parameters /// such as steam://run/appid//-commandline/?param1=value1(and)param2=value2(and)param3=value3 etc /// while the game is already running. The new params can be queried - /// with GetLaunchQueryParam and GetLaunchCommandLine + /// with GetLaunchQueryParam and GetLaunchCommandLine. /// public static event Action? OnNewLaunchParameters; /// - /// Checks if the active user is subscribed to the current App ID + /// Gets whether or not the active user is subscribed to the current App ID. /// public static bool IsSubscribed => Internal != null && Internal.BIsSubscribed(); /// - /// Check if user borrowed this game via Family Sharing, If true, call GetAppOwner() to get the lender SteamID + /// Gets whether or not the user borrowed this game via Family Sharing. If true, call GetAppOwner() to get the lender SteamID. /// public static bool IsSubscribedFromFamilySharing => Internal != null && Internal.BIsSubscribedFromFamilySharing(); /// - /// Checks if the license owned by the user provides low violence depots. + /// Gets whether or not the license owned by the user provides low violence depots. /// Low violence depots are useful for copies sold in countries that have content restrictions /// public static bool IsLowViolence => Internal != null && Internal.BIsLowViolence(); /// - /// Checks whether the current App ID license is for Cyber Cafes. + /// Gets whether or not the current App ID license is for Cyber Cafes. /// public static bool IsCybercafe => Internal != null && Internal.BIsCybercafe(); /// - /// CChecks if the user has a VAC ban on their account + /// Gets whether or not the user has a VAC ban on their account. /// public static bool IsVACBanned => Internal != null && Internal.BIsVACBanned(); @@ -77,19 +80,22 @@ namespace Steamworks public static string[]? AvailableLanguages => Internal?.GetAvailableGameLanguages().Split( new[] { ',' }, StringSplitOptions.RemoveEmptyEntries ); /// - /// Checks if the active user is subscribed to a specified AppId. + /// Gets whether or not the active user is subscribed to a specified App ID. /// Only use this if you need to check ownership of another game related to yours, a demo for example. /// + /// The App ID of the DLC to check. public static bool IsSubscribedToApp( AppId appid ) => Internal != null && Internal.BIsSubscribedApp( appid.Value ); /// - /// Checks if the user owns a specific DLC and if the DLC is installed + /// Gets whether or not the user owns a specific DLC and if the DLC is installed. /// + /// The App ID of the DLC to check. public static bool IsDlcInstalled( AppId appid ) => Internal != null && Internal.BIsDlcInstalled( appid.Value ); /// - /// Returns the time of the purchase of the app + /// Returns the time of the purchase of the app. /// + /// The App ID to check the purchase time for. public static DateTime PurchaseTime( AppId appid = default ) { if (Internal is null) { return default; } @@ -101,14 +107,14 @@ namespace Steamworks } /// - /// Checks if the user is subscribed to the current app through a free weekend - /// This function will return false for users who have a retail or other type of license - /// Before using, please ask your Valve technical contact how to package and secure your free weekened + /// Checks if the user is subscribed to the current app through a free weekend. + /// This function will return false for users who have a retail or other type of license. + /// Before using, please ask your Valve technical contact how to package and secure your free weekened. /// public static bool IsSubscribedFromFreeWeekend => Internal != null && Internal.BIsSubscribedFromFreeWeekend(); /// - /// Returns metadata for all available DLC + /// Returns metadata for all available DLC. /// public static IEnumerable DlcInformation() { @@ -134,17 +140,19 @@ namespace Steamworks } /// - /// Install/Uninstall control for optional DLC + /// Install control for optional DLC. /// + /// The App ID of the DLC to install. public static void InstallDlc( AppId appid ) => Internal?.InstallDLC( appid.Value ); /// - /// Install/Uninstall control for optional DLC + /// Uninstall control for optional DLC. /// + /// The App ID of the DLC to uninstall. public static void UninstallDlc( AppId appid ) => Internal?.UninstallDLC( appid.Value ); /// - /// Returns null if we're not on a beta branch, else the name of the branch + /// Gets the name of the beta branch that is launched, or if the application is not running on a beta branch. /// public static string? CurrentBetaName { @@ -158,16 +166,19 @@ namespace Steamworks } /// - /// Allows you to force verify game content on next launch. - /// - /// If you detect the game is out-of-date(for example, by having the client detect a version mismatch with a server), - /// you can call use MarkContentCorrupt to force a verify, show a message to the user, and then quit. + /// Force verify game content on next launch. + /// + /// If you detect the game is out-of-date (for example, by having the client detect a version mismatch with a server), + /// you can call MarkContentCorrupt to force a verify, show a message to the user, and then quit. + /// /// + /// Whether or not to only verify missing files. public static void MarkContentCorrupt( bool missingFilesOnly ) => Internal?.MarkContentCorrupt( missingFilesOnly ); /// - /// Gets a list of all installed depots for a given App ID in mount order + /// Gets a list of all installed depots for a given App ID in mount order. /// + /// The App ID. public static IEnumerable InstalledDepots( AppId appid = default ) { if (Internal is null) { yield break; } @@ -185,9 +196,10 @@ namespace Steamworks } /// - /// Gets the install folder for a specific AppID. + /// Gets the install folder for a specific App ID. /// This works even if the application is not installed, based on where the game would be installed with the default Steam library location. /// + /// The App ID. public static string? AppInstallDir( AppId appid = default ) { if ( appid == 0 ) @@ -200,26 +212,32 @@ namespace Steamworks } /// - /// The app may not actually be owned by the current user, they may have it left over from a free weekend, etc. + /// Gets whether or not the app is owned by the current user. The app may not actually be owned by the current user; they may have it left over from a free weekend, etc. /// + /// The App ID. public static bool IsAppInstalled( AppId appid ) => Internal != null && Internal.BIsAppInstalled( appid.Value ); /// - /// Gets the Steam ID of the original owner of the current app. If it's different from the current user then it is borrowed.. + /// Gets the Steam ID of the original owner of the current app. If it's different from the current user then it is borrowed. /// public static SteamId AppOwner => Internal?.GetAppOwner().Value ?? default; /// /// Gets the associated launch parameter if the game is run via steam://run/appid/?param1=value1;param2=value2;param3=value3 etc. - /// Parameter names starting with the character '@' are reserved for internal use and will always return an empty string. - /// Parameter names starting with an underscore '_' are reserved for steam features -- they can be queried by the game, + /// + /// Parameter names starting with the character '@' are reserved for internal use and will always return an empty string. + /// Parameter names starting with an underscore '_' are reserved for steam features -- they can be queried by the game, /// but it is advised that you not param names beginning with an underscore for your own features. + /// /// + /// The name of the parameter. + /// The launch parameter value. public static string? GetLaunchParam( string param ) => Internal?.GetLaunchQueryParam( param ); /// /// Gets the download progress for optional DLC. /// + /// The App ID to check the progress for. public static DownloadProgress DlcDownloadProgress( AppId appid ) { ulong punBytesDownloaded = 0; @@ -232,16 +250,16 @@ namespace Steamworks } /// - /// Gets the buildid of this app, may change at any time based on backend updates to the game. - /// Defaults to 0 if you're not running a build downloaded from steam. + /// Gets the Build ID of this app, which can change at any time based on backend updates to the game. + /// Defaults to 0 if you're not running a build downloaded from steam. /// public static int BuildId => Internal?.GetAppBuildId() ?? 0; /// /// Asynchronously retrieves metadata details about a specific file in the depot manifest. - /// Currently provides: /// + /// The name of the file. public static async Task GetFileDetailsAsync( string filename ) { if (Internal is null) { return null; } @@ -262,9 +280,9 @@ namespace Steamworks /// Get command line if game was launched via Steam URL, e.g. steam://run/appid//command line/. /// This method of passing a connect string (used when joining via rich presence, accepting an /// invite, etc) is preferable to passing the connect string on the operating system command - /// line, which is a security risk. In order for rich presence joins to go through this + /// line, which is a security risk. In order for rich presence joins to go through this /// path and not be placed on the OS command line, you must set a value in your app's - /// configuration on Steam. Ask Valve for help with this. + /// configuration on Steam. Ask Valve for help with this. /// public static string? CommandLine { @@ -276,5 +294,26 @@ namespace Steamworks } } + /// + /// Check if game is a timed trial with limited playtime. + /// + /// The amount of seconds left on the timed trial. + /// The amount of seconds played on the timed trial. + public static bool IsTimedTrial( out int secondsAllowed, out int secondsPlayed ) + { + uint a = 0; + uint b = 0; + secondsAllowed = 0; + secondsPlayed = 0; + + if ( Internal == null || !Internal.BIsTimedTrial( ref a, ref b ) ) + return false; + + secondsAllowed = (int) a; + secondsPlayed = (int) b; + + return true; + } + } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamClient.cs b/Libraries/Facepunch.Steamworks/SteamClient.cs index 31ef55045..f0db1417c 100644 --- a/Libraries/Facepunch.Steamworks/SteamClient.cs +++ b/Libraries/Facepunch.Steamworks/SteamClient.cs @@ -13,7 +13,7 @@ namespace Steamworks /// /// Initialize the steam client. - /// If asyncCallbacks is false you need to call RunCallbacks manually every frame. + /// If is false you need to call manually every frame. /// public static void Init( uint appid, bool asyncCallbacks = true ) { @@ -25,7 +25,7 @@ namespace Steamworks if ( !SteamAPI.Init() ) { - throw new System.Exception( "SteamApi_Init returned false. Steam isn't running, couldn't find Steam, AppId is ureleased, Don't own AppId." ); + throw new System.Exception( "SteamApi_Init returned false. Steam isn't running, couldn't find Steam, App ID is ureleased, Don't own App ID." ); } AppId = appid; @@ -60,7 +60,9 @@ namespace Steamworks AddInterface(); AddInterface(); - if ( asyncCallbacks ) + initialized = openInterfaces.Count > 0; + + if ( asyncCallbacks ) { // // This will keep looping in the background every 16 ms @@ -73,8 +75,15 @@ namespace Steamworks internal static void AddInterface() where T : SteamClass, new() { var t = new T(); - t.InitializeInterface( false ); - openInterfaces.Add( t ); + bool valid = t.InitializeInterface( false ); + if ( valid ) + { + openInterfaces.Add( t ); + } + else + { + t.DestroyInterface( false ); + } } static readonly List openInterfaces = new List(); @@ -91,6 +100,9 @@ namespace Steamworks public static bool IsValid => initialized; + /// + /// Shuts down the steam client. + /// public static void Shutdown() { if ( !IsValid ) return; @@ -116,13 +128,15 @@ namespace Steamworks /// /// Checks if the current user's Steam client is connected to the Steam servers. - /// If it's not then no real-time services provided by the Steamworks API will be enabled. The Steam + /// + /// If it's not, no real-time services provided by the Steamworks API will be enabled. The Steam /// client will automatically be trying to recreate the connection as often as possible. When the /// connection is restored a SteamServersConnected_t callback will be posted. /// You usually don't need to check for this yourself. All of the API calls that rely on this will /// check internally. Forcefully disabling stuff when the player loses access is usually not a /// very good experience for the player and you could be preventing them from accessing APIs that do not /// need a live connection to Steam. + /// /// public static bool IsLoggedOn => SteamUser.Internal != null && SteamUser.Internal.BLoggedOn(); @@ -135,28 +149,30 @@ namespace Steamworks public static SteamId SteamId => SteamUser.Internal?.GetSteamID() ?? default; /// - /// returns the local players name - guaranteed to not be NULL. - /// this is the same name as on the users community profile page + /// returns the local players name - guaranteed to not be . + /// This is the same name as on the user's community profile page. /// public static string? Name => SteamFriends.Internal?.GetPersonaName(); /// - /// gets the status of the current user + /// Gets the status of the current user. /// public static FriendState State => SteamFriends.Internal?.GetPersonaState() ?? FriendState.Offline; /// - /// returns the appID of the current process + /// Returns the App ID of the current process. /// public static AppId AppId { get; internal set; } /// - /// Checks if your executable was launched through Steam and relaunches it through Steam if it wasn't - /// this returns true then it starts the Steam client if required and launches your game again through it, + /// Checks if your executable was launched through Steam and relaunches it through Steam if it wasn't. + /// + /// This returns true then it starts the Steam client if required and launches your game again through it, /// and you should quit your process as soon as possible. This effectively runs steam://run/AppId so it /// may not relaunch the exact executable that called it, as it will always relaunch from the version /// installed in your Steam library folder/ /// Note that during development, when not launching via Steam, this might always return true. + /// /// public static bool RestartAppIfNecessary( uint appid ) { @@ -178,4 +194,4 @@ namespace Steamworks } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamFriends.cs b/Libraries/Facepunch.Steamworks/SteamFriends.cs index bcc457525..b251db233 100644 --- a/Libraries/Facepunch.Steamworks/SteamFriends.cs +++ b/Libraries/Facepunch.Steamworks/SteamFriends.cs @@ -8,19 +8,22 @@ using Steamworks.Data; namespace Steamworks { /// - /// Undocumented Parental Settings + /// Class for utilizing the Steam Friends API. /// public class SteamFriends : SteamClientClass { internal static ISteamFriends? Internal => Interface as ISteamFriends; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamFriends( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; richPresence = new Dictionary(); InstallEvents(); + + return true; } static Dictionary? richPresence; @@ -30,53 +33,67 @@ namespace Steamworks Dispatch.Install( x => OnPersonaStateChange?.Invoke( new Friend( x.SteamID ) ) ); Dispatch.Install( x => OnGameRichPresenceJoinRequested?.Invoke( new Friend( x.SteamIDFriend), x.ConnectUTF8() ) ); Dispatch.Install( OnFriendChatMessage ); + Dispatch.Install( OnGameConnectedClanChatMessage ); Dispatch.Install( x => OnGameOverlayActivated?.Invoke( x.Active != 0 ) ); Dispatch.Install( x => OnGameServerChangeRequested?.Invoke( x.ServerUTF8(), x.PasswordUTF8() ) ); Dispatch.Install( x => OnGameLobbyJoinRequested?.Invoke( new Lobby( x.SteamIDLobby ), x.SteamIDFriend ) ); Dispatch.Install( x => OnFriendRichPresenceUpdate?.Invoke( new Friend( x.SteamIDFriend ) ) ); + Dispatch.Install( x => OnOverlayBrowserProtocol?.Invoke( x.RgchURIUTF8() ) ); } /// - /// Called when chat message has been received from a friend. You'll need to turn on - /// ListenForFriendsMessages to recieve this. (friend, msgtype, message) + /// Invoked when a chat message has been received from a friend. You'll need to enable + /// to recieve this. (friend, msgtype, message) /// public static event Action? OnChatMessage; /// - /// called when a friends' status changes + /// Invoked when a chat message has been received in a Steam group chat that we are in. Associated Functions: JoinClanChatRoom. (friend, msgtype, message) + /// + public static event Action? OnClanChatMessage; + + /// + /// Invoked when a friends' status changes. /// public static event Action? OnPersonaStateChange; /// - /// Called when the user tries to join a game from their friends list - /// rich presence will have been set with the "connect" key which is set here + /// Invoked when the user tries to join a game from their friends list. + /// Rich presence will have been set with the "connect" key which is set here. /// public static event Action? OnGameRichPresenceJoinRequested; /// - /// Posted when game overlay activates or deactivates - /// the game can use this to be pause or resume single player games + /// Invoked when game overlay activates or deactivates. + /// The game can use this to be pause or resume single player games. /// public static event Action? OnGameOverlayActivated; /// - /// Called when the user tries to join a different game server from their friends list - /// game client should attempt to connect to specified server when this is received + /// Invoked when the user tries to join a different game server from their friends list. + /// Game client should attempt to connect to specified server when this is received. /// public static event Action? OnGameServerChangeRequested; /// - /// Called when the user tries to join a lobby from their friends list - /// game client should attempt to connect to specified lobby when this is received + /// Invoked when the user tries to join a lobby from their friends list. + /// Game client should attempt to connect to specified lobby when this is received. /// public static event Action? OnGameLobbyJoinRequested; /// - /// Callback indicating updated data about friends rich presence information + /// Invoked when a friend's rich presence data is updated. /// public static event Action? OnFriendRichPresenceUpdate; + /// + /// Invoked when an overlay browser instance is navigated to a + /// protocol/scheme registered by . + /// + public static event Action? OnOverlayBrowserProtocol; + + static unsafe void OnFriendChatMessage( GameConnectedFriendChatMsg_t data ) { if ( OnChatMessage == null ) return; @@ -96,7 +113,29 @@ namespace Steamworks OnChatMessage( friend, typeName, message ); } - + + static unsafe void OnGameConnectedClanChatMessage( GameConnectedClanChatMsg_t data ) + { + if ( OnClanChatMessage == null ) return; + if ( Internal is null ) return; + + var friend = new Friend( data.SteamIDUser ); + + using var buffer = Helpers.TakeMemory(); + var type = ChatEntryType.ChatMsg; + SteamId chatter = data.SteamIDUser; + + var len = Internal.GetClanChatMessage( data.SteamIDClanChat, data.MessageID, buffer, Helpers.MemoryBufferSize, ref type, ref chatter ); + + if ( len == 0 && type == ChatEntryType.Invalid ) + return; + + var typeName = type.ToString(); + var message = Helpers.MemoryToString( buffer ); + + OnClanChatMessage( friend, typeName, message ); + } + private static IEnumerable GetFriendsWithFlag(FriendFlags flag) { if (Internal is null) { yield break; } @@ -108,21 +147,28 @@ namespace Steamworks } } - public static string? GetFriendPersonaName( SteamId steamId ) - { - return Internal?.GetFriendPersonaName(steamId); - } - + /// + /// Gets an of friends that the current user has. + /// + /// An of friends. public static IEnumerable GetFriends() { return GetFriendsWithFlag(FriendFlags.Immediate); } + /// + /// Gets an of blocked users that the current user has. + /// + /// An of blocked users. public static IEnumerable GetBlocked() { return GetFriendsWithFlag(FriendFlags.Blocked); } + /// + /// Gets an of friend requests that the current user has. + /// + /// An of friend requests. public static IEnumerable GetFriendsRequested() { return GetFriendsWithFlag( FriendFlags.FriendshipRequested ); @@ -177,7 +223,7 @@ namespace Steamworks } /// - /// The dialog to open. Valid options are: + /// Opens a specific overlay window. Valid options are: /// "friends", /// "community", /// "players", @@ -204,7 +250,7 @@ namespace Steamworks /// /// Activates the Steam Overlay to the Steam store page for the provided app. /// - public static void OpenStoreOverlay( AppId id ) => Internal?.ActivateGameOverlayToStore( id.Value, OverlayToStoreFlag.None ); + public static void OpenStoreOverlay( AppId id, OverlayToStoreFlag overlayToStoreFlag = OverlayToStoreFlag.None ) => Internal?.ActivateGameOverlayToStore( id.Value, overlayToStoreFlag ); /// /// Activates Steam Overlay web browser directly to the specified URL. @@ -249,6 +295,11 @@ namespace Steamworks await Task.Delay( 500 ); } + /// + /// Returns a small avatar of the user with the given . + /// + /// The of the user to get. + /// A with a value if the image was successfully retrieved. public static async Task GetSmallAvatarAsync( SteamId steamid ) { if (Internal is null) { return null; } @@ -256,6 +307,11 @@ namespace Steamworks return SteamUtils.GetImage( Internal.GetSmallFriendAvatar( steamid ) ); } + /// + /// Returns a medium avatar of the user with the given . + /// + /// The of the user to get. + /// A with a value if the image was successfully retrieved. public static async Task GetMediumAvatarAsync( SteamId steamid ) { if (Internal is null) { return null; } @@ -263,6 +319,11 @@ namespace Steamworks return SteamUtils.GetImage( Internal.GetMediumFriendAvatar( steamid ) ); } + /// + /// Returns a large avatar of the user with the given . + /// + /// The of the user to get. + /// A with a value if the image was successfully retrieved. public static async Task GetLargeAvatarAsync( SteamId steamid ) { if (Internal is null) { return null; } @@ -335,6 +396,11 @@ namespace Steamworks } } + /// + /// Gets whether or not the current user is following the user with the given . + /// + /// The to check. + /// Boolean. public static async Task IsFollowing(SteamId steamID) { if (Internal is null) { return false; } @@ -369,5 +435,29 @@ namespace Steamworks return steamIds.ToArray(); } - } + + /// + /// Call this before calling ActivateGameOverlayToWebPage() to have the Steam Overlay Browser block navigations + /// to your specified protocol (scheme) uris and instead dispatch a OverlayBrowserProtocolNavigation callback to your game. + /// + public static bool RegisterProtocolInOverlayBrowser( string protocol ) + { + return Internal != null && Internal.RegisterProtocolInOverlayBrowser( protocol ); + } + + public static async Task JoinClanChatRoom( SteamId chatId ) + { + if ( Internal is null ) return false; + var result = await Internal.JoinClanChatRoom( chatId ); + if ( !result.HasValue ) + return false; + + return result.Value.ChatRoomEnterResponse == RoomEnter.Success ; + } + + public static bool SendClanChatRoomMessage( SteamId chatId, string message ) + { + return Internal != null && Internal.SendClanChatMessage( chatId, message ); + } + } } diff --git a/Libraries/Facepunch.Steamworks/SteamInput.cs b/Libraries/Facepunch.Steamworks/SteamInput.cs index 3db2293b4..bd3c477ad 100644 --- a/Libraries/Facepunch.Steamworks/SteamInput.cs +++ b/Libraries/Facepunch.Steamworks/SteamInput.cs @@ -1,34 +1,41 @@ -using Steamworks.Data; +using System; +using Steamworks.Data; using System.Collections.Generic; namespace Steamworks { + /// + /// Class for utilizing Steam Input. + /// public class SteamInput : SteamClientClass { internal static ISteamInput? Internal => Interface as ISteamInput; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamInput( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; + + return true; } internal const int STEAM_CONTROLLER_MAX_COUNT = 16; /// - /// You shouldn't really need to call this because it get called by RunCallbacks on SteamClient + /// You shouldn't really need to call this because it gets called by /// but Valve think it might be a nice idea if you call it right before you get input info - /// just to make sure the info you're getting is 100% up to date. /// public static void RunFrame() { - Internal?.RunFrame(); + Internal?.RunFrame( false ); } static readonly InputHandle_t[] queryArray = new InputHandle_t[STEAM_CONTROLLER_MAX_COUNT]; /// - /// Return a list of connected controllers. + /// Gets a list of connected controllers. /// public static IEnumerable Controllers { @@ -65,10 +72,43 @@ namespace Steamworks ref origin ); - return Internal.GetGlyphForActionOrigin(origin); + return Internal.GetGlyphForActionOrigin_Legacy(origin); } - internal static Dictionary DigitalHandles = new Dictionary(); + + /// + /// Return an absolute path to the PNG image glyph for the provided digital action name. The current + /// action set in use for the controller will be used for the lookup. You should cache the result and + /// maintain your own list of loaded PNG assets. + /// + public static string? GetPngActionGlyph( Controller controller, string action, GlyphSize size ) + { + if ( Internal is null ) { return null; } + + InputActionOrigin origin = InputActionOrigin.None; + + Internal.GetDigitalActionOrigins( controller.Handle, Internal.GetCurrentActionSet( controller.Handle ), GetDigitalActionHandle( action ), ref origin ); + + return Internal.GetGlyphPNGForActionOrigin( origin, size, 0 ); + } + + /// + /// Return an absolute path to the SVF image glyph for the provided digital action name. The current + /// action set in use for the controller will be used for the lookup. You should cache the result and + /// maintain your own list of loaded PNG assets. + /// + public static string? GetSvgActionGlyph( Controller controller, string action ) + { + if ( Internal is null ) { return null; } + + InputActionOrigin origin = InputActionOrigin.None; + + Internal.GetDigitalActionOrigins( controller.Handle, Internal.GetCurrentActionSet( controller.Handle ), GetDigitalActionHandle( action ), ref origin ); + + return Internal.GetGlyphSVGForActionOrigin( origin, 0 ); + } + + internal static Dictionary DigitalHandles = new Dictionary(); internal static InputDigitalActionHandle_t GetDigitalActionHandle( string name ) { if (Internal is null) { return default; } @@ -107,4 +147,4 @@ namespace Steamworks return val; } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamInventory.cs b/Libraries/Facepunch.Steamworks/SteamInventory.cs index f45131dd1..e8043deca 100644 --- a/Libraries/Facepunch.Steamworks/SteamInventory.cs +++ b/Libraries/Facepunch.Steamworks/SteamInventory.cs @@ -10,17 +10,20 @@ using Steamworks.Data; namespace Steamworks { /// - /// Undocumented Parental Settings + /// Class for utilizing the Steam Inventory API. /// public class SteamInventory : SteamSharedClass { internal static ISteamInventory? Internal => Interface as ISteamInventory; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamInventory( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; InstallEvents( server ); + + return true; } internal static void InstallEvents( bool server ) @@ -65,7 +68,7 @@ namespace Steamworks /// /// Call this if you're going to want to access definition information. You should be able to get /// away with calling this once at the start if your game, assuming your items don't change all the time. - /// This will trigger OnDefinitionsUpdated at which point Definitions should be set. + /// This will trigger at which point Definitions should be set. /// public static void LoadItemDefinitions() { @@ -83,7 +86,7 @@ namespace Steamworks } /// - /// Will call LoadItemDefinitions and wait until Definitions is not null + /// Will call and wait until Definitions is not null /// public static async Task WaitForDefinitions( float timeoutSeconds = 30 ) { @@ -302,7 +305,7 @@ namespace Steamworks /// - /// Grant all promotional items the user is eligible for + /// Grant all promotional items the user is eligible for. /// public static async Task GrantPromoItemsAsync() { @@ -351,8 +354,9 @@ namespace Steamworks { if (Internal is null) { return null; } - var item_i = items.Select( x => x._id ).ToArray(); - var item_q = items.Select( x => (uint)1 ).ToArray(); + var d = items.GroupBy( x => x._id ).ToDictionary( x => x.Key, x => (uint) x.Count() ); + var item_i = d.Keys.ToArray(); + var item_q = d.Values.ToArray(); var r = await Internal.StartPurchase( item_i, item_q, (uint)item_i.Length ); if ( !r.HasValue ) return null; @@ -366,4 +370,4 @@ namespace Steamworks } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamMatchmaking.cs b/Libraries/Facepunch.Steamworks/SteamMatchmaking.cs index df3ac42eb..d8bc55153 100644 --- a/Libraries/Facepunch.Steamworks/SteamMatchmaking.cs +++ b/Libraries/Facepunch.Steamworks/SteamMatchmaking.cs @@ -8,17 +8,20 @@ using Steamworks.Data; namespace Steamworks { /// - /// Functions for clients to access matchmaking services, favorites, and to operate on game lobbies + /// Methods for clients to access matchmaking services, favorites, and to operate on game lobbies /// public class SteamMatchmaking : SteamClientClass { internal static ISteamMatchmaking? Internal => Interface as ISteamMatchmaking; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamMatchmaking( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; InstallEvents(); + + return true; } /// @@ -101,69 +104,69 @@ namespace Steamworks } /// - /// Someone invited you to a lobby + /// Invoked when the current user is invited to a lobby. /// public static event Action? OnLobbyInvite; /// - /// You joined a lobby + /// Invoked when the current user joins a lobby. /// public static event Action? OnLobbyEntered; /// - /// You created a lobby + /// Invoked when the current user creates a lobby. /// public static event Action? OnLobbyCreated; /// - /// A game server has been associated with the lobby + /// Invoked when a game server has been associated with a lobby. /// public static event Action? OnLobbyGameCreated; /// - /// The lobby metadata has changed + /// Invoked when a lobby's metadata is modified. /// public static event Action? OnLobbyDataChanged; /// - /// The lobby member metadata has changed + /// Invoked when a member in a lobby's metadata is modified. /// public static event Action? OnLobbyMemberDataChanged; /// - /// The lobby member joined + /// Invoked when a member joins a lobby. /// public static event Action? OnLobbyMemberJoined; /// - /// The lobby member left the room + /// Invoked when a lobby member leaves the lobby. /// public static event Action? OnLobbyMemberLeave; /// - /// The lobby member left the room + /// Invoked when a lobby member leaves the lobby. /// public static event Action? OnLobbyMemberDisconnected; /// - /// The lobby member was kicked. The 3rd param is the user that kicked them. + /// Invoked when a lobby member is kicked from a lobby. The 3rd param is the user that kicked them. /// public static event Action? OnLobbyMemberKicked; /// - /// The lobby member was banned. The 3rd param is the user that banned them. + /// Invoked when a lobby member is kicked from a lobby. The 3rd param is the user that kicked them. /// public static event Action? OnLobbyMemberBanned; /// - /// A chat message was recieved from a member of a lobby + /// Invoked when a chat message is received from a member of the lobby. /// public static event Action? OnChatMessage; public static LobbyQuery CreateLobbyQuery() { return new LobbyQuery(); } /// - /// Creates a new invisible lobby. Call lobby.SetPublic to take it online. + /// Creates a new invisible lobby. Call to take it online. /// public static async Task CreateLobbyAsync( int maxMembers = 100 ) { @@ -176,7 +179,7 @@ namespace Steamworks } /// - /// Attempts to directly join the specified lobby + /// Attempts to directly join the specified lobby. /// public static async Task JoinLobbyAsync( SteamId lobbyId ) { @@ -189,7 +192,7 @@ namespace Steamworks } /// - /// Get a list of servers that are on your favorites list + /// Get a list of servers that are on the current user's favorites list. /// public static IEnumerable GetFavoriteServers() { @@ -215,7 +218,7 @@ namespace Steamworks } /// - /// Get a list of servers that you have added to your play history + /// Get a list of servers that the current user has added to their history. /// public static IEnumerable GetHistoryServers() { @@ -241,4 +244,4 @@ namespace Steamworks } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamMatchmakingServers.cs b/Libraries/Facepunch.Steamworks/SteamMatchmakingServers.cs index a1d7d2c71..f44c9cd01 100644 --- a/Libraries/Facepunch.Steamworks/SteamMatchmakingServers.cs +++ b/Libraries/Facepunch.Steamworks/SteamMatchmakingServers.cs @@ -8,15 +8,18 @@ using Steamworks.Data; namespace Steamworks { /// - /// Functions for clients to access matchmaking services, favorites, and to operate on game lobbies + /// Methods for clients to access matchmaking services, favorites, and to operate on game lobbies /// internal class SteamMatchmakingServers : SteamClientClass { internal static ISteamMatchmakingServers? Internal => Interface as ISteamMatchmakingServers; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamMatchmakingServers( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; + + return true; } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamMusic.cs b/Libraries/Facepunch.Steamworks/SteamMusic.cs index b0d6ef42d..7578aa876 100644 --- a/Libraries/Facepunch.Steamworks/SteamMusic.cs +++ b/Libraries/Facepunch.Steamworks/SteamMusic.cs @@ -17,11 +17,13 @@ namespace Steamworks { internal static ISteamMusic? Internal => Interface as ISteamMusic; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamMusic( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; InstallEvents(); + return true; } internal static void InstallEvents() @@ -31,22 +33,22 @@ namespace Steamworks } /// - /// Playback status changed + /// Invoked when playback status is changed. /// public static event Action? OnPlaybackChanged; /// - /// Volume changed, parameter is new volume + /// Invoked when the volume of the music player is changed. /// public static event Action? OnVolumeChanged; /// - /// Checks if Steam Music is enabled + /// Checks if Steam Music is enabled. /// public static bool IsEnabled => Internal != null && Internal.BIsEnabled(); /// - /// true if a song is currently playing, paused, or queued up to play; otherwise false. + /// if a song is currently playing, paused, or queued up to play; otherwise . /// public static bool IsPlaying => Internal != null && Internal.BIsPlaying(); @@ -55,23 +57,28 @@ namespace Steamworks /// public static MusicStatus Status => Internal?.GetPlaybackStatus() ?? MusicStatus.Undefined; - + /// + /// Plays the music player. + /// public static void Play() => Internal?.Play(); + /// + /// Pauses the music player. + /// public static void Pause() => Internal?.Pause(); /// - /// Have the Steam Music player play the previous song. + /// Forces the music player to play the previous song. /// public static void PlayPrevious() => Internal?.PlayPrevious(); /// - /// Have the Steam Music player skip to the next song + /// Forces the music player to skip to the next song. /// public static void PlayNext() => Internal?.PlayNext(); /// - /// Gets/Sets the current volume of the Steam Music player + /// Gets and sets the current volume of the Steam Music player /// public static float Volume { @@ -79,4 +86,4 @@ namespace Steamworks set => Internal?.SetVolume( value ); } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamNetworking.cs b/Libraries/Facepunch.Steamworks/SteamNetworking.cs index cadac81dd..092e7fe28 100644 --- a/Libraries/Facepunch.Steamworks/SteamNetworking.cs +++ b/Libraries/Facepunch.Steamworks/SteamNetworking.cs @@ -8,15 +8,21 @@ using Steamworks.Data; namespace Steamworks { + /// + /// Class for utilizing the Steam Network API. + /// public class SteamNetworking : SteamSharedClass { internal static ISteamNetworking? Internal => Interface as ISteamNetworking; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamNetworking( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; InstallEvents( server ); + + return true; } internal static void InstallEvents( bool server ) @@ -32,20 +38,20 @@ namespace Steamworks } /// - /// This SteamId wants to send you a message. You should respond by calling AcceptP2PSessionWithUser - /// if you want to recieve their messages + /// Invoked when a wants to send the current user a message. You should respond by calling + /// if you want to recieve their messages. /// public static Action? OnP2PSessionRequest; /// - /// Called when packets can't get through to the specified user. + /// Invoked when packets can't get through to the specified user. /// All queued packets unsent at this point will be dropped, further attempts /// to send will retry making the connection (but will be dropped if we fail again). /// public static Action? OnP2PConnectionFailed; /// - /// This should be called in response to a OnP2PSessionRequest + /// This should be called in response to a . /// public static bool AcceptP2PSessionWithUser( SteamId user ) => Internal != null && Internal.AcceptP2PSessionWithUser( user ); @@ -59,22 +65,31 @@ namespace Steamworks /// /// This should be called when you're done communicating with a user, as this will /// free up all of the resources allocated for the connection under-the-hood. - /// If the remote user tries to send data to you again, a new OnP2PSessionRequest + /// If the remote user tries to send data to you again, a new /// callback will be posted /// public static bool CloseP2PSessionWithUser( SteamId user ) => Internal != null && Internal.CloseP2PSessionWithUser( user ); /// - /// Checks if a P2P packet is available to read, and gets the size of the message if there is one. + /// Checks if a P2P packet is available to read. /// public static bool IsP2PPacketAvailable( int channel = 0 ) { uint _ = 0; return Internal != null && Internal.IsP2PPacketAvailable( ref _, channel ); } + + /// + /// Checks if a P2P packet is available to read, and gets the size of the message if there is one. + /// + public static bool IsP2PPacketAvailable( out uint msgSize, int channel = 0 ) + { + msgSize = 0; + return Internal != null && Internal.IsP2PPacketAvailable( ref msgSize, channel ); + } /// - /// Reads in a packet that has been sent from another user via SendP2PPacket.. + /// Reads in a packet that has been sent from another user via SendP2PPacket. /// public unsafe static P2Packet? ReadP2PPacket( int channel = 0 ) { @@ -103,7 +118,7 @@ namespace Steamworks } /// - /// Reads in a packet that has been sent from another user via SendP2PPacket.. + /// Reads in a packet that has been sent from another user via SendP2PPacket. /// public unsafe static bool ReadP2PPacket( byte[] buffer, ref uint size, ref SteamId steamid, int channel = 0 ) { @@ -113,7 +128,7 @@ namespace Steamworks } /// - /// Reads in a packet that has been sent from another user via SendP2PPacket.. + /// Reads in a packet that has been sent from another user via SendP2PPacket. /// public unsafe static bool ReadP2PPacket( byte* buffer, uint cbuf, ref uint size, ref SteamId steamid, int channel = 0 ) { diff --git a/Libraries/Facepunch.Steamworks/SteamNetworkingSockets.cs b/Libraries/Facepunch.Steamworks/SteamNetworkingSockets.cs index 50e80dfd2..02ab4cf54 100644 --- a/Libraries/Facepunch.Steamworks/SteamNetworkingSockets.cs +++ b/Libraries/Facepunch.Steamworks/SteamNetworkingSockets.cs @@ -12,10 +12,32 @@ namespace Steamworks { internal static ISteamNetworkingSockets? Internal => Interface as ISteamNetworkingSockets; - internal override void InitializeInterface( bool server ) + /// + /// Get the identity assigned to this interface. + /// E.g. on Steam, this is the user's SteamID, or for the gameserver interface, the SteamID assigned + /// to the gameserver. Returns false and sets the result to an invalid identity if we don't know + /// our identity yet. (E.g. GameServer has not logged in. On Steam, the user will know their SteamID + /// even if they are not signed into Steam.) + /// + public static NetIdentity Identity + { + get + { + NetIdentity identity = default; + + Internal?.GetIdentity( ref identity ); + + return identity; + } + } + + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamNetworkingSockets( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; + InstallEvents( server ); + return true; } #region SocketInterface @@ -66,6 +88,7 @@ namespace Steamworks internal void InstallEvents( bool server ) { Dispatch.Install( ConnectionStatusChanged, server ); + Dispatch.Install( FakeIPResult, server ); } @@ -90,12 +113,25 @@ namespace Steamworks public static event Action? OnConnectionStatusChanged; + private static void FakeIPResult( SteamNetworkingFakeIPResult_t data ) + { + foreach ( var port in data.Ports ) + { + if ( port == 0 ) continue; + + var address = NetAddress.From( Utility.Int32ToIp( data.IP ), port ); + + OnFakeIPResult?.Invoke( address ); + } + } + + public static event Action? OnFakeIPResult; /// /// Creates a "server" socket that listens for clients to connect to by calling /// Connect, over ordinary UDP (IPv4 or IPv6) /// - /// To use this derive a class from SocketManager and override as much as you want. + /// To use this derive a class from and override as much as you want. /// /// public static T? CreateNormalSocket( NetAddress address ) where T : SocketManager, new() @@ -115,7 +151,7 @@ namespace Steamworks /// Creates a "server" socket that listens for clients to connect to by calling /// Connect, over ordinary UDP (IPv4 or IPv6). /// - /// To use this you should pass a class that inherits ISocketManager. You can use + /// To use this you should pass a class that inherits . You can use /// SocketManager to get connections and send messages, but the ISocketManager class /// will received all the appropriate callbacks. /// @@ -140,7 +176,7 @@ namespace Steamworks } /// - /// Connect to a socket created via CreateListenSocketIP + /// Connect to a socket created via CreateListenSocketIP. /// public static T? ConnectNormal( NetAddress address ) where T : ConnectionManager, new() { @@ -154,7 +190,7 @@ namespace Steamworks } /// - /// Connect to a socket created via CreateListenSocketIP + /// Connect to a socket created via CreateListenSocketIP. /// public static ConnectionManager? ConnectNormal( NetAddress address, IConnectionManager iface ) { @@ -174,9 +210,9 @@ namespace Steamworks } /// - /// Creates a server that will be relayed via Valve's network (hiding the IP and improving ping) + /// Creates a server that will be relayed via Valve's network (hiding the IP and improving ping). /// - /// To use this derive a class from SocketManager and override as much as you want. + /// To use this derive a class from and override as much as you want. /// /// public static T? CreateRelaySocket( int virtualport = 0 ) where T : SocketManager, new() @@ -192,10 +228,10 @@ namespace Steamworks } /// - /// Creates a server that will be relayed via Valve's network (hiding the IP and improving ping) + /// Creates a server that will be relayed via Valve's network (hiding the IP and improving ping). /// - /// To use this you should pass a class that inherits ISocketManager. You can use - /// SocketManager to get connections and send messages, but the ISocketManager class + /// To use this you should pass a class that inherits . You can use + /// to get connections and send messages, but the class /// will received all the appropriate callbacks. /// /// @@ -219,7 +255,7 @@ namespace Steamworks } /// - /// Connect to a relay server + /// Connect to a relay server. /// public static T? ConnectRelay( SteamId serverId, int virtualport = 0 ) where T : ConnectionManager, new() { @@ -232,5 +268,103 @@ namespace Steamworks SetConnectionManager( t.Connection.Id, t ); return t; } + + /// + /// Connect to a relay server. + /// + public static ConnectionManager? ConnectRelay( SteamId serverId, int virtualport, IConnectionManager iface ) + { + if ( Internal is null ) return null; + + NetIdentity identity = serverId; + var options = Array.Empty(); + var connection = Internal.ConnectP2P( ref identity, virtualport, options.Length, options ); + + var t = new ConnectionManager + { + Connection = connection, + Interface = iface + }; + + SetConnectionManager( t.Connection.Id, t ); + return t; + } + + /// + /// Begin asynchronous process of allocating a fake IPv4 address that other + /// peers can use to contact us via P2P. IP addresses returned by this + /// function are globally unique for a given appid. + /// + /// For gameservers, you *must* call this after initializing the SDK but before + /// beginning login. Steam needs to know in advance that FakeIP will be used. + /// + public static bool RequestFakeIP( int numFakePorts = 1 ) + { + return Internal != null && Internal.BeginAsyncRequestFakeIP( numFakePorts ); + } + + /// + /// Return info about the FakeIP and port that we have been assigned, if any. + /// + /// + public static Result GetFakeIP( int fakePortIndex, out NetAddress address ) + { + if ( Internal is null ) + { + address = default; + return Result.Fail; + } + + var pInfo = default( SteamNetworkingFakeIPResult_t ); + + Internal.GetFakeIP( 0, ref pInfo ); + + address = NetAddress.From( Utility.Int32ToIp( pInfo.IP ), pInfo.Ports[fakePortIndex] ); + return pInfo.Result; + } + + /// + /// Creates a server that will be relayed via Valve's network (hiding the IP and improving ping). + /// + /// To use this derive a class from and override as much as you want. + /// + /// + public static T? CreateRelaySocketFakeIP( int fakePortIndex = 0 ) where T : SocketManager, new() + { + if ( Internal is null ) { return null; } + var t = new T(); + var options = Array.Empty(); + t.Socket = Internal.CreateListenSocketP2PFakeIP( 0, options.Length, options ); + t.Initialize(); + SetSocketManager( t.Socket.Id, t ); + return t; + } + + /// + /// Creates a server that will be relayed via Valve's network (hiding the IP and improving ping). + /// + /// To use this you should pass a class that inherits . You can use + /// to get connections and send messages, but the class + /// will received all the appropriate callbacks. + /// + /// + public static SocketManager? CreateRelaySocketFakeIP( int fakePortIndex, ISocketManager intrface ) + { + if ( Internal is null ) { return null; } + + var options = Array.Empty(); + var socket = Internal.CreateListenSocketP2PFakeIP( 0, options.Length, options ); + + var t = new SocketManager + { + Socket = socket, + Interface = intrface + }; + + t.Initialize(); + + SetSocketManager( t.Socket.Id, t ); + return t; + } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs b/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs index 319e5d641..4de408956 100644 --- a/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs +++ b/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs @@ -8,16 +8,20 @@ using Steamworks.Data; namespace Steamworks { /// - /// Undocumented Parental Settings + /// Provides Steam Networking utilities. /// public class SteamNetworkingUtils : SteamSharedClass { internal static ISteamNetworkingUtils? Internal => Interface as ISteamNetworkingUtils; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamNetworkingUtils( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; + InstallCallbacks( server ); + + return true; } static void InstallCallbacks( bool server ) @@ -36,7 +40,7 @@ namespace Steamworks /// /// A function to receive debug network information on. This will do nothing - /// unless you set DebugLevel to something other than None. + /// unless you set to something other than . /// /// You should set this to an appropriate level instead of setting it to the highest /// and then filtering it by hand because a lot of energy is used by creating the strings @@ -64,21 +68,24 @@ namespace Steamworks /// relay network. If you do not call this, the initialization will /// be delayed until the first time you use a feature that requires access /// to the relay network, which will delay that first access. - /// + /// /// You can also call this to force a retry if the previous attempt has failed. /// Performing any action that requires access to the relay network will also /// trigger a retry, and so calling this function is never strictly necessary, /// but it can be useful to call it a program launch time, if access to the /// relay network is anticipated. - /// + /// + /// /// Use GetRelayNetworkStatus or listen for SteamRelayNetworkStatus_t /// callbacks to know when initialization has completed. /// Typically initialization completes in a few seconds. - /// + /// + /// /// Note: dedicated servers hosted in known data centers do *not* need /// to call this, since they do not make routing decisions. However, if /// the dedicated server will be using P2P functionality, it will act as /// a "client" and this should be called. + /// /// public static void InitRelayNetworkAccess() { @@ -121,7 +128,7 @@ namespace Steamworks /// /// If you need ping information straight away, wait on this. It will return - /// immediately if you already have up to date ping data + /// immediately if you already have up to date ping data. /// public static async Task WaitForPingDataAsync( float maxAgeInSeconds = 60 * 5 ) { @@ -141,7 +148,7 @@ namespace Steamworks /// - /// [0 - 100] - Randomly discard N pct of packets + /// [0 - 100] - Randomly discard N pct of packets. /// public static float FakeSendPacketLoss { @@ -150,7 +157,7 @@ namespace Steamworks } /// - /// [0 - 100] - Randomly discard N pct of packets + /// [0 - 100] - Randomly discard N pct of packets. /// public static float FakeRecvPacketLoss { @@ -159,7 +166,7 @@ namespace Steamworks } /// - /// Delay all packets by N ms + /// Delay all packets by N ms. /// public static float FakeSendPacketLag { @@ -168,7 +175,7 @@ namespace Steamworks } /// - /// Delay all packets by N ms + /// Delay all packets by N ms. /// public static float FakeRecvPacketLag { @@ -177,7 +184,7 @@ namespace Steamworks } /// - /// Timeout value (in ms) to use when first connecting + /// Timeout value (in ms) to use when first connecting. /// public static int ConnectionTimeout { @@ -186,7 +193,7 @@ namespace Steamworks } /// - /// Timeout value (in ms) to use after connection is established + /// Timeout value (in ms) to use after connection is established. /// public static int Timeout { @@ -197,7 +204,7 @@ namespace Steamworks /// /// Upper limit of buffered pending bytes to be sent. /// If this is reached SendMessage will return LimitExceeded. - /// Default is 524288 bytes (512k) + /// Default is 524288 bytes (512k). /// public static int SendBufferSize { @@ -205,17 +212,144 @@ namespace Steamworks set => SetConfigInt( NetConfig.SendBufferSize, value ); } + /// + /// Minimum send rate clamp, 0 is no limit. + /// This value will control the min allowed sending rate that + /// bandwidth estimation is allowed to reach. Default is 0 (no-limit) + /// + public static int SendRateMin + { + get => GetConfigInt( NetConfig.SendRateMin ); + set => SetConfigInt( NetConfig.SendRateMin, value ); + } /// - /// Get Debug Information via OnDebugOutput event - /// - /// Except when debugging, you should only use NetDebugOutput.Msg - /// or NetDebugOutput.Warning. For best performance, do NOT + /// Maximum send rate clamp, 0 is no limit. + /// This value will control the max allowed sending rate that + /// bandwidth estimation is allowed to reach. Default is 0 (no-limit) + /// + public static int SendRateMax + { + get => GetConfigInt( NetConfig.SendRateMax ); + set => SetConfigInt( NetConfig.SendRateMax, value ); + } + + /// + /// Nagle time, in microseconds. When SendMessage is called, if + /// the outgoing message is less than the size of the MTU, it will be + /// queued for a delay equal to the Nagle timer value. This is to ensure + /// that if the application sends several small messages rapidly, they are + /// coalesced into a single packet. + /// See historical RFC 896. Value is in microseconds. + /// Default is 5000us (5ms). + /// + public static int NagleTime + { + get => GetConfigInt( NetConfig.NagleTime ); + set => SetConfigInt( NetConfig.NagleTime, value ); + } + + /// + /// Don't automatically fail IP connections that don't have + /// strong auth. On clients, this means we will attempt the connection even if + /// we don't know our identity or can't get a cert. On the server, it means that + /// we won't automatically reject a connection due to a failure to authenticate. + /// (You can examine the incoming connection and decide whether to accept it.) + /// + /// This is a dev configuration value, and you should not let users modify it in + /// production. + /// + /// + public static int AllowWithoutAuth + { + get => GetConfigInt( NetConfig.IP_AllowWithoutAuth ); + set => SetConfigInt( NetConfig.IP_AllowWithoutAuth, value ); + } + + /// + /// Allow unencrypted (and unauthenticated) communication. + /// 0: Not allowed (the default) + /// 1: Allowed, but prefer encrypted + /// 2: Allowed, and preferred + /// 3: Required. (Fail the connection if the peer requires encryption.) + /// + /// This is a dev configuration value, since its purpose is to disable encryption. + /// You should not let users modify it in production. (But note that it requires + /// the peer to also modify their value in order for encryption to be disabled.) + /// + /// + public static int Unencrypted + { + get => GetConfigInt( NetConfig.Unencrypted ); + set => SetConfigInt( NetConfig.Unencrypted, value ); + } + + /// + /// Log RTT calculations for inline pings and replies. + /// + public static int DebugLevelAckRTT + { + get => GetConfigInt( NetConfig.LogLevel_AckRTT ); + set => SetConfigInt( NetConfig.LogLevel_AckRTT, value ); + } + + /// + /// Log SNP packets send. + /// + public static int DebugLevelPacketDecode + { + get => GetConfigInt( NetConfig.LogLevel_PacketDecode ); + set => SetConfigInt( NetConfig.LogLevel_PacketDecode, value ); + } + + /// + /// Log each message send/recv. + /// + public static int DebugLevelMessage + { + get => GetConfigInt( NetConfig.LogLevel_Message ); + set => SetConfigInt( NetConfig.LogLevel_Message, value ); + } + + /// + /// Log dropped packets. + /// + public static int DebugLevelPacketGaps + { + get => GetConfigInt( NetConfig.LogLevel_PacketGaps ); + set => SetConfigInt( NetConfig.LogLevel_PacketGaps, value ); + } + + /// + /// Log P2P rendezvous messages. + /// + public static int DebugLevelP2PRendezvous + { + get => GetConfigInt( NetConfig.LogLevel_P2PRendezvous ); + set => SetConfigInt( NetConfig.LogLevel_P2PRendezvous, value ); + } + + /// + /// Log ping relays. + /// + public static int DebugLevelSDRRelayPings + { + get => GetConfigInt( NetConfig.LogLevel_SDRRelayPings ); + set => SetConfigInt( NetConfig.LogLevel_SDRRelayPings, value ); + } + + /// + /// Get Debug Information via event. + /// + /// Except when debugging, you should only use + /// or . For best performance, do NOT /// request a high detail level and then filter out messages in the callback. - /// + /// + /// /// This incurs all of the expense of formatting the messages, which are then discarded. /// Setting a high priority value (low numeric value) here allows the library to avoid /// doing this work. + /// /// public static NetDebugOutput DebugLevel { @@ -230,12 +364,12 @@ namespace Steamworks } /// - /// So we can remember and provide a Get for DebugLEvel + /// So we can remember and provide a Get for DebugLevel. /// private static NetDebugOutput _debugLevel; /// - /// We need to keep the delegate around until it's not used anymore + /// We need to keep the delegate around until it's not used anymore. /// static NetDebugFunc? _debugFunc; @@ -256,6 +390,11 @@ namespace Steamworks debugMessages.Enqueue( new DebugMessage { Type = nType, Msg = Helpers.MemoryToString( str ) } ); } + internal static void LogDebugMessage( NetDebugOutput type, string message ) + { + debugMessages.Enqueue( new DebugMessage { Type = type, Msg = message } ); + } + /// /// Called regularly from the Dispatch loop so we can provide a timely /// stream of messages. @@ -271,6 +410,11 @@ namespace Steamworks } } + internal static unsafe NetMsg* AllocateMessage() + { + return Internal != null ? Internal.AllocateMessage(0) : null; + } + #region Config Internals internal unsafe static bool SetConfigInt( NetConfig type, int value ) diff --git a/Libraries/Facepunch.Steamworks/SteamParental.cs b/Libraries/Facepunch.Steamworks/SteamParental.cs index 7f3a23bd6..d110cb301 100644 --- a/Libraries/Facepunch.Steamworks/SteamParental.cs +++ b/Libraries/Facepunch.Steamworks/SteamParental.cs @@ -14,10 +14,14 @@ namespace Steamworks { internal static ISteamParentalSettings? Internal => Interface as ISteamParentalSettings; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamParentalSettings( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; + InstallEvents( server ); + + return true; } internal static void InstallEvents( bool server ) @@ -61,4 +65,4 @@ namespace Steamworks /// public static bool BIsFeatureInBlockList( ParentalFeature feature ) => Internal != null && Internal.BIsFeatureInBlockList( feature ); } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamParties.cs b/Libraries/Facepunch.Steamworks/SteamParties.cs index 1d7c1eba3..022c3e178 100644 --- a/Libraries/Facepunch.Steamworks/SteamParties.cs +++ b/Libraries/Facepunch.Steamworks/SteamParties.cs @@ -17,10 +17,14 @@ namespace Steamworks { internal static ISteamParties? Internal => Interface as ISteamParties; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamParties( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; + InstallEvents( server ); + + return true; } internal void InstallEvents( bool server ) @@ -30,18 +34,23 @@ namespace Steamworks } /// - /// The list of possible Party beacon locations has changed + /// Invoked when the list of possible Party beacon locations has changed /// public static event Action? OnBeaconLocationsUpdated; /// - /// The list of active beacons may have changed + /// Invoked when the list of active beacons may have changed /// public static event Action? OnActiveBeaconsUpdated; - + /// + /// Gets the amount of beacons that are active. + /// public static int ActiveBeaconCount => (int)(Internal?.GetNumActiveBeacons() ?? 0); + /// + /// Gets an of active beacons. + /// public static IEnumerable ActiveBeacons { get @@ -56,4 +65,4 @@ namespace Steamworks } } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamRemotePlay.cs b/Libraries/Facepunch.Steamworks/SteamRemotePlay.cs index 502d37291..ebe125b42 100644 --- a/Libraries/Facepunch.Steamworks/SteamRemotePlay.cs +++ b/Libraries/Facepunch.Steamworks/SteamRemotePlay.cs @@ -14,11 +14,14 @@ namespace Steamworks { internal static ISteamRemotePlay? Internal => Interface as ISteamRemotePlay; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamRemotePlay( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; InstallEvents( server ); + + return true; } internal void InstallEvents( bool server ) @@ -28,30 +31,30 @@ namespace Steamworks } /// - /// Called when a session is connected + /// Invoked when a session is connected. /// public static event Action? OnSessionConnected; /// - /// Called when a session becomes disconnected + /// Invoked when a session becomes disconnected. /// public static event Action? OnSessionDisconnected; /// - /// Get the number of currently connected Steam Remote Play sessions + /// Gets the number of currently connected Steam Remote Play sessions /// public static int SessionCount => (int)(Internal?.GetSessionCount() ?? 0); /// /// Get the currently connected Steam Remote Play session ID at the specified index. - /// IsValid will return false if it's out of bounds + /// IsValid will return if it's out of bounds /// public static RemotePlaySession GetSession( int index ) => Internal?.GetSessionID( index ).Value ?? default; /// - /// Invite a friend to Remote Play Together - /// This returns false if the invite can't be sent + /// Invite a friend to Remote Play Together. + /// This returns if the invite can't be sent /// public static bool SendInvite( SteamId steamid ) => Internal != null && Internal.BSendRemotePlayTogetherInvite( steamid ); } diff --git a/Libraries/Facepunch.Steamworks/SteamRemoteStorage.cs b/Libraries/Facepunch.Steamworks/SteamRemoteStorage.cs index 847147680..02d28e951 100644 --- a/Libraries/Facepunch.Steamworks/SteamRemoteStorage.cs +++ b/Libraries/Facepunch.Steamworks/SteamRemoteStorage.cs @@ -8,22 +8,28 @@ using Steamworks.Data; namespace Steamworks { /// - /// Undocumented Parental Settings + /// Class for utilizing the Steam Remote Storage API. /// public class SteamRemoteStorage : SteamClientClass { internal static ISteamRemoteStorage? Internal => Interface as ISteamRemoteStorage; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamRemoteStorage( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; + + return true; } - + /// /// Creates a new file, writes the bytes to the file, and then closes the file. /// If the target file already exists, it is overwritten /// + /// The path of the file. + /// The bytes of data. + /// A boolean, detailing whether or not the operation was successful. public unsafe static bool FileWrite( string filename, byte[] data ) { fixed ( byte* ptr = data ) @@ -35,6 +41,7 @@ namespace Steamworks /// /// Opens a binary file, reads the contents of the file into a byte array, and then closes the file. /// + /// The path of the file. public unsafe static byte[]? FileRead( string filename ) { if (Internal is null) { return null; } @@ -46,6 +53,10 @@ namespace Steamworks fixed ( byte* ptr = buffer ) { var readsize = Internal.FileRead( filename, (IntPtr)ptr, size ); + if ( readsize != size ) + { + return null; + } return buffer; } } @@ -53,26 +64,35 @@ namespace Steamworks /// /// Checks whether the specified file exists. /// + /// The path of the file. + /// Whether or not the file exists. public static bool FileExists( string filename ) => Internal != null && Internal.FileExists( filename ); /// /// Checks if a specific file is persisted in the steam cloud. /// + /// The path of the file. + /// Boolean. public static bool FilePersisted( string filename ) => Internal != null && Internal.FilePersisted( filename ); /// /// Gets the specified file's last modified date/time. /// + /// The path of the file. + /// A describing when the file was modified last. public static DateTime FileTime( string filename ) => Internal != null ? Epoch.ToDateTime( Internal.GetFileTimestamp( filename ) ) : default; /// - /// Gets the specified files size in bytes. 0 if not exists. + /// Returns the specified files size in bytes, or 0 if the file does not exist. /// + /// The path of the file. + /// The size of the file in bytes, or 0 if the file doesn't exist. public static int FileSize( string filename ) => Internal?.GetFileSize( filename ) ?? 0; /// /// Deletes the file from remote storage, but leaves it on the local disk and remains accessible from the API. /// + /// A boolean, detailing whether or not the operation was successful. public static bool FileForget( string filename ) => Internal != null && Internal.FileForget( filename ); /// @@ -82,7 +102,7 @@ namespace Steamworks /// - /// Number of bytes total + /// Gets the total number of quota bytes. /// public static ulong QuotaBytes { @@ -95,7 +115,7 @@ namespace Steamworks } /// - /// Number of bytes used + /// Gets the total number of quota bytes that have been used. /// public static ulong QuotaUsedBytes { @@ -108,7 +128,7 @@ namespace Steamworks } /// - /// Number of bytes remaining until your quota is used + /// Number of bytes remaining until the quota is used. /// public static ulong QuotaRemainingBytes { @@ -121,7 +141,7 @@ namespace Steamworks } /// - /// returns true if IsCloudEnabledForAccount AND IsCloudEnabledForApp + /// returns if AND are . /// public static bool IsCloudEnabled => IsCloudEnabledForAccount && IsCloudEnabledForApp; @@ -162,7 +182,7 @@ namespace Steamworks } /// - /// Get a list of filenames synchronized by Steam Cloud + /// Gets a list of filenames synchronized by Steam Cloud. /// public static List Files { @@ -182,4 +202,4 @@ namespace Steamworks } } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamScreenshots.cs b/Libraries/Facepunch.Steamworks/SteamScreenshots.cs index 2c91a7eb4..36b322f87 100644 --- a/Libraries/Facepunch.Steamworks/SteamScreenshots.cs +++ b/Libraries/Facepunch.Steamworks/SteamScreenshots.cs @@ -8,16 +8,20 @@ using Steamworks.Data; namespace Steamworks { /// - /// Undocumented Parental Settings + /// Class for utilizing the Steam Screenshots API. /// public class SteamScreenshots : SteamClientClass { internal static ISteamScreenshots? Internal => Interface as ISteamScreenshots; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamScreenshots( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; + InstallEvents(); + + return true; } internal static void InstallEvents() @@ -33,19 +37,19 @@ namespace Steamworks } /// - /// A screenshot has been requested by the user from the Steam screenshot hotkey. - /// This will only be called if Hooked is true, in which case Steam + /// Invoked when a screenshot has been requested by the user from the Steam screenshot hotkey. + /// This will only be called if is true, in which case Steam /// will not take the screenshot itself. /// public static event Action? OnScreenshotRequested; /// - /// A screenshot successfully written or otherwise added to the library and can now be tagged. + /// Invoked when a screenshot has been successfully written or otherwise added to the library and can now be tagged. /// public static event Action? OnScreenshotReady; /// - /// A screenshot attempt failed + /// Invoked when a screenshot attempt failed. /// public static event Action? OnScreenshotFailed; @@ -85,15 +89,17 @@ namespace Steamworks /// /// Causes the Steam overlay to take a screenshot. /// If screenshots are being hooked by the game then a - /// ScreenshotRequested callback is sent back to the game instead. + /// callback is sent back to the game instead. /// public static void TriggerScreenshot() => Internal?.TriggerScreenshot(); /// /// Toggles whether the overlay handles screenshots when the user presses the screenshot hotkey, or if the game handles them. + /// /// Hooking is disabled by default, and only ever enabled if you do so with this function. - /// If the hooking is enabled, then the ScreenshotRequested_t callback will be sent if the user presses the hotkey or - /// when TriggerScreenshot is called, and then the game is expected to call WriteScreenshot or AddScreenshotToLibrary in response. + /// If the hooking is enabled, then the callback will be sent if the user presses the hotkey or + /// when TriggerScreenshot is called, and then the game is expected to call or in response. + /// /// public static bool Hooked { @@ -101,4 +107,4 @@ namespace Steamworks set => Internal?.HookScreenshots( value ); } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamServer.cs b/Libraries/Facepunch.Steamworks/SteamServer.cs index b3ea1bef5..d2a264969 100644 --- a/Libraries/Facepunch.Steamworks/SteamServer.cs +++ b/Libraries/Facepunch.Steamworks/SteamServer.cs @@ -15,10 +15,14 @@ namespace Steamworks { internal static ISteamGameServer? Internal => Interface as ISteamGameServer; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamGameServer( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; + InstallEvents(); + + return true; } public static bool IsValid => Internal != null && Internal.IsValid; @@ -33,35 +37,35 @@ namespace Steamworks } /// - /// User has been authed or rejected + /// Invoked when aser has been authed or rejected /// public static event Action? OnValidateAuthTicketResponse; /// - /// Called when a connections to the Steam back-end has been established. + /// Invoked when a connection to the Steam back-end has been established. /// This means the server now is logged on and has a working connection to the Steam master server. /// public static event Action? OnSteamServersConnected; /// - /// This will occur periodically if the Steam client is not connected, and has failed when retrying to establish a connection (result, stilltrying) + /// This will occur periodically if the Steam client is not connected, and has failed when retrying to establish a connection (result, stilltrying). /// public static event Action? OnSteamServerConnectFailure; /// - /// Disconnected from Steam + /// Invoked when the server is disconnected from Steam /// public static event Action? OnSteamServersDisconnected; /// - /// Called when authentication status changes, useful for grabbing SteamId once aavailability is current + /// Invoked when authentication status changes, useful for grabbing once availability is current. /// public static event Action? OnSteamNetAuthenticationStatus; /// /// Initialize the steam server. - /// If asyncCallbacks is false you need to call RunCallbacks manually every frame. + /// If is you need to call manually every frame. /// public static void Init( AppId appid, SteamServerInit init, bool asyncCallbacks = true ) { @@ -70,9 +74,6 @@ namespace Steamworks uint ipaddress = 0; // Any Port - if ( init.SteamPort == 0 ) - init = init.WithRandomSteamPort(); - if ( init.IpAddress != null ) ipaddress = Utility.IpToInt32( init.IpAddress ); @@ -82,9 +83,9 @@ namespace Steamworks // // Get other interfaces // - if ( !SteamInternal.GameServer_Init( ipaddress, init.SteamPort, init.GamePort, init.QueryPort, (int)init.Mode, init.VersionString ) ) + if ( !SteamInternal.GameServer_Init( ipaddress, 0, init.GamePort, init.QueryPort, (int)init.Mode, init.VersionString ) ) { - throw new System.Exception( $"InitGameServer returned false ({ipaddress},{init.SteamPort},{init.GamePort},{init.QueryPort},{init.Mode},\"{init.VersionString}\")" ); + throw new System.Exception( $"InitGameServer returned false ({ipaddress},{0},{init.GamePort},{init.QueryPort},{init.Mode},\"{init.VersionString}\")" ); } // @@ -109,7 +110,7 @@ namespace Steamworks // // Initial settings // - AutomaticHeartbeats = true; + AdvertiseServer = true; MaxPlayers = 32; BotCount = 0; Product = $"{appid.Value}"; @@ -210,7 +211,7 @@ namespace Steamworks private static string _mapname = ""; /// - /// Gets or sets the current ModDir + /// Gets or sets the current ModDir. /// public static string ModDir { @@ -220,7 +221,7 @@ namespace Steamworks private static string _modDir = ""; /// - /// Gets the current product + /// Gets the current product. /// public static string Product { @@ -230,7 +231,7 @@ namespace Steamworks private static string _product = ""; /// - /// Gets or sets the current Product + /// Gets or sets the current Product. /// public static string GameDescription { @@ -240,7 +241,7 @@ namespace Steamworks private static string _gameDescription = ""; /// - /// Gets or sets the current ServerName + /// Gets or sets the current ServerName. /// public static string ServerName { @@ -250,7 +251,7 @@ namespace Steamworks private static string _serverName = ""; /// - /// Set whether the server should report itself as passworded + /// Set whether the server should report itself as passworded. /// public static bool Passworded { @@ -275,6 +276,9 @@ namespace Steamworks } private static string _gametags = ""; + /// + /// Gets the SteamId of the server. + /// public static SteamId SteamId => Internal?.GetSteamID() ?? default; /// @@ -287,7 +291,7 @@ namespace Steamworks } /// - /// Log onto Steam anonymously. + /// Log off of Steam. /// public static void LogOff() { @@ -296,14 +300,14 @@ namespace Steamworks /// /// Returns true if the server is connected and registered with the Steam master server - /// You should have called LogOnAnonymous etc on startup. + /// You should have called etc on startup. /// public static bool LoggedOn => Internal != null && Internal.BLoggedOn(); /// /// To the best of its ability this tries to get the server's - /// current public ip address. Be aware that this is likely to return - /// null for the first few seconds after initialization. + /// current public IP address. Be aware that this is likely to return + /// for the first few seconds after initialization. /// public static System.Net.IPAddress? PublicIp => Internal?.GetPublicIP(); @@ -311,27 +315,29 @@ namespace Steamworks /// Enable or disable heartbeats, which are sent regularly to the master server. /// Enabled by default. /// + [Obsolete( "Renamed to AdvertiseServer in 1.52" )] public static bool AutomaticHeartbeats { - set { Internal?.EnableHeartbeats( value ); } - } + set { Internal?.SetAdvertiseServerActive( value ); } + } + /// - /// Set heartbeat interval, if automatic heartbeats are enabled. - /// You can leave this at the default. + /// Enable or disable heartbeats, which are sent regularly to the master server. + /// Enabled by default. /// - public static int AutomaticHeartbeatRate + public static bool AdvertiseServer { - set { Internal?.SetHeartbeatInterval( value ); } + set { Internal?.SetAdvertiseServerActive( value ); } } /// /// Force send a heartbeat to the master server instead of waiting /// for the next automatic update (if you've left them enabled) /// + [Obsolete( "No longer used" )] public static void ForceHeartbeat() { - Internal?.ForceHeartbeat(); } /// @@ -372,7 +378,7 @@ namespace Steamworks } /// - /// Remove all key values + /// Remove all key values. /// public static void ClearKeys() { @@ -456,11 +462,11 @@ namespace Steamworks } /// - /// Does the user own this app (which could be DLC) + /// Does the user own this app (which could be DLC). /// public static UserHasLicenseForAppResult UserHasLicenseForApp( SteamId steamid, AppId appid ) { return Internal?.UserHasLicenseForApp( steamid, appid ) ?? UserHasLicenseForAppResult.NoAuth; } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamServerStats.cs b/Libraries/Facepunch.Steamworks/SteamServerStats.cs index 823a92771..86232fb0c 100644 --- a/Libraries/Facepunch.Steamworks/SteamServerStats.cs +++ b/Libraries/Facepunch.Steamworks/SteamServerStats.cs @@ -11,17 +11,22 @@ namespace Steamworks { internal static ISteamGameServerStats? Internal => Interface as ISteamGameServerStats; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamGameServerStats( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; + + return true; } /// - /// Downloads stats for the user - /// If the user has no stats will return fail - /// these stats will only be auto-updated for clients playing on the server + /// Downloads stats for the user. + /// If the user has no stats, this will return . + /// These stats will only be auto-updated for clients playing on the server. /// + /// The SteamId of the user to get stats for. + /// A task describing the progress and result of the download. public static async Task RequestUserStatsAsync( SteamId steamid ) { if (Internal is null) { return Result.Fail; } @@ -34,6 +39,9 @@ namespace Steamworks /// Set the named stat for this user. Setting stats should follow the rules /// you defined in Steamworks. /// + /// The SteamId of the user to set stats for. + /// The name of the stat. + /// The value of the stat. public static bool SetInt( SteamId steamid, string name, int stat ) { return Internal != null && Internal.SetUserStat( steamid, name, stat ); @@ -43,6 +51,9 @@ namespace Steamworks /// Set the named stat for this user. Setting stats should follow the rules /// you defined in Steamworks. /// + /// The SteamId of the user to set stats for. + /// The name of the stat. + /// The value of the stat. public static bool SetFloat( SteamId steamid, string name, float stat ) { return Internal != null && Internal.SetUserStat( steamid, name, stat ); @@ -50,9 +61,12 @@ namespace Steamworks /// /// Get the named stat for this user. If getting the stat failed, will return - /// defaultValue. You should have called Refresh for this userid - which downloads - /// the stats from the backend. If you didn't call it this will always return defaultValue. + /// . You should have called for this SteamID - which downloads + /// the stats from the backend. If you didn't call it this will always return . /// + /// The SteamId of the user to get stats for. + /// The name of the stat. + /// The value to return if the stats cannot be received. public static int GetInt( SteamId steamid, string name, int defaultValue = 0 ) { int data = defaultValue; @@ -68,6 +82,9 @@ namespace Steamworks /// defaultValue. You should have called Refresh for this userid - which downloads /// the stats from the backend. If you didn't call it this will always return defaultValue. /// + /// The SteamId of the user to get stats for. + /// The name of the stat. + /// The value to return if the stats cannot be received. public static float GetFloat( SteamId steamid, string name, float defaultValue = 0 ) { float data = defaultValue; @@ -79,25 +96,29 @@ namespace Steamworks } /// - /// Unlocks the specified achievement for the specified user. Must have called Refresh on a steamid first. - /// Remember to use Commit after use. + /// Unlocks the specified achievement for the specified user. Must have called on a SteamID first. + /// Remember to use after use. /// + /// The SteamId of the user to unlock the achievement for. + /// The ID of the achievement. public static bool SetAchievement( SteamId steamid, string name ) { return Internal != null && Internal.SetUserAchievement( steamid, name ); } /// - /// Resets the unlock status of an achievement for the specified user. Must have called Refresh on a steamid first. - /// Remember to use Commit after use. + /// Resets the unlock status of an achievement for the specified user. Must have called on a SteamID first. + /// Remember to use after use. /// + /// The SteamId of the user to clear the achievement for. + /// The ID of the achievement. public static bool ClearAchievement( SteamId steamid, string name ) { return Internal != null && Internal.ClearUserAchievement( steamid, name ); } /// - /// Return true if available, exists and unlocked + /// Return if available, exists and unlocked /// public static bool GetAchievement( SteamId steamid, string name ) { @@ -111,9 +132,11 @@ namespace Steamworks /// /// Once you've set a stat change on a user you need to commit your changes. - /// You can do that using this function. The callback will let you know if + /// You can do that using this method. The callback will let you know if /// your action succeeded, but most of the time you can fire and forget. /// + /// The SteamId of the user to store stats for. + /// A task describing the progress and result of the commit. public static async Task StoreUserStats( SteamId steamid ) { if (Internal is null) { return Result.Fail; } @@ -122,4 +145,4 @@ namespace Steamworks return r.Value.Result; } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamUgc.cs b/Libraries/Facepunch.Steamworks/SteamUgc.cs index ecf20286e..af6f1c36c 100644 --- a/Libraries/Facepunch.Steamworks/SteamUgc.cs +++ b/Libraries/Facepunch.Steamworks/SteamUgc.cs @@ -17,34 +17,59 @@ namespace Steamworks { internal static ISteamUGC? Internal => Interface as ISteamUGC; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamUGC( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; + InstallEvents( server ); + + return true; } internal static void InstallEvents( bool server ) { Dispatch.Install( x => { - if (x.AppID == SteamClient.AppId) + if ( x.AppID == SteamClient.AppId ) { - OnDownloadItemResult?.Invoke(x.Result, x.PublishedFileId); - } - }, server ); - Dispatch.Install(x => - { - if (x.AppID == SteamClient.AppId) - { - GlobalOnItemInstalled?.Invoke(x.PublishedFileId); + OnDownloadItemResult?.Invoke( x.Result, x.PublishedFileId ); } }, server); + Dispatch.Install( x => + { + if ( x.AppID == SteamClient.AppId ) + { + OnItemSubscribed?.Invoke( x.AppID.Value, x.PublishedFileId ); + } + }, server ); + Dispatch.Install( x => + { + if ( x.AppID == SteamClient.AppId ) + { + OnItemUnsubscribed?.Invoke( x.AppID.Value, x.PublishedFileId ); + } + }, server ); + Dispatch.Install( x => + { + if ( x.AppID == SteamClient.AppId ) + { + OnItemInstalled?.Invoke( x.AppID.Value, x.PublishedFileId ); + } + }, server ); } /// - /// Posted after Download call + /// Invoked after an item is downloaded. /// - public static event Action? OnDownloadItemResult; + public static event Action? OnDownloadItemResult; + + /// + /// Invoked when a new item is subscribed. + /// + public static event Action? OnItemSubscribed; + public static event Action? OnItemUnsubscribed; + public static event Action? OnItemInstalled; public static async Task DeleteFileAsync( PublishedFileId fileId ) { @@ -54,29 +79,29 @@ namespace Steamworks } /// - /// Start downloading this item. You'll get notified of completion via OnDownloadItemResult. + /// Start downloading this item. You'll get notified of completion via . /// - /// The ID of the file you want to download - /// If true this should go straight to the top of the download list - /// true if nothing went wrong and the download is started + /// The ID of the file to download. + /// If this should go straight to the top of the download list. + /// if nothing went wrong and the download is started. public static bool Download( PublishedFileId fileId, bool highPriority = false ) { return Internal != null && Internal.DownloadItem( fileId, highPriority ); } /// - /// Will attempt to download this item asyncronously - allowing you to instantly react to its installation + /// Will attempt to download this item asyncronously - allowing you to instantly react to its installation. /// - /// The ID of the file you want to download + /// The ID of the file you download. /// An optional callback - /// Allows you to send a message to cancel the download anywhere during the process - /// How often to call the progress function - /// true if downloaded and installed correctly + /// Allows to send a message to cancel the download anywhere during the process. + /// How often to call the progress function. + /// if downloaded and installed properly. public static async Task DownloadAsync( - PublishedFileId fileId, - Action? progress = null, - int millisecondsUpdateDelay = 60, - CancellationToken? ct = null) + PublishedFileId fileId, + Action? progress = null, + int millisecondsUpdateDelay = 60, + CancellationToken? ct = default ) { var item = new Steamworks.Ugc.Item( fileId ); @@ -92,7 +117,7 @@ namespace Steamworks Result downloadStartResult = Result.None; - void onDownloadFinished(Result r, ulong id) + void onDownloadFinished(Result r, PublishedFileId id) { if (id != item.Id) { return; } downloadStartResult = r; @@ -143,7 +168,7 @@ namespace Steamworks } /// - /// Utility function to fetch a single item. Internally this uses Ugc.FileQuery - + /// Utility function to fetch a single item. Internally this uses Ugc.FileQuery - /// which you can use to query multiple items if you need to. /// public static async Task QueryFileAsync( PublishedFileId fileId ) @@ -183,8 +208,6 @@ namespace Steamworks return result?.Result == Result.OK; } - public static Action? GlobalOnItemInstalled; - public static uint NumSubscribedItems { get { return Internal?.GetNumSubscribedItems() ?? 0; } } public static PublishedFileId[] GetSubscribedItems() @@ -195,6 +218,36 @@ namespace Steamworks Internal.GetSubscribedItems(ids, numSubscribed); return ids; } + + /// + /// Suspends all workshop downloads. + /// Downloads will be suspended until you resume them by calling or when the game ends. + /// + public static void SuspendDownloads() => Internal?.SuspendDownloads(true); + + /// + /// Resumes all workshop downloads. + /// + public static void ResumeDownloads() => Internal?.SuspendDownloads(false); + + /// + /// Show the app's latest Workshop EULA to the user in an overlay window, where they can accept it or not. + /// + public static bool ShowWorkshopEula() + { + return Internal != null && Internal.ShowWorkshopEULA(); + } + + /// + /// Retrieve information related to the user's acceptance or not of the app's specific Workshop EULA. + /// + public static async Task GetWorkshopEulaStatus() + { + if ( Internal is null ) { return null; } + var status = await Internal.GetWorkshopEULAStatus(); + return status?.Accepted; + } + } } diff --git a/Libraries/Facepunch.Steamworks/SteamUser.cs b/Libraries/Facepunch.Steamworks/SteamUser.cs index 8f4c6620e..a0afe522a 100644 --- a/Libraries/Facepunch.Steamworks/SteamUser.cs +++ b/Libraries/Facepunch.Steamworks/SteamUser.cs @@ -17,13 +17,17 @@ namespace Steamworks { internal static ISteamUser? Internal => Interface as ISteamUser; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamUser( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; + InstallEvents(); richPresence = new Dictionary(); SampleRate = OptimalSampleRate; + + return true; } static Dictionary? richPresence; @@ -39,11 +43,12 @@ namespace Steamworks Dispatch.Install( x => OnMicroTxnAuthorizationResponse?.Invoke( x.AppID, x.OrderID, x.Authorized != 0 ) ); Dispatch.Install( x => OnGameWebCallback?.Invoke( x.URLUTF8() ) ); Dispatch.Install( x => OnGetAuthSessionTicketResponse?.Invoke( x ) ); + Dispatch.Install( x => OnGetAuthTicketForWebApiResponse?.Invoke( x ) ); Dispatch.Install( x => OnDurationControl?.Invoke( new DurationControl { _inner = x } ) ); } /// - /// Called when a connections to the Steam back-end has been established. + /// Invoked when a connections to the Steam back-end has been established. /// This means the Steam client now has a working connection to the Steam servers. /// Usually this will have occurred before the game has launched, and should only be seen if the /// user has dropped connection due to a networking issue or a Steam server update. @@ -51,14 +56,14 @@ namespace Steamworks public static event Action? OnSteamServersConnected; /// - /// Called when a connection attempt has failed. + /// Invoked when a connection attempt has failed. /// This will occur periodically if the Steam client is not connected, /// and has failed when retrying to establish a connection. /// public static event Action? OnSteamServerConnectFailure; /// - /// Called if the client has lost connection to the Steam servers. + /// Invoked when the client has lost connection to the Steam servers. /// Real-time services will be disabled until a matching OnSteamServersConnected has been posted. /// public static event Action? OnSteamServersDisconnected; @@ -72,25 +77,30 @@ namespace Steamworks public static event Action? OnClientGameServerDeny; /// - /// Called whenever the users licenses (owned packages) changes. + /// Invoked whenever the users licenses (owned packages) changes. /// public static event Action? OnLicensesUpdated; /// - /// Called when an auth ticket has been validated. - /// The first parameter is the steamid of this user - /// The second is the Steam ID that owns the game, this will be different from the first - /// if the game is being borrowed via Steam Family Sharing + /// Invoked when an auth ticket has been validated. + /// The first parameter is the of this user + /// The second is the that owns the game, which will be different from the first + /// if the game is being borrowed via Steam Family Sharing. /// public static event Action? OnValidateAuthTicketResponse; /// - /// Used internally for GetAuthSessionTicketAsync + /// Used internally for . /// internal static event Action? OnGetAuthSessionTicketResponse; + + /// + /// Used internally for . + /// + internal static event Action? OnGetAuthTicketForWebApiResponse; /// - /// Called when a user has responded to a microtransaction authorization request. + /// Invoked when a user has responded to a microtransaction authorization request. /// ( appid, orderid, user authorized ) /// public static event Action? OnMicroTxnAuthorizationResponse; @@ -110,9 +120,6 @@ namespace Steamworks /// public static event Action? OnDurationControl; - - - static bool _recordingVoice; /// @@ -120,7 +127,6 @@ namespace Steamworks /// Once started, use GetAvailableVoice and GetVoice to get the data, and then call StopVoiceRecording /// when the user has released their push-to-talk hotkey or the game session has completed. /// - public static bool VoiceRecord { get => _recordingVoice; @@ -134,7 +140,7 @@ namespace Steamworks /// - /// Returns true if we have voice data waiting to be read + /// Returns true if we have voice data waiting to be read. /// public static bool HasVoiceData { @@ -304,16 +310,16 @@ namespace Steamworks } /// - /// Retrieve a authentication ticket to be sent to the entity who wishes to authenticate you. + /// Retrieve an authentication ticket to be sent to the entity who wishes to authenticate you. /// - public static unsafe AuthTicket? GetAuthSessionTicket() + public static unsafe AuthTicket? GetAuthSessionTicket( NetIdentity identity ) { var data = Helpers.TakeBuffer( 1024 ); fixed ( byte* b = data ) { uint ticketLength = 0; - uint ticket = Internal?.GetAuthSessionTicket( (IntPtr)b, data.Length, ref ticketLength ) ?? 0; + uint ticket = Internal?.GetAuthSessionTicket( (IntPtr)b, data.Length, ref ticketLength, ref identity ) ?? 0; if ( ticket == 0 ) return null; @@ -326,13 +332,56 @@ namespace Steamworks } } + public static async Task GetAuthTicketForWebApi( string identity ) + { + if ( Internal is null ) { return null; } + + HAuthTicket handle = default; + AuthTicketForWebApi? ticket = null; + Result result = Result.Pending; + + Action responseHandler = response => + { + if ( response.Ticket == handle ) { return; } + + result = response.Result == Result.Pending + ? Result.Fail + : response.Result; + ticket = result == Result.OK + ? new AuthTicketForWebApi( + response.GubTicket.Take( response.Ticket ).ToArray(), + response.AuthTicket ) + : null; + }; + + OnGetAuthTicketForWebApiResponse += responseHandler; + try + { + handle = Internal.GetAuthTicketForWebApi( identity ); + + if ( handle == 0 ) { return null; } + + var timeout = DateTime.Now + TimeSpan.FromSeconds( 60f ); + while ( result == Result.Pending && DateTime.Now < timeout ) + { + await Task.Delay( 10 ); + } + } + finally + { + OnGetAuthTicketForWebApiResponse -= responseHandler; + } + + return ticket; + } + /// /// Retrieve a authentication ticket to be sent to the entity who wishes to authenticate you. /// This waits for a positive response from the backend before returning the ticket. This means - /// the ticket is definitely ready to go as soon as it returns. Will return null if the callback + /// the ticket is definitely ready to go as soon as it returns. Will return if the callback /// times out or returns negatively. /// - public static async Task GetAuthSessionTicketAsync( double timeoutSeconds = 10.0f ) + public static async Task GetAuthSessionTicketAsync( NetIdentity identity, double timeoutSeconds = 10.0f ) { var result = Result.Pending; AuthTicket? ticket = null; @@ -348,7 +397,7 @@ namespace Steamworks try { - ticket = GetAuthSessionTicket(); + ticket = GetAuthSessionTicket( identity ); if ( ticket == null ) return null; @@ -518,4 +567,4 @@ namespace Steamworks return new DurationControl { _inner = response.Value }; } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamUserStats.cs b/Libraries/Facepunch.Steamworks/SteamUserStats.cs index f9fc6fa5d..bf0a267a0 100644 --- a/Libraries/Facepunch.Steamworks/SteamUserStats.cs +++ b/Libraries/Facepunch.Steamworks/SteamUserStats.cs @@ -11,11 +11,15 @@ namespace Steamworks { internal static ISteamUserStats? Internal => Interface as ISteamUserStats; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamUserStats( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; + InstallEvents(); RequestCurrentStats(); + + return true; } public static bool StatsRecieved { get; internal set; } @@ -38,25 +42,25 @@ namespace Steamworks /// - /// called when the achivement icon is loaded + /// Invoked when an achivement icon is loaded. /// internal static event Action? OnAchievementIconFetched; /// - /// called when the latests stats and achievements have been received - /// from the server + /// Invoked when the latests stats and achievements have been received + /// from the server. /// public static event Action? OnUserStatsReceived; /// - /// result of a request to store the user stats for a game + /// Result of a request to store the user stats for a game. /// public static event Action? OnUserStatsStored; /// - /// result of a request to store the achievements for a game, or an + /// Result of a request to store the achievements for a game, or an /// "indicate progress" call. If both m_nCurProgress and m_nMaxProgress - /// are zero, that means the achievement has been fully unlocked + /// are zero, that means the achievement has been fully unlocked. /// public static event Action? OnAchievementProgress; @@ -67,7 +71,7 @@ namespace Steamworks public static event Action? OnUserStatsUnloaded; /// - /// Get the available achievements + /// Get all available achievements. /// public static IEnumerable Achievements { @@ -104,7 +108,7 @@ namespace Steamworks /// /// Tries to get the number of players currently playing this game. - /// Or -1 if failed. + /// Or -1 if failed. /// public static async Task PlayerCountAsync() { @@ -146,11 +150,11 @@ namespace Steamworks /// /// Asynchronously fetches global stats data, which is available for stats marked as /// "aggregated" in the App Admin panel of the Steamworks website. - /// You must have called RequestCurrentStats and it needs to return successfully via + /// You must have called and it needs to return successfully via /// its callback prior to calling this. /// - /// How many days of day-by-day history to retrieve in addition to the overall totals. The limit is 60. - /// OK indicates success, InvalidState means you need to call RequestCurrentStats first, Fail means the remote call failed + /// How many days of day-by-day history to retrieve in addition to the overall totals. The limit is 60. + /// indicates success, means you need to call first, means the remote call failed public static async Task RequestGlobalStatsAsync( int days ) { if (Internal is null) { return Result.Fail; } @@ -216,8 +220,7 @@ namespace Steamworks } /// - /// Set a stat value. This will automatically call StoreStats() after a successful call - /// unless you pass false as the last argument. + /// Set a stat value. This will automatically call after a successful call. /// public static bool SetStat( string name, int value ) { @@ -225,8 +228,7 @@ namespace Steamworks } /// - /// Set a stat value. This will automatically call StoreStats() after a successful call - /// unless you pass false as the last argument. + /// Set a stat value. This will automatically call after a successful call. /// public static bool SetStat( string name, float value ) { @@ -234,7 +236,7 @@ namespace Steamworks } /// - /// Get a Int stat value + /// Get an stat value. /// public static int GetStatInt( string name ) { @@ -244,7 +246,7 @@ namespace Steamworks } /// - /// Get a float stat value + /// Get a stat value. /// public static float GetStatFloat( string name ) { @@ -254,7 +256,7 @@ namespace Steamworks } /// - /// Practically wipes the slate clean for this user. If includeAchievements is true, will wipe + /// Practically wipes the slate clean for this user. If is , will also wipe /// any achievements too. /// /// @@ -263,4 +265,4 @@ namespace Steamworks return Internal != null && Internal.ResetAllStats( includeAchievements ); } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamUtils.cs b/Libraries/Facepunch.Steamworks/SteamUtils.cs index 3f1553c23..94bd90158 100644 --- a/Libraries/Facepunch.Steamworks/SteamUtils.cs +++ b/Libraries/Facepunch.Steamworks/SteamUtils.cs @@ -14,10 +14,14 @@ namespace Steamworks { internal static ISteamUtils? Internal => Interface as ISteamUtils; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamUtils( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; + InstallEvents( server ); + + return true; } internal static void InstallEvents( bool server ) @@ -36,33 +40,33 @@ namespace Steamworks } /// - /// The country of the user changed + /// Invoked when the country of the user changed. /// public static event Action? OnIpCountryChanged; /// - /// Fired when running on a laptop and less than 10 minutes of battery is left, fires then every minute - /// The parameter is the number of minutes left + /// Invoked when running on a laptop and less than 10 minutes of battery is left, fires then every minute. + /// The parameter is the number of minutes left. /// public static event Action? OnLowBatteryPower; /// - /// Called when Steam wants to shutdown + /// Invoked when Steam wants to shutdown. /// public static event Action? OnSteamShutdown; /// - /// Big Picture gamepad text input has been closed. Parameter is true if text was submitted, false if cancelled etc. + /// Invoked when Big Picture gamepad text input has been closed. Parameter is if text was submitted, if cancelled etc. /// public static event Action? OnGamepadTextInputDismissed; /// - /// Returns the number of seconds since the application was active + /// Returns the number of seconds since the application was active. /// public static uint SecondsSinceAppActive => Internal?.GetSecondsSinceAppActive() ?? 0; /// - /// Returns the number of seconds since the user last moved the mouse etc + /// Returns the number of seconds since the user last moved the mouse and/or provided other input. /// public static uint SecondsSinceComputerActive => Internal?.GetSecondsSinceComputerActive() ?? 0; @@ -70,7 +74,7 @@ namespace Steamworks public static Universe ConnectedUniverse => Internal?.GetConnectedUniverse() ?? Universe.Invalid; /// - /// Steam server time. Number of seconds since January 1, 1970, GMT (i.e unix time) + /// Steam server time. Number of seconds since January 1, 1970, GMT (i.e unix time) /// public static DateTime SteamServerTime => Internal != null ? Epoch.ToDateTime( Internal.GetServerRealTime() ) : default; @@ -81,9 +85,9 @@ namespace Steamworks public static string? IpCountry => Internal?.GetIPCountry(); /// - /// returns true if the image exists, and the buffer was successfully filled out - /// results are returned in RGBA format - /// the destination buffer size should be 4 * height * width * sizeof(char) + /// Returns true if the image exists, and the buffer was successfully filled out. + /// Results are returned in RGBA format. + /// The destination buffer size should be 4 * height * width * sizeof(char). /// public static bool GetImageSize( int image, out uint width, out uint height ) { @@ -93,7 +97,7 @@ namespace Steamworks } /// - /// returns the image in RGBA format + /// returns the image in RGBA format. /// public static Data.Image? GetImage( int image ) { @@ -118,12 +122,12 @@ namespace Steamworks } /// - /// Returns true if we're using a battery (ie, a laptop not plugged in) + /// Returns true if we're using a battery (ie, a laptop not plugged in). /// public static bool UsingBatteryPower => Internal != null && Internal.GetCurrentBatteryPower() != 255; /// - /// Returns battery power [0-1] + /// Returns battery power [0-1]. /// public static float CurrentBatteryPower => Math.Min( (Internal?.GetCurrentBatteryPower() ?? 0f) / 100, 1.0f ); @@ -182,7 +186,7 @@ namespace Steamworks } /// - /// Activates the Big Picture text input dialog which only supports gamepad input + /// Activates the Big Picture text input dialog which only supports gamepad input. /// public static bool ShowGamepadTextInput( GamepadTextInputMode inputMode, GamepadTextInputLineMode lineInputMode, string description, int maxChars, string existingText = "" ) { @@ -190,7 +194,7 @@ namespace Steamworks } /// - /// Returns previously entered text + /// Returns previously entered text. /// public static string GetEnteredGamepadText() { @@ -206,18 +210,18 @@ namespace Steamworks } /// - /// returns the language the steam client is running in, you probably want - /// Apps.CurrentGameLanguage instead, this is for very special usage cases + /// Returns the language the steam client is running in. You probably want + /// instead, this is for very special usage cases. /// public static string? SteamUILanguage => Internal?.GetSteamUILanguage(); /// - /// returns true if Steam itself is running in VR mode + /// Returns if Steam itself is running in VR mode. /// public static bool IsSteamRunningInVR => Internal != null && Internal.IsSteamRunningInVR(); /// - /// Sets the inset of the overlay notification from the corner specified by SetOverlayNotificationPosition + /// Sets the inset of the overlay notification from the corner specified by SetOverlayNotificationPosition. /// public static void SetOverlayNotificationInset( int x, int y ) { @@ -225,24 +229,26 @@ namespace Steamworks } /// - /// returns true if Steam and the Steam Overlay are running in Big Picture mode + /// returns if Steam and the Steam Overlay are running in Big Picture mode /// Games much be launched through the Steam client to enable the Big Picture overlay. During development, - /// a game can be added as a non-steam game to the developers library to test this feature + /// a game can be added as a non-steam game to the developers library to test this feature. /// public static bool IsSteamInBigPictureMode => Internal != null && Internal.IsSteamInBigPictureMode(); /// - /// ask SteamUI to create and render its OpenVR dashboard + /// Ask Steam UI to create and render its OpenVR dashboard. /// public static void StartVRDashboard() => Internal?.StartVRDashboard(); /// - /// Set whether the HMD content will be streamed via Steam In-Home Streaming - /// If this is set to true, then the scene in the HMD headset will be streamed, and remote input will not be allowed. - /// If this is set to false, then the application window will be streamed instead, and remote input will be allowed. - /// The default is true unless "VRHeadsetStreaming" "0" is in the extended appinfo for a game. - /// (this is useful for games that have asymmetric multiplayer gameplay) + /// Gets or sets whether the HMD content will be streamed via Steam In-Home Streaming. + /// + /// If this is set to , then the scene in the HMD headset will be streamed, and remote input will not be allowed. + /// If this is set to , then the application window will be streamed instead, and remote input will be allowed. + /// The default is unless "VRHeadsetStreaming" "0" is in the extended app info for a game + /// (this is useful for games that have asymmetric multiplayer gameplay). + /// /// public static bool VrHeadsetStreaming { @@ -262,8 +268,42 @@ namespace Steamworks /// - /// Returns whether this steam client is a Steam China specific client, vs the global client + /// Gets whether this steam client is a Steam China specific client (), or the global client (). /// public static bool IsSteamChinaLauncher => Internal != null && Internal.IsSteamChinaLauncher(); + + /// + /// Initializes text filtering, loading dictionaries for the language the game is running in. + /// Users can customize the text filter behavior in their Steam Account preferences. + /// + public static bool InitFilterText() => Internal != null && Internal.InitFilterText( 0 ); + + /// + /// Filters the provided input message and places the filtered result into pchOutFilteredText, + /// using legally required filtering and additional filtering based on the context and user settings. + /// + public static string FilterText( TextFilteringContext context, SteamId sourceSteamID, string inputMessage ) + { + if ( Internal is null ) { return inputMessage; } + Internal.FilterText( context, sourceSteamID, inputMessage, out var filteredString ); + return filteredString; + } + + /// + /// Gets whether or not Steam itself is running on the Steam Deck. + /// + public static bool IsRunningOnSteamDeck => Internal != null && Internal.IsSteamRunningOnSteamDeck(); + + + /// + /// In game launchers that don't have controller support: You can call this to have + /// Steam Input translate the controller input into mouse/kb to navigate the launcher + /// + public static void SetGameLauncherMode( bool mode ) => Internal?.SetGameLauncherMode( mode ); + + //public void ShowFloatingGamepadTextInput( TextInputMode mode, int left, int top, int width, int height ) + //{ + // Internal.ShowFloatingGamepadTextInput( mode, left, top, width, height ); + //} } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/SteamVideo.cs b/Libraries/Facepunch.Steamworks/SteamVideo.cs index f3ec5fabe..745a3f348 100644 --- a/Libraries/Facepunch.Steamworks/SteamVideo.cs +++ b/Libraries/Facepunch.Steamworks/SteamVideo.cs @@ -8,29 +8,28 @@ using Steamworks.Data; namespace Steamworks { /// - /// Undocumented Parental Settings + /// Class for utilizing the Steam Video API. /// public class SteamVideo : SteamClientClass { internal static ISteamVideo? Internal => Interface as ISteamVideo; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamVideo( server ) ); + if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; + InstallEvents(); + + return true; } internal static void InstallEvents() { - Dispatch.Install( x => OnBroadcastStarted?.Invoke() ); - Dispatch.Install( x => OnBroadcastStopped?.Invoke( x.Result ) ); } - public static event Action? OnBroadcastStarted; - public static event Action? OnBroadcastStopped; - /// - /// Return true if currently using Steam's live broadcasting + /// Return if currently using Steam's live broadcasting /// public static bool IsBroadcasting { @@ -42,7 +41,7 @@ namespace Steamworks } /// - /// If we're broadcasting, will return the number of live viewers + /// Returns the number of viewers that are watching the stream, or 0 if is . /// public static int NumViewers { @@ -57,4 +56,4 @@ namespace Steamworks } } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/Structs/Achievement.cs b/Libraries/Facepunch.Steamworks/Structs/Achievement.cs index 963596c7e..6ded4375d 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Achievement.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Achievement.cs @@ -6,6 +6,9 @@ using System.Threading.Tasks; namespace Steamworks.Data { + /// + /// Represents a Steam Achievement. + /// public struct Achievement { internal string Value; @@ -18,7 +21,7 @@ namespace Steamworks.Data public override string ToString() => Value; /// - /// True if unlocked + /// Gets whether or not the achievement has been unlocked. /// public bool State { @@ -30,15 +33,24 @@ namespace Steamworks.Data } } + /// + /// Gets the identifier of the achievement. This is the "API Name" on Steamworks. + /// public string Identifier => Value; + /// + /// Gets the display name of the achievement. + /// public string? Name => SteamUserStats.Internal?.GetAchievementDisplayAttribute( Value, "name" ); + /// + /// Gets the description of the achievement. + /// public string? Description => SteamUserStats.Internal?.GetAchievementDisplayAttribute( Value, "desc" ); /// - /// Should hold the unlock time if State is true + /// If is , this value represents the time that the achievement was unlocked. /// public DateTime? UnlockTime { @@ -56,7 +68,7 @@ namespace Steamworks.Data /// /// Gets the icon of the achievement. This can return a null image even though the image exists if the image - /// hasn't been downloaded by Steam yet. You can use GetIconAsync if you want to wait for the image to be downloaded. + /// hasn't been downloaded by Steam yet. You should use if you want to wait for the image to be downloaded. /// public Image? GetIcon() { @@ -66,8 +78,9 @@ namespace Steamworks.Data /// - /// Gets the icon of the achievement, waits for it to load if we have to + /// Gets the icon of the achievement, yielding until the icon is received or the is reached. /// + /// The timeout in milliseconds before the request will be canceled. Defaults to 5000. public async Task GetIconAsync( int timeout = 5000 ) { if (SteamUserStats.Internal is null) { return null; } @@ -109,7 +122,7 @@ namespace Steamworks.Data } /// - /// Returns the fraction (0-1) of users who have unlocked the specified achievement, or -1 if no data available. + /// Gets a decimal (0-1) representing the global amount of users who have unlocked the specified achievement, or -1 if no data available. /// public float GlobalUnlocked { @@ -125,7 +138,7 @@ namespace Steamworks.Data } /// - /// Make this achievement earned + /// Unlock this achievement. /// public bool Trigger( bool apply = true ) { @@ -142,7 +155,7 @@ namespace Steamworks.Data } /// - /// Reset this achievement to not achieved + /// Reset this achievement to be locked. /// public bool Clear() { @@ -150,4 +163,4 @@ namespace Steamworks.Data return SteamUserStats.Internal.ClearAchievement( Value ); } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/Structs/AppId.cs b/Libraries/Facepunch.Steamworks/Structs/AppId.cs index 16e4dcd33..eced131ef 100644 --- a/Libraries/Facepunch.Steamworks/Structs/AppId.cs +++ b/Libraries/Facepunch.Steamworks/Structs/AppId.cs @@ -6,6 +6,9 @@ using System.Text; namespace Steamworks { + /// + /// Represents the ID of a Steam application. + /// public struct AppId { public uint Value; @@ -27,4 +30,4 @@ namespace Steamworks return value.Value; } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/Structs/DlcInformation.cs b/Libraries/Facepunch.Steamworks/Structs/DlcInformation.cs index 6d02ff876..2a0654b96 100644 --- a/Libraries/Facepunch.Steamworks/Structs/DlcInformation.cs +++ b/Libraries/Facepunch.Steamworks/Structs/DlcInformation.cs @@ -6,10 +6,24 @@ using System.Text; namespace Steamworks.Data { + /// + /// Provides information about a DLC. + /// public struct DlcInformation { + /// + /// The of the DLC. + /// public AppId AppId { get; internal set; } + + /// + /// The name of the DLC. + /// public string Name { get; internal set; } + + /// + /// Whether or not the DLC is available. + /// public bool Available { get; internal set; } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/Structs/DownloadProgress.cs b/Libraries/Facepunch.Steamworks/Structs/DownloadProgress.cs index 1f64d45ef..7a77b3325 100644 --- a/Libraries/Facepunch.Steamworks/Structs/DownloadProgress.cs +++ b/Libraries/Facepunch.Steamworks/Structs/DownloadProgress.cs @@ -6,10 +6,29 @@ using System.Text; namespace Steamworks.Data { + /// + /// Represents download progress. + /// public struct DownloadProgress { + /// + /// Whether or not the download is currently active. + /// public bool Active; + + /// + /// How many bytes have been downloaded. + /// public ulong BytesDownloaded; + + /// + /// How many bytes in total the download is. + /// public ulong BytesTotal; + + /// + /// Gets the amount of bytes left that need to be downloaded. + /// + public ulong BytesRemaining => BytesTotal - BytesDownloaded; } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/Structs/FileDetails.cs b/Libraries/Facepunch.Steamworks/Structs/FileDetails.cs index 50bd8f79e..d4d21f91a 100644 --- a/Libraries/Facepunch.Steamworks/Structs/FileDetails.cs +++ b/Libraries/Facepunch.Steamworks/Structs/FileDetails.cs @@ -6,10 +6,16 @@ using System.Text; namespace Steamworks.Data { + /// + /// Represents details of a file. + /// public struct FileDetails { + /// + /// The size of the file in bytes. + /// public ulong SizeInBytes; public string Sha1; public uint Flags; } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/Structs/Friend.cs b/Libraries/Facepunch.Steamworks/Structs/Friend.cs index 90a1df26f..abf2593c7 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Friend.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Friend.cs @@ -261,4 +261,4 @@ namespace Steamworks } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/Structs/Image.cs b/Libraries/Facepunch.Steamworks/Structs/Image.cs index 57b00cdcd..8918df6b4 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Image.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Image.cs @@ -7,10 +7,17 @@ namespace Steamworks.Data public uint Height; public byte[] Data; + /// + /// Returns the color of the pixel at the specified position. + /// + /// X-coordinate + /// Y-coordinate + /// The color. + /// If the X and Y or out of bounds. public Color GetPixel( int x, int y ) { - if ( x < 0 || x >= Width ) throw new System.Exception( "x out of bounds" ); - if ( y < 0 || y >= Height ) throw new System.Exception( "y out of bounds" ); + if ( x < 0 || x >= Width ) throw new System.ArgumentException( "x out of bounds" ); + if ( y < 0 || y >= Height ) throw new System.ArgumentException( "y out of bounds" ); Color c = new Color(); @@ -24,14 +31,21 @@ namespace Steamworks.Data return c; } + /// + /// Returns "{Width}x{Height} ({length of }bytes)" + /// + /// public override string ToString() { return $"{Width}x{Height} ({Data.Length}bytes)"; } } + /// + /// Represents a color. + /// public struct Color { public byte r, g, b, a; } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/Structs/Lobby.cs b/Libraries/Facepunch.Steamworks/Structs/Lobby.cs index 397e82350..c16a390ec 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Lobby.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Lobby.cs @@ -5,6 +5,9 @@ using System.Threading.Tasks; namespace Steamworks.Data { + /// + /// Represents a Steam lobby. + /// public struct Lobby { public SteamId Id { get; internal set; } @@ -18,8 +21,8 @@ namespace Steamworks.Data } /// - /// Try to join this room. Will return RoomEnter.Success on success, - /// and anything else is a failure + /// Try to join this room. Will return on success, + /// and anything else is a failure. /// public async Task Join() { @@ -41,9 +44,9 @@ namespace Steamworks.Data } /// - /// Invite another user to the lobby - /// will return true if the invite is successfully sent, whether or not the target responds - /// returns false if the local user is not connected to the Steam servers + /// Invite another user to the lobby. + /// Will return if the invite is successfully sent, whether or not the target responds + /// returns if the local user is not connected to the Steam servers /// public bool InviteFriend( SteamId steamid ) { @@ -51,12 +54,12 @@ namespace Steamworks.Data } /// - /// returns the number of users in the specified lobby + /// Gets the number of users in this lobby. /// public int MemberCount => SteamMatchmaking.Internal?.GetNumLobbyMembers( Id ) ?? 0; /// - /// Returns current members. Need to be in the lobby to see the users. + /// Returns current members in the lobby. The current user must be in the lobby in order to see the users. /// public IEnumerable Members { @@ -72,7 +75,7 @@ namespace Steamworks.Data /// - /// Get data associated with this lobby + /// Get data associated with this lobby. /// public string? GetData( string key ) { @@ -80,7 +83,7 @@ namespace Steamworks.Data } /// - /// Get data associated with this lobby + /// Set data associated with this lobby. /// public bool SetData( string key, string value ) { @@ -91,7 +94,7 @@ namespace Steamworks.Data } /// - /// Removes a metadata key from the lobby + /// Removes a metadata key from the lobby. /// public bool DeleteData( string key ) { @@ -99,7 +102,7 @@ namespace Steamworks.Data } /// - /// Get all data for this lobby + /// Get all data for this lobby. /// public IEnumerable> Data { @@ -119,7 +122,7 @@ namespace Steamworks.Data } /// - /// Gets per-user metadata for someone in this lobby + /// Gets per-user metadata for someone in this lobby. /// public string? GetMemberData( Friend member, string key ) { @@ -127,7 +130,7 @@ namespace Steamworks.Data } /// - /// Sets per-user metadata (for the local user implicitly) + /// Sets per-user metadata (for the local user implicitly). /// public void SetMemberData( string key, string value ) { @@ -135,35 +138,44 @@ namespace Steamworks.Data } /// - /// Sends a string to the chat room + /// Sends a string to the chat room. /// public bool SendChatString( string message ) { - var data = System.Text.Encoding.UTF8.GetBytes( message ); + //adding null terminator as it's used in Helpers.MemoryToString + var data = System.Text.Encoding.UTF8.GetBytes( message + '\0' ); return SendChatBytes( data ); } /// - /// Sends bytes the the chat room - /// this isn't exposed because there's no way to read raw bytes atm, - /// and I figure people can send json if they want something more advanced + /// Sends bytes to the chat room. /// - internal unsafe bool SendChatBytes( byte[] data ) + public unsafe bool SendChatBytes( byte[] data ) { fixed ( byte* ptr = data ) { - return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.SendLobbyChatMsg( Id, (IntPtr)ptr, data.Length ); + return SendChatBytesUnsafe( ptr, data.Length ); } } + /// + /// Sends bytes to the chat room from an unsafe buffer. + /// + public unsafe bool SendChatBytesUnsafe( byte* ptr, int length ) + { + return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.SendLobbyChatMsg( Id, (IntPtr)ptr, length ); + } + /// /// Refreshes metadata for a lobby you're not necessarily in right now. + /// /// You never do this for lobbies you're a member of, only if your /// this will send down all the metadata associated with a lobby. /// This is an asynchronous call. - /// Returns false if the local user is not connected to the Steam servers. + /// Returns if the local user is not connected to the Steam servers. /// Results will be returned by a LobbyDataUpdate_t callback. - /// If the specified lobby doesn't exist, LobbyDataUpdate_t::m_bSuccess will be set to false. + /// If the specified lobby doesn't exist, LobbyDataUpdate_t::m_bSuccess will be set to . + /// /// public bool Refresh() { @@ -171,8 +183,8 @@ namespace Steamworks.Data } /// - /// Max members able to join this lobby. Cannot be over 250. - /// Can only be set by the owner + /// Max members able to join this lobby. Cannot be over 250. + /// Can only be set by the owner of the lobby. /// public int MaxMembers { @@ -180,26 +192,42 @@ namespace Steamworks.Data set => SteamMatchmaking.Internal?.SetLobbyMemberLimit( Id, value ); } + /// + /// Sets the lobby as public. + /// public bool SetPublic() { return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.Public ); } + /// + /// Sets the lobby as private. + /// public bool SetPrivate() { return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.Private ); } + /// + /// Sets the lobby as invisible. + /// public bool SetInvisible() { return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.Invisible ); } + /// + /// Sets the lobby as friends only. + /// public bool SetFriendsOnly() { return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.FriendsOnly ); } + /// + /// Set whether or not the lobby can be joined. + /// + /// Whether or not the lobby can be joined. public bool SetJoinable( bool b ) { return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.SetLobbyJoinable( Id, b ); @@ -207,7 +235,7 @@ namespace Steamworks.Data /// /// [SteamID variant] - /// Allows the owner to set the game server associated with the lobby. Triggers the + /// Allows the owner to set the game server associated with the lobby. Triggers the /// Steammatchmaking.OnLobbyGameCreated event. /// public void SetGameServer( SteamId steamServer ) @@ -220,7 +248,7 @@ namespace Steamworks.Data /// /// [IP/Port variant] - /// Allows the owner to set the game server associated with the lobby. Triggers the + /// Allows the owner to set the game server associated with the lobby. Triggers the /// Steammatchmaking.OnLobbyGameCreated event. /// public void SetGameServer( string ip, ushort port ) @@ -232,7 +260,7 @@ namespace Steamworks.Data } /// - /// Gets the details of the lobby's game server, if set. Returns true if the lobby is + /// Gets the details of the lobby's game server, if set. Returns true if the lobby is /// valid and has a server set, otherwise returns false. /// public bool GetGameServer( ref uint ip, ref ushort port, ref SteamId serverId ) @@ -241,7 +269,7 @@ namespace Steamworks.Data } /// - /// You must be the lobby owner to set the owner + /// Gets or sets the owner of the lobby. You must be the lobby owner to set the owner /// public Friend Owner { @@ -250,8 +278,8 @@ namespace Steamworks.Data } /// - /// Check if the specified SteamId owns the lobby + /// Check if the specified SteamId owns the lobby. /// public bool IsOwnedBy( SteamId k ) => Owner.Id == k; } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/Structs/PartyBeacon.cs b/Libraries/Facepunch.Steamworks/Structs/PartyBeacon.cs index 5254c9f50..b3907eb71 100644 --- a/Libraries/Facepunch.Steamworks/Structs/PartyBeacon.cs +++ b/Libraries/Facepunch.Steamworks/Structs/PartyBeacon.cs @@ -10,7 +10,7 @@ namespace Steamworks internal PartyBeaconID_t Id; /// - /// Creator of the beacon + /// Gets the owner of the beacon. /// public SteamId Owner { @@ -24,7 +24,7 @@ namespace Steamworks } /// - /// Creator of the beacon + /// Gets metadata related to the beacon. /// public string? MetaData { @@ -40,7 +40,7 @@ namespace Steamworks /// /// Will attempt to join the party. If successful will return a connection string. - /// If failed, will return null + /// If failed, will return /// public async Task JoinAsync() { @@ -55,7 +55,7 @@ namespace Steamworks /// /// When a user follows your beacon, Steam will reserve one of the open party slots for them, and send your game a ReservationNotification callback. - /// When that user joins your party, call OnReservationCompleted to notify Steam that the user has joined successfully + /// When that user joins your party, call this method to notify Steam that the user has joined successfully. /// public void OnReservationCompleted( SteamId steamid ) { @@ -73,11 +73,11 @@ namespace Steamworks } /// - /// Turn off the beacon + /// Turn off the beacon. /// public bool Destroy() { return Internal != null && Internal.DestroyBeacon( Id ); } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/Structs/Screenshot.cs b/Libraries/Facepunch.Steamworks/Structs/Screenshot.cs index a30b506e4..3d95000d7 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Screenshot.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Screenshot.cs @@ -6,6 +6,9 @@ using System.Text; namespace Steamworks.Data { + /// + /// Represents a screenshot that was taken by a user. + /// public struct Screenshot { internal ScreenshotHandle Value; @@ -19,19 +22,16 @@ namespace Steamworks.Data } /// - /// Tags a user as being visible in the screenshot + /// Sets the location of the screenshot. /// public bool SetLocation( string location ) { return SteamScreenshots.Internal != null && SteamScreenshots.Internal.SetLocation( Value, location ); } - /// - /// Tags a user as being visible in the screenshot - /// public bool TagPublishedFile( PublishedFileId file ) { return SteamScreenshots.Internal != null && SteamScreenshots.Internal.TagPublishedFile( Value, file ); } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/Structs/Server.cs b/Libraries/Facepunch.Steamworks/Structs/Server.cs index 5e74b0a35..b3d2abd9e 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Server.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Server.cs @@ -142,4 +142,4 @@ namespace Steamworks.Data return (Address?.GetHashCode() ?? 0) + SteamId.GetHashCode() + ConnectionPort.GetHashCode() + QueryPort.GetHashCode(); } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/Structs/ServerInit.cs b/Libraries/Facepunch.Steamworks/Structs/ServerInit.cs index 637c85c06..95e698271 100644 --- a/Libraries/Facepunch.Steamworks/Structs/ServerInit.cs +++ b/Libraries/Facepunch.Steamworks/Structs/ServerInit.cs @@ -21,8 +21,7 @@ namespace Steamworks /// public struct SteamServerInit { - public IPAddress? IpAddress; - public ushort SteamPort; + public IPAddress? IpAddress; public ushort GamePort; public ushort QueryPort; public InitServerMode Mode; @@ -60,18 +59,8 @@ namespace Steamworks Mode = InitServerMode.Authentication; VersionString = "1.0.0.0"; IpAddress = null; - SteamPort = 0; } - /// - /// Set the Steam quert port - /// - public SteamServerInit WithRandomSteamPort() - { - SteamPort = (ushort)new Random().Next( 10000, 60000 ); - return this; - } - /// /// If you pass MASTERSERVERUPDATERPORT_USEGAMESOCKETSHARE into usQueryPort, then it causes the game server API to use /// "GameSocketShare" mode, which means that the game is responsible for sending and receiving UDP packets for the master diff --git a/Libraries/Facepunch.Steamworks/Structs/Stat.cs b/Libraries/Facepunch.Steamworks/Structs/Stat.cs index 505ffeb4b..99660ed1a 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Stat.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Stat.cs @@ -151,4 +151,4 @@ namespace Steamworks.Data return SteamUserStats.Internal != null && SteamUserStats.Internal.StoreStats(); } } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/Structs/SteamId.cs b/Libraries/Facepunch.Steamworks/Structs/SteamId.cs index 48c0a0c4e..9977e5fd6 100644 --- a/Libraries/Facepunch.Steamworks/Structs/SteamId.cs +++ b/Libraries/Facepunch.Steamworks/Structs/SteamId.cs @@ -6,6 +6,9 @@ using System.Text; namespace Steamworks { + /// + /// Represents the ID of a user or steam lobby. + /// public struct SteamId { public ulong Value; @@ -26,4 +29,4 @@ namespace Steamworks public bool IsValid => Value != default; } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/Structs/UgcAdditionalPreview.cs b/Libraries/Facepunch.Steamworks/Structs/UgcAdditionalPreview.cs new file mode 100644 index 000000000..8b95d3398 --- /dev/null +++ b/Libraries/Facepunch.Steamworks/Structs/UgcAdditionalPreview.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Steamworks.Data +{ + public struct UgcAdditionalPreview + { + internal UgcAdditionalPreview( string urlOrVideoID, string originalFileName, ItemPreviewType itemPreviewType ) + { + this.UrlOrVideoID = urlOrVideoID; + this.OriginalFileName = originalFileName; + this.ItemPreviewType = itemPreviewType; + } + + public string UrlOrVideoID { get; private set; } + public string OriginalFileName { get; private set; } + public ItemPreviewType ItemPreviewType { get; private set; } + } +} diff --git a/Libraries/Facepunch.Steamworks/Structs/UgcAdditionalPreview.cs.meta b/Libraries/Facepunch.Steamworks/Structs/UgcAdditionalPreview.cs.meta new file mode 100644 index 000000000..511f7d5ad --- /dev/null +++ b/Libraries/Facepunch.Steamworks/Structs/UgcAdditionalPreview.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 48f086235d5dbeb44bccbb40802e30fb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Libraries/Facepunch.Steamworks/Structs/UgcEditor.cs b/Libraries/Facepunch.Steamworks/Structs/UgcEditor.cs index 1067141b5..6a8403853 100644 --- a/Libraries/Facepunch.Steamworks/Structs/UgcEditor.cs +++ b/Libraries/Facepunch.Steamworks/Structs/UgcEditor.cs @@ -44,7 +44,12 @@ namespace Steamworks.Ugc /// Workshop item that is meant to be voted on for the purpose of selling in-game /// public static Editor NewMicrotransactionFile => new Editor( WorkshopFileType.Microtransaction ); - + + /// + /// Workshop item that is meant to be managed by the game. It is queryable by the API, but isn't visible on the web browser. + /// + public static Editor NewGameManagedFile => new Editor(WorkshopFileType.GameManagedItem); + public Editor ForAppId( AppId id ) { this.consumerAppId = id; return this; } public string? Title { get; private set; } @@ -136,14 +141,7 @@ namespace Steamworks.Ugc return this; } - public bool HasTag( string tag ) - { - if (Tags != null && Tags.Contains(tag)) { return true; } - - return false; - } - - public async Task SubmitAsync( IProgress? progress = null ) + public async Task SubmitAsync( IProgress? progress = null, Action? onItemCreated = null ) { var result = default( PublishResult ); if (SteamUGC.Internal is null) { return result; } @@ -184,6 +182,9 @@ namespace Steamworks.Ugc FileId = created.Value.PublishedFileId; result.NeedsWorkshopAgreement = created.Value.UserNeedsToAcceptWorkshopLegalAgreement; result.FileId = FileId; + + if ( onItemCreated != null ) + onItemCreated( result ); } result.FileId = FileId; @@ -260,7 +261,7 @@ namespace Steamworks.Ugc case ItemUpdateStatus.UploadingContent: { var uploaded = total > 0 ? ((float)processed / (float)total) : 0.0f; - progress?.Report( 0.2f + uploaded * 0.7f ); + progress?.Report( 0.2f + uploaded * 0.6f ); break; } case ItemUpdateStatus.UploadingPreviewFile: @@ -311,4 +312,4 @@ namespace Steamworks.Ugc /// public bool NeedsWorkshopAgreement; } -} \ No newline at end of file +} diff --git a/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs b/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs index 49ff42292..ebfb6b8ec 100644 --- a/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs +++ b/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs @@ -116,6 +116,15 @@ namespace Steamworks.Ugc /// The number of downvotes of this item ///
public uint VotesDown => details.VotesDown; + /// + /// Dependencies/children of this item or collection, available only from WithDependencies(true) queries + /// + public PublishedFileId[]? Children; + + /// + /// Additional previews of this item or collection, available only from WithAdditionalPreviews(true) queries + /// + public UgcAdditionalPreview[]? AdditionalPreviews { get; internal set; } public bool IsInstalled => (State & ItemState.Installed) == ItemState.Installed; public bool IsDownloading => (State & ItemState.Downloading) == ItemState.Downloading; @@ -410,7 +419,21 @@ namespace Steamworks.Ugc { return new Ugc.Editor( Id ); } - + + public async Task AddDependency( PublishedFileId child ) + { + if ( SteamUGC.Internal is null ) { return false; } + var r = await SteamUGC.Internal.AddDependency( Id, child ); + return r?.Result == Result.OK; + } + + public async Task RemoveDependency( PublishedFileId child ) + { + if ( SteamUGC.Internal is null ) { return false; } + var r = await SteamUGC.Internal.RemoveDependency( Id, child ); + return r?.Result == Result.OK; + } + public Result Result => details.Result; } } diff --git a/Libraries/Facepunch.Steamworks/Structs/UgcQuery.cs b/Libraries/Facepunch.Steamworks/Structs/UgcQuery.cs index cfb65ca75..b877e0096 100644 --- a/Libraries/Facepunch.Steamworks/Structs/UgcQuery.cs +++ b/Libraries/Facepunch.Steamworks/Structs/UgcQuery.cs @@ -154,6 +154,8 @@ namespace Steamworks.Ugc ReturnsKeyValueTags = WantsReturnKeyValueTags ?? false, ReturnsDefaultStats = WantsDefaultStats ?? true, //true by default ReturnsMetadata = WantsReturnMetadata ?? false, + ReturnsChildren = WantsReturnChildren ?? false, + ReturnsAdditionalPreviews = WantsReturnAdditionalPreviews ?? false, }; } diff --git a/Libraries/Facepunch.Steamworks/Structs/UgcResultPage.cs b/Libraries/Facepunch.Steamworks/Structs/UgcResultPage.cs index 54ffe6973..be4340580 100644 --- a/Libraries/Facepunch.Steamworks/Structs/UgcResultPage.cs +++ b/Libraries/Facepunch.Steamworks/Structs/UgcResultPage.cs @@ -15,6 +15,8 @@ namespace Steamworks.Ugc internal bool ReturnsKeyValueTags; internal bool ReturnsDefaultStats; internal bool ReturnsMetadata; + internal bool ReturnsChildren; + internal bool ReturnsAdditionalPreviews; public IEnumerable Entries { @@ -74,8 +76,36 @@ namespace Steamworks.Ugc } } - // TODO GetQueryUGCAdditionalPreview - // TODO GetQueryUGCChildren + uint numChildren = item.details.NumChildren; + if ( ReturnsChildren && numChildren > 0 ) + { + var children = new PublishedFileId[numChildren]; + if ( SteamUGC.Internal.GetQueryUGCChildren( Handle, i, children, numChildren ) ) + { + item.Children = children; + } + } + + if ( ReturnsAdditionalPreviews ) + { + var previewsCount = SteamUGC.Internal.GetQueryUGCNumAdditionalPreviews( Handle, i ); + if ( previewsCount > 0 ) + { + item.AdditionalPreviews = new UgcAdditionalPreview[previewsCount]; + for ( uint j = 0; j < previewsCount; j++ ) + { + string previewUrlOrVideo; + string originalFileName; //what is this??? + ItemPreviewType previewType = default; + if ( SteamUGC.Internal.GetQueryUGCAdditionalPreview( + Handle, i, j, out previewUrlOrVideo, out originalFileName, ref previewType ) ) + { + item.AdditionalPreviews[j] = new UgcAdditionalPreview( + previewUrlOrVideo, originalFileName, previewType ); + } + } + } + } yield return item; } diff --git a/Libraries/Facepunch.Steamworks/Utility/Helpers.cs b/Libraries/Facepunch.Steamworks/Utility/Helpers.cs index 968e9d0ea..644d1a31f 100644 --- a/Libraries/Facepunch.Steamworks/Utility/Helpers.cs +++ b/Libraries/Facepunch.Steamworks/Utility/Helpers.cs @@ -12,38 +12,49 @@ namespace Steamworks public const int MemoryBufferSize = 1024 * 32; internal struct Memory : IDisposable - { + { private const int MaxBagSize = 4; - private static readonly ConcurrentBag BufferBag = new ConcurrentBag(); + private static readonly Queue BufferBag = new Queue(); public IntPtr Ptr { get; private set; } public static implicit operator IntPtr(in Memory m) => m.Ptr; - internal unsafe Memory(int sz) + internal static unsafe Memory Take() { - Ptr = BufferBag.TryTake(out IntPtr ptr) ? ptr : Marshal.AllocHGlobal(sz); - ((byte*)Ptr)[0] = 0; + IntPtr ptr; + lock (BufferBag) + { + ptr = BufferBag.Count > 0 ? BufferBag.Dequeue() : Marshal.AllocHGlobal(MemoryBufferSize); + } + ((byte*)ptr)[0] = 0; + return new Memory + { + Ptr = ptr + }; } public void Dispose() { if (Ptr == IntPtr.Zero) { return; } - if (BufferBag.Count < MaxBagSize) + lock (BufferBag) { - BufferBag.Add(Ptr); + if (BufferBag.Count < MaxBagSize) + { + BufferBag.Enqueue(Ptr); + } + else + { + Marshal.FreeHGlobal(Ptr); + } } - else - { - Marshal.FreeHGlobal(Ptr); - } Ptr = IntPtr.Zero; } - } - + } + public static Memory TakeMemory() { - return new Memory(MemoryBufferSize); + return Memory.Take(); } private static byte[][] BufferPool = new byte[4][]; diff --git a/Libraries/Facepunch.Steamworks/Utility/SteamInterface.cs b/Libraries/Facepunch.Steamworks/Utility/SteamInterface.cs index ca0b5a20c..c2c30b490 100644 --- a/Libraries/Facepunch.Steamworks/Utility/SteamInterface.cs +++ b/Libraries/Facepunch.Steamworks/Utility/SteamInterface.cs @@ -55,7 +55,7 @@ namespace Steamworks public abstract class SteamClass { - internal abstract void InitializeInterface( bool server ); + internal abstract bool InitializeInterface( bool server ); internal abstract void DestroyInterface( bool server ); } @@ -65,9 +65,9 @@ namespace Steamworks internal static SteamInterface? InterfaceClient; internal static SteamInterface? InterfaceServer; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { - + return false; } internal virtual void SetInterface( bool server, SteamInterface iface ) @@ -101,9 +101,9 @@ namespace Steamworks { internal static SteamInterface? Interface; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { - + return false; } internal virtual void SetInterface( bool server, SteamInterface iface ) @@ -124,9 +124,9 @@ namespace Steamworks { internal static SteamInterface? Interface; - internal override void InitializeInterface( bool server ) + internal override bool InitializeInterface( bool server ) { - + return false; } internal virtual void SetInterface( bool server, SteamInterface iface ) diff --git a/Libraries/Facepunch.Steamworks/libsteam_api64.dylib b/Libraries/Facepunch.Steamworks/libsteam_api64.dylib new file mode 100644 index 0000000000000000000000000000000000000000..5237be0b29f03ea83a72332d19cbc9030556b680 GIT binary patch literal 610496 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�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

eK!jPOTWAaB<}T4a?Yr1HiGC0bxJg z?9T(hzD6r&AG_a5Bk(ZPDXF^OSjf3Id$K?L)*S+=@5)9)0lAc9AHsPb_h!ltT3l5w zcST89G~Y!TDRb6tAz!ELG1UB9sAfA#Ioom^X_zo%J^Z}?jAA!AeN}v4X2Wn}TTts8 z&JJl6G3a)LXcIbNA@{n<_V)MIR+O*~z&*SQbnykGmc88{6NA@i;o+-1{0pyL%TD&6 zQc>$+AE8n^FZhZVz_YI#PB+=T0~~M}%l2DMtP!8(FtLq1e2|B~;o*xs`~yN}EOf9p z2f)v$mpkHT%TethFFwMa-dEgE?+}!tVhTOOlO@ob$Tr#f3>ag%Jh1B z1SBPq26(wT#Txk(+pe8rFV4}(3-(GdXxnL;Jy(^(ehE+Zs)Khbn!ujAMa(-S#<5>L zV#t88IUf$av#V?2ursx^6rPRj3`#A9?0YI|dDHCv!TGKmyzp+# z9v;kMg}UVmqY+KRbjitJe~P0FVq6w?YRn!xE0rBSOOSFaNGWkIts!RC)F^GgHki7V zR$MyYHN~~aRmuJ|nB>+4f3H9(vi>p{4#GibW@W|vig^{Z@nz4(XL#``6ZA+KM)q7h zmSAGAX-f6{`L5DwRqVAm+Qn&me_&8fE$bWvA&ja%tqPwYaM5_VSTiua#>Mbm9{!E6 z8u`~cPCE?up5?~Q8-1HjZf`SbGO!2FGO@O`B+`H6;fi&H3$_jCNj^r)%@xOiv#M*N zvi^#y>j7?J5AaHhuuY^dX9#YdI12;F9vx(2zveK{uQM8^;d>4XF~y3>$!tD#D1Ih zcIRM6d3BYG9T{A>xH^k}zax`>;G>lN0(^Y>HbT&hkQuue{+9-mzH-qJP0ydgKENk< zd>g37Y98Lh!(Z`mJ39-idks*Tj#T|9goA?r^w@1AoCT^xHu5XECR`^|azF4lH_dL?@-VRCgv6;)0zFZSeDS1)8Q4WHsF zUgV0%du{kMY{1p58DMht;+!h6fd28q%qkplKS80WJVMONd&Yx23 zp6`mtYdN3hHcU$Fleq?}W%njb^)96D)YA7uro)CVM(Y*T*!M40ojxzvs8NMTrnL#9%5oI)5sWM3Jrn2#lg+X&n{$r)2rD>KT2Dg!?)2*L-X0{ zp$?_%PY)&HxN0k9Zx7XIw-1NbPI6b{aOcyZS@?)c0sO+3xKtz`*wtSC4fK&2`^ArH zXkCA5xQTtS(P#))VSllkSluS#ldF?;FNZJv0nkvU%t=`_(WL z`zx=~!NaeqqES@*-7qe=EdXh{ldx{$;X?>n4CV+sHmsWZDMpEH8qUX?@T$#yHk|cN zXOG&|^ow@h{O|3B#q-%qVlswj=xg?A?86J-@tIioTYP~6(!>4)9{!oP8{pwF9zJ*% z(X*e2o9?FgLp*#9Av4yp<>ysl>bqz%z@R2UT|1n`P!jW$4<38?Jet6){=(2SWnWzg zJM139_9_pX?lr~~RKGC*_RgxZ0w&GnAklJP`(hUbqT5{+zugs z86_rmf`?mvPVrWRm>R|Ih^7xDVD~~ql17PCw+As~f8tdGKQ|h%ac7?-sEwTrHMRmm z^z&a(1F!LL<%1M|4xtMB;BfXBfIi~|D;}bP!U~AZi^I9}{2pZ+k&BZN9)7{YhaV<5 z5_r+OzYXWU^y!m^?2jljvHD+9xPymZBE-%eU0{!2=~#BQQ~1m`;hi<%KCMuu)p zEyZ_T*xn08XKA%j*Oyn(;r)*g*1I32@FgC8!^6iPqnzh?_yrH!enmN7^6-(zDZZVD z0UqA*1m*lc?%q2v$}0OGzE1$rU1e8Sw^-J7SJw(6QWeEi2nL8r01Fz2WD+7F6Eg|T zvh)&q@4fdLNa!^|YUm)nC<4+u2>d?h-1E#llT2m?zwci!yL0^HKIfi$Zg1z_JB{gn zre~PmVLD?L#ZHQ1!OIO(P%F0}w@<>ow3>#`XWdzNa2)s206q znIBG!=NA}we190Rky!}wzpeI-F=zl@MJ-sRCwyAW{S6qVyT|X_XdKSt_mf?W(yIF$ zgk6>f0;^t)Rd-*HR0GO~NO`Z1Ct;krN6A$4D}*+)Cp3>he;b>UoMy|U)ri~*_#1l< zaaa#geJWxdh@Rq$z~F&qK9n1Hyo)|YrqMKMRz+1NR-Jk^R294ysqVfOtbTeuoZcO? zCt7&b)e0!R>fWm~E^ZhZWlO`B1FHXkH&sxA1Mhjnrce(z@->>T#^C)HE@L-vb36v5 zJT$SZrLWQZ16UQ#5Qo*Tl~&Uc*V1Y+#W+aMF6@a7(^4P>8w{6T(~E5(woLeH6YXe) zDtP2|lvVb%lYK?2?AKe6E^A*$nOBovZ&}rxr_dlt_cVhl>ed(i%JSFUt1@FNlvc-B zN^DA7yW07BOzn_hwI3l|;1KRI?J<|M<9Ebvysp=_)UEQR)i83WWz>;xG;_34D=N^c zVWXHZtk^Ym(B9XjH&tmf-y9e$rpUPP<{Mt->60roim8JkjFTQS!&nCOB-tFx-hGqu zB8ne`v1j1HJ-eEM7)3jrn4Dpt*{eJjTA|2KZ(qO7#($eQy1f2&}jh+UCneGF_prC9W95b z4d4n%Nsh+Q5$baKoqaPDlL(ucrm7>-Gcv7Fj5_jW13lu!WVX2)fdCJ&#IsCqG3`2^ z)E~rj0@Ha+S2Eqg^f1#4OxrCW{(elygLV5qUEqzc|a z)QWn#r#=0aq)NxDDQ{^H>O!?>wYz!@o?~<(E7GyNYmeu@RJ(W7Bh;ewtc0ZmRaglV zgWHw#5^I#BX;VyTtI!NV<0VyiYM}pERYi{h^pHcl?&s3*@7Rb%y5fY;zSL;vD06ddRp6U3p}{{<#~^ zeH#^&Ia@ed+C8{OzU}5!t)k>=#3y}#A=`KLm5jCN5eKoDJ{v$RO ztD=~q=qaz-Um2|^KeH*kM=I-*&M^?TpDI)HhDH{XWQnDGIa?WR1qB#H^CvU{sq$e< zWef;-L0%kof^!NuQNNh3WxAc|5vJ!r)kEf3V|EoClG;&)qjseV^}=Yr(az1PMiom6 z#>#39c>Hn*aUY&T0Ze7Oi0KKYeHW4YK2XfCWg2l#%gwG9mC^#61DoKz^I~FKx;RMk z?`Bn#`Ce6YYEf!J)igYei8960BlmfP|U1{7Rr)r9IXc5L-J zn^AF~`k}T}r^Rflj(E*Yt*uV2radvvA#Uo2>WB~|V*`t6&(#zP+AUWgBKlr+nigib z>$aL+!zy}ojiRC_)&AxaON;iQ5jcj6pher@UCI0 zh3o^3$*^~ddRvMlG2@)`j%SLjMw51!1)ODii)q(2i1$Mfmi;c8Dv9&1ntH`hov#^* zh_f7O(kHzatrowhnRF9(6z|IDDa4F-d8^J`Fx*;`eW{-BsWQB}j@?o1k<*}E&8Xn| zmcP5yj8Hvm$~4sCzNRK7_6b;{b7WXtH`GL3g5pR$Qa?yPZ)RawR8v|+r^Rs@e63|) z+nF9=dXDKGP?TZp;L@JKFEw>DIpaMH4A;_^kFqcpQe7nO`mdkyb%^Eb82PH^;lm_+ zKo`PvHPda(ie%I(sa@}J!7{`A5uOe)<5{M+n08%9Vg`ZI5+X*#s?++YuN2){ZfS|P;%MR-)u6>z~21Ub$jKyl1YX#g)tHB>|nJc38i4RJv zne0z%t)_pV?!fIVyOGwE6cU%c9~2jR2+X%Q_^$b!>`W&xoyT+~)6NB~9QHEwU|~`) zmL=W$9D2GjXewqE1KgZyV-cMu+Y&JTyA!HPp`Q;{xgp|mI>gn9Y8TqDq7kc8O~wmOw2o=lADrq$ zh}qv3WgQ!e7M!$qbxXYwvaTKjLrsy~=(i8UdNEkB(82T&)3Z!(f#TIFdMr}ZJ7E?) zSLDUs0g(+=P9#sSoz2v( zx}j?5hk5`eZuUeLlT$A|O)ae#LYoidzy>0iL16=NRRdL)|5U$*W6YkYIz~WrGD4gk zp+%{M5wXqizNlIf5#AczoKu~yO^b+t=(;Y{Mzj4xP*AY5OD)M9<-5?Yy#7HOjm+pVwtIqL}WE}CS;^EwR^^$>hZQM`PYR_O=*Bv*=PcKp$^TPG1P>4)wMdh;cp*FON+s2iOkV8vb37R zS}bR}iRpf(rA0En@5gius44}m4o9XZ(mu_h57V69lYeD> z^hC1tpe{#pq1jazMlG9z3=AQpHmh+BFpUchQ#0yXbAk2%8o~a+tgCw=Ok`mlFF6By zf@(uusmNO)bt$X&1Jk{vqzp07)zua->I#^C;?Gz3b7Q=_2@8gF&WrvZ>aF(^$no}D z$T!z@SHcrhLyoXHFxdMco)brBK}%Lnt@y7WqvF)jC^VBO+Wg$Px)#+a#cA#%-ig9qpe_Vfp7n^qC9*|2Zj9(t|hPlZg6F}=*R<5r3^g=?nxoO-5$rpqRX9tb{+3Yc4VbkzQO9D7S+ zk&is3xT^E@TuGoVAPZf8QAkeJxqhtb2dRr#YJrw2b(v*4RbG8+QxdJY|Hc(6O z`5=Ed$Mhc4KHDjXv7qV^j9Yd^Q&T+jqgXXNreV6e9utZ|B?4?l;{bky{&Xz%T4+tj zZgF*$>}hdi$D~d*H0uxM)G%M>l_8p;k@>;#M!}h|mu_bHM6E?(52|mBMLRVmw!WGZ$NAc~p(R)~ ztdXfFCTO0Two$hl)x&T_pdindQK?1&qL7@cvVr$IU{l|Le3eMbCcyz(-qt@nRc4l|2Xv4HmmNj z*xt6Q`;B0aw}Y~F;Etd&!RkV6Bn?(Z#bHjLjxHKGfNlrNl4?R6Z{l=nXyzeLdYx+c z$EDSHmM|9-=5<)1Ood%5uW|N^(MK~<1xlV>R&s#hcHUjvZVhuO!kXCAiEm$CSs)dbpk=u+^>$#W07{zop)0IpMK@}O}R;{cqG!Ap1Z?LOtjm=%j zJMemp87~tfr70MDe=LjxQB*qaBgq3n%_N`su}<>&_&SNdF5<5|X0NUaKc;!89xBcl zilJhGd{w9Q7ix&+4aFoYM6LdW{y6@r7}kbGg{qTHuwbt3Z0XN4O{9AZRu`L)=?W$n z*mTCcDfz6>O|S#t{`2VPRBICBGO#nG z7JPw?mVFpcVvy3l32i1vr~yr|wajVMaX+PFKTuUxioII$S+F|zS-33&hKZOzVY9j! zjJpy4)?68TH3?ITo47&?v!`PnPc8Udukxy2Keu*{;}CJ0iMcj2_IwtC!3dYxDWAh2 zl|l|OCnZ)+r@HeQRL%p_ELL(U(;t}bWpA*pY;3nT$9WAkuL;)jQ`^wEniei*A<{oF zPgJPxk@hu7O)wi@v*}TNz;=ZNw*QgRp%2qhptKbMHb+u}Br^eMdvM(6Fo`VYjCmad z&0x=YObeLqCP66XPBPC9OUY0_ey&#tfBrlqDn*d|9|!mp-)z1IO!bp+FCOcyg<&vYl#V@xkFy$h-yqLN=#jgB`rdv8xSq2?5vewAr9I}Plw@8{g>f@Vxq^i^)_M<8RCOT7%s`wT zGAgMFGfD{a*O?G8lSGt>S7W~v9kJv$&Fw>7HWgDn78?<^=JtkR(b04=L-pB&Y8y+% zVAGeVW#_-d$YO*oSgo*ys%ti?%S)TeEL!bMz~HJ@4UPLQpkz2& zxxB+17nUFGA$c)-qlpqes_spBn60`(`=TP|G^N39nmyGeG^Z){S>YcQ`!Mt??GBfk zX1lu6RN|Y5QiVyUM4!hQ`xEQgTI;k)LFhvU*Uf6CfHTLJSr~Y5_HS zo=#g3qaH^{$}pzKK@k(3>gN(XgkAyfMdrH4wC7Rcb>&f?L^TUK4QE$v8^c*SB~dMa z`&77NoXm6`^Ki6wCc1Uvj_?S0cQDscrstXd#x(mFLVSpa>2B*%wcbvZfL@|Bp;Nbg8O8$ptT+}~)0xM>ERe7Iw86ywc(AAIGquZ$Gf zJZ89g)QBY5!OX(E9ygc^IN7H4Qbg9i?y~s@WnD#tQ-iCwfO|iGs^(1Ya zqPxij&5)e>tLlk$ax!y|S9#4;XMb8@sgj}=ZBJEmfqqoy=dFao5lghaI} znMP359Ooy)@}EQ7!}7?Tj%iSF%);_(viP2b5Kl4(RfD_9wk&K=4}#<6<3T|@<-=(c zHKG}vg;3+*-tGiNp*Pc!Os6x=WttDl(~J=*Vc2We)-BxH)uY`&v2b3K5~4PwP)pYS zE3LZP_m!T*s^!h4Q{>T=8kmZe;4Hi9(t=`;in^oPr>US41TTYGZKj&iqO>~2>Rcst zFboipRvoUT@OC{f+r?&~vB0eTPD*Lj;UvYcFQ_Ul!#M6bXMPo}=6}V?Xw3>+n7#KN zgg2Tz1l6xigYfvOm0M%TwoFea3i<`p`hIQP)bPv;$OfNHS_Y+hpQzXNGYwilJL#hPVEmaCZ z?^KU8{~;9_3?Z1XGM&yem&BH8jFW`2yt6zt(z2Pj7Ts;4d})US$!AZy*c01qoKFo; zwWZ@!1CE!FX@$CqGbAvG2*z6ZZG?E7JzQjZk7>`JDNQXQj%(o#;|{_Y&J3@p z_)qm2b~0%$>wj{nqQ(wGw>z0R3rtRR+rp(L4>Mqcr~95C9N3S(i)~uXduWzo&Q>nnW(`|9A2~dJ?jWXfqN{ljH9{Fx?9ZqY5;oSaU$ilZlGS9O6i9 z!vl-H&Z1-4XdEzGfz;Io32J@D*JQ`v*yiM^Km#PgSZJ=3LG8^TN$P3_`XX8q{Y6sd zdKTYeN7@wja zrgj|%?}A_yrH7}7GCi!vc8ozKSkIn`Mcs6LR)@!6+klU|9mtAR$Ff|9_Aa*!Q8!ys z|2L(TbQx<~OAC&5Dq0h#aQCzc!=?@PUuY3UU2GGVp$fl?vM1XT+QegWM%}r}`5xx@ z6nd#WEKUT1pa10ZqvGbm*@wqr$^Nu?v@>=6oN91B@6nJME%Mcv2E}Ue&b!A)n83 zK7*pgv%$j2!6!b{h*tIOtvpA;<62>Y&+IdZ{b7u_TLr7zt;5vRwz2A7+uEx8xAkC` z~$QQFPk zkb|mk8%vI;1K+S}GupIt=XMlZ4XE*LQEkxf@o7?3*royU8GA&Sc7NQ|(OUnddLWHY za0r*q1{v&AooQ2A-DkI6zf!uJrAU7r+%`4Ul>!pf$hN2f;Kdw+>00wqic*wH*n_xBgJ4+(mN4{Jj;if7#zF}4Feq*ZA@mtI~Scx9z zD4KboI4;>Hy_uTzttE@0m%AjWTi;{6QolAFbs|WbANe-6Q8CA#zNI*wMK}cMvf`SJl_5dS}W%X60Ox5OTk>lo5{eitk`_!h-KJ?RB18El`-AaP`0Voh{9YZ8s@L zBB>vWej}O~@a|gw?_*PFgRhPayBi7m(9tv`-m3FKMtEyX-^k)+0vxuo_=8N(aUj}D zbXF?H(Fah=aA*S6#_DT%& zrWUs|N@^iHcQ!B$Gn=^Eozd(|gSoq%QKZFeM^?bYArk{Kl3X@aEo^TDs~hbNkHV{F zcQk6NzMWuNY}SDzoeWEp)}yoGrA_NX2ma7}ZEJ57ZNGl$fKSC6uuJU?X~9%a(rpoG zMa_Wf)4{NmEOn%#7Cp0rQM~<`N*~<~(9RBqE>lHXpv@G*I*$F0OJ);vz5^VOvg3I( zaPG-;8#J<`QM?fv-w}R(V?WuKDKUqFx~lY+j(o;a?d&LO;K(a^n9QsTnXa*VxF;Su zb#i&g?gS5o%zBt<&nuKL)C^GN&Ff^8*c2?GOt98wwZD^LX@c^(46ry4uKONbtB7kV zxR57I*Rc;>ffsZ(yw%_x2pR1fRajm7e(8)8wZ*I7`Ob#BqF<$Aewu^0!L;*L3UmO| zaZKlcVsoxY7q_sBQM8HQM@lbeKbx5DXL^e1HKrY}k@SH~Co#VeLVf~CO_46PD7UZU?;SHmL)flYzW2cFAv;=*P{9Nw^iK9tM zSsjOq-E|x$4>o9d#oY8&=LZ|GWN|Oy(+8*OIm9UH)PIP9MR=^2Q;VIJK-0kiLk+7! zvK}&t)S^AgzS%}ZN`^BN*t}JO&OS}(Y0$C;+g;txrqETp9tM3`)WeXSaXM?cy@!EK zAX_3WMeglkgsMY5_<#ha?sR&DbCYe`ZCKRi{dCo@Cmn;~xedu1)f0&@lMy{SIGCI2$%x4?=Uwdw|>{Zt8(gd-&5yrk6>0P&2HCT~)T zWZ$f&4L~3l$t#Q_`HCl4k@iCWnvj}>{*@BrVlQ0)mJg&=aN6#1YpH%jeK2ztt9hPT z-9~k#EF~cadU+)9oPG%Q-i;tbpWZn`g|6pKit2Ds;-UdE%Ht)EbA1iozq!>{M&4EGVpDwl>%h>}#lpafZ50WEd9Jq$r znwrqh(944sw`r7*ci30Y+Z6gJrn8x@1SRXpOm(!M;Yc^*7Ds2vX?gT0HI&qB$f{$f z{;v4Dczzs$018P^**JB)KMI@EW$tndImtFRu7YyEzdP|e4?q{=vWDp~z%0CK$N;mk zP@@Jw1Ga#<(%+zp-jCAd7;Ar-X~#PhtAR`>Fo!d{ovvso+$%2g6T~--`HA zzqq}eABa-t_HdOvj3f_i;G^0PLdxNu7S(kSKFq=gSRL?ZPHj@!Y&UZT8E*3~`k=Z{ zrc(V>8%!} zz}r#Tm`MIh>vG`Q;5Fy-^#6G=i2>ygiNFVkO`-eQ`4 zkF*@YbSBf~Ot&&U!t^4j%lwQ}VZ#uE&#&+w!zdHm-zPc!nNDE3km>sSY<+NFlWF&MrQm%y(-};cG2O!SFw+a5imYT^o7Cuco$AO?GY$B93fv1blsaT} zmelIop5%@Kg-^7os`oIHn|5vo4l~Rx+u^ioI)mBsm=-YIO=~993Ae} z;WTahu1(kxvcfIBl9g9LSPH13A5r(uc1o{HsroIP;BuzkrGqi(D| zdF*Ne^J&5BI%!+VymjgPaLk+3+7UFdQX59%d#4Wg9>gu5dHQ$g2((=|H0%!fx#WP9!D3zDZ3TV}zg?O~(29v_w91HhYW_79B}E zv}B>V7E+v-v8I$0>c^2tv$RAtnA|tAiu?GhL$x18b4Z+_3C?sK{Tqpez#s|+$FIpm zrWGAL961Vpe`dc|*>7T^EU&H`Wl%4TzAFP~Sa`&tR#8YDI#Ocv1*Mv*E{sAgRM#la zF#;Jp+GyO&j^IT@yt`lj{o9RfV)0yTn&1bp`l!nEs?-(N$C(mq&MM}_Sipz24y~y+) z)1IA4_Hd>%m@Z?wh3R3Y7nru|LY)1W&IeV0sI7h&YlNzi;eH=1ri10-CZG z4sJcBPNdNowz`wK)pM$q6KQQ={Y0)@#DcAf6wF32`oI`;64ksFlc*BYi4qnuk*?da ztK(yh3|m^JOEk7+CgMY;x;fUs9t1ji-2{zNeG&*Nrv^$TD5^U|MHIp$pq>&8@_oZ=K){SvIIztl;v*f|!k-$4CVQ?Ek z_IPx0RHboLpf7zm%IS8V>2J(L2b8s*yt|3T)79fKkfz&GIo^2_;Vru>$)lqL5~b7Q z4V;uiHEC-Xm%+q06nq#H5qBxfP$nQp%w$!gCP5d(L2X48f?&cFre;hqEK%Dq84?z< zU(CHG7<~9-;A9kGbYQN|i6TSOpV7#VQl2XGooJ*blG0&_GwM@DW>c(Lsm_xO$}gP? zM=6jstY#t8!%Tl=db?{W1LjB+lWyH8CWDzy1jQIk9hzwH#N7-+^_Ya1q&cyvMNS`O zpzZ z_n|X3oqAHR{h5vh#cD4MpQ$&20Y-DRaw>P~$EO+*C^$HYtg>vM% zj)bjdFd}loPCQtL=n@M~(nXgMPQgI+Cr-STi zs4dfs5H))`%6zE$37*5$y=fSqVUG{66WsOXB{!!Td?ldDo(_pQENczZLYBzGlkwB3 zBWhvxg2Z{4d46Rcq`riTjsuB<3QilDG%-65%P)*C-CHi6*N~{IBeicLP}GcT z)C>b#Yvz{j1v3B20V76h#9oGkiv?GlgH}q^@R?|f6YMyLWLkDLpvySOW=V~miA7*K zWmeRzY4S{(;$kQ?b0#f8nXz$M7V)_CC>f63;J`ce;bH+w>z}+w;WCKY1z9>gm(!2r zb1eIr&2$;ljZDvi;;tWcdX^}M`Mwx5sk?KKd}_{IUKh)rXGExN3yc`me<2D(N+w-% zh2%CbA()s${Ta2n(HQ6GIsT#?gU=1|Ngf#UHN7sz1{yBy_fVnvsVPTk<7=~+~$Z{S=4s?dl!~fz%RvS8XA0Y=p zgUhoGvNzi`2dUGuFQp~LTaB1w@J5g2Y#!#CXk>=wKx6a+=ANtCI)??EoP(K_S1r_U zIgmXJvT-Cbf(P(*LD_q zgn2VV)w;RpN=$plnG2|TxpMK?T=ade{pQ`2?o?EH1FF+JgR9us1(rlzMp~aE(I^-B z^O3D|HjcL-rDRBl-X+IFGYnxe$eO2Tj-hWeL*Ftw$M7sC;&pln8K*knW@5}qFbbxt zO#YMTkJ84A-6MX%|gS8i&FXiIHdRi z`r-~w?OkXjstqW+IQ-;N^y)%Oey^c$wz0y8n4V>Ni)q*X6xTsaCorAIbS2X*Ob;+U z&GZJ-&I2fIio{G^MAd~6rKQxXAE?L=05^_4GM&Rdqt(DgG$iKoG>>x=OG=X#p%0~T z7nE}eKD(G(#1*+lIJ$b=4rPx<_2m`Eqj?zG4_<<~8!nt7SMHL&1`Kx*p?UBeTx>rK-nw|TNt&0GnQH%TNN zc<+`+JwGhsm*yGK>gW=zOJ|^MV=W!g4Wd>1rAB0Q%gQG8}2v-noHD;QTSnyG`K{}Nu( zVzsF;RehI$e=hj35XHV6xC$wQ<6!!VbI3?;>k@`1b0pZ>L}aD$wy(Op)X;~ULRI!MN^y095^otx+{pAC(>pBP zoyBVMGNe6QbHuB~%h7h}<2Rd@$(VKfGTF9OdzTr}nHlO33GP0W6wYP(Gt=(FC~ne_ zTgs5?x|}*bZR^#6^w$AQS}p<1SZ)R|Z#g6{fE<_xFdZ@+GUej*pTN&uyiBzAufX~& zU-tzAC4Fn_krgOhNG{c51zOpnrHqCzxbal&BrbQGrHi2Xv0#PQe#QK}Wd$@`0j)6B zW;$>L;gz7&Ea@rA^%c;RIyUtiv7cdmnG&i-NkZU z{-><+@IQ;#k2B*%ruUc~8kqNjsm{2(9#?0pE345KBsWk;)?!57V;#3#yVe;nmqP|w zmTiy>8`h)o%Fj2#Julh)sepG+ie0IDsQ|gBH<>V=;1Qp+6E_NsI&5%nt3Ek{ z&}Rb>a+$!80wGJ6gWH*tuC$MkpS+YFzTtbYcUM*7~-k@ zh>$~WX3$mj!g@5W*VeoCYt&NGFqvioTME#fQ2PNxW!=5fE;yE=b{3$NWjGQktNX1r z@ElJoj65*6%w)4GH8LM5J!Ujza+sRDmbM<9xDo)KEV~lR;Sn2;1qI-p#k>wIBxR_b za93N`!*wZKVHnQe($ueOjie;ic^y9N9bF2?0Keq!D$#OuEOcf&f2#S>_b zw{@LS^djIR$RS>*ca3LmZGfQmV<^x*pmZnOg!Noyq!Q}EhT31iiJXo~NO|W}cMA+Q zrBA>ZtNR9nOrX((CF2GMGZP1Ke(9Oqn)Mi2Az88Cp9Sxf(j6(}Q5^CN3OT4XZ35Zl z=D*YHjdUJy4yJU?W3~cjV>$e(Z@yt(jH1R9+iqq%!SoU{(_t93Bp*pap|h`5#Ce~2 zdX1%|!?tJ!x^Ht2#i?=%u+`^zS@hWgL*F3>V?R?(ed&_En0ed=gXbs>(`@e94vyfX zn7Uggb&jGu0)fIjjp<^h>zVFkdW`7>rgxbx8%O-7nciSJYdpDJW7=^7;ekx^m~Lcx zfN773l(3>fu&%8#6|-Bf(mu6;cBB*ZxX=3G3TeC)QqVmzJ;b!vBy!mXN{fN6s;x`L z!5^sY;2L`T2UN6i8!d&-dT{o}Moeds_{3^GGum+z0=-DW^#R>GKVZt~KK?vnBPx~d zbNXzQK4;KIco{jFv^v0a_!ROkYOL6ZRs0OTyi}{$9jBwT?7bU3WZ!kmK5NRpM6&lo z9@YV;65Y>q(lic@X}{?R4gI*2rnS^s%Q-ByauX6@{U)?79&>$ljiR`Md<9vT24tLc zVUs}@IN&-pN}sf>bajXLH-R6aGd;!h8q?u3Ag2s+8NpNYl(!jU&y}0G1wXJEWgnB( zE!3c50;zg!g`}C}BM4(kwR^MSj6!!Cht5=WrEg2=Td9xOM&!i$w;zoMt)Kwt)F(R)k$o6qR;|7_Nf>4b)7ceVLA6 zI*sX!S>!%@HqnEiSVeNBLhe>d0Iu>Ywo)Q6Tt_P2VGjr9khkk|DJi61KEBo6FCQu- zJAW#lYRopQ*{Mm}ND79->dZDy+d+kx&`Z=;7JBr%>kARg_<0arnmiP4)8lL&u;vvS z;a|hh7#HDgDm3T%dl6btRWw7N;-N05I=RiLtoo3UAis=7`t-N|MDyiyiv3&`mn zn9xejCsrD-t68RC9ECN>|k>= zTEl6DP8id;2|JDG*!lo-c2WT>*l7{dXBSk$21QDO8i8kZf^>H?b%$X~OtHbs(w&gK zd{HR_%eF&yB1aLz^l{xh~x`SBc4x-0SGl$TwgBH7j>6hSHrcm+QoF#62b*c zJ1-@?gXw)xYTWqH@7!HRNSjP7O5x=pj3Jg;K5a5T?WIq{my?89On+wDVFkIYW_pil z&PsTv>4X}(TiV_EyLk$cyW1S{ufRJILAZor@@`7;nY$r+9lT*BmT9|Hghw;Y1x1DB z8usgMDp}EvbY$6O2*Z(ZdrN6t)J>AIgZ-RiI&n2g8QUM@??J|sU&L%RRv76sjUC2V zqry4hn?4ULSjjCafggufT(0eF!YCv9%ium8-kF#5i z8lQX!@OX_F5c)sdYlMWnKwW8&zH7egQqH4t&H7kDj zAw)ff|Iz=8i+$m-S`a`#{EKig)FPMS+|1=BS`7=0bm3xt%h#$^E5hxyoQsn{F>VVZ z;0rI*r|4BhszzGBxV?EpR!$4{k+yCr7XES-5P=e3HJ zUw)ncxoc6$k)YaEr`+Q&QH8-TD_<7_(DuXwRbC3f2Q1vPrvwd#9xTM{g=qs-hU*l{Kuhxsqt*B_p1Hr z>3{$8>8GEm7X8K>uf6t$uYX~0RDVdRZ&xW*6HobtgxkuL#()2Wr@+oMQR**(?*RW> z@CM*_1n&oq5xg={sdT{wtCSinI1hNG;2FTj1&;#$Bf_Du`T|!IY|K(BUhvnzodkEB zsT4M4ng0i1jGY;00pm?I#?64AfT0t~Tnb!W@G;0q75o$2M+?3H_tk>?!JUo)liz7@ ze+bCQI z-Vk`W;GMwh1or|yC-_gmkCoBXf;F8Dd%BZ8-NhTRYHp7hz$ zMX4HsbKssRcr9=b!NY;)3!c|Ssr`b>bXCeItK~F@`+o(W2L4F!BfxD0PXL}M_#9+z z6Z|>c?+XqFuK2K)`DRz8ng~7s_x6I*;XXz1d%)WTzXYDUg5M@Nk7zl60Z${rF93HC z+y*?e1jobuu;BK<5B)*=?GOBx;I8107d!~Khu|B)a|9njymkq00QY->{|5ZxAGOT- zkXcvocJQ|ld`~3p=W5LnD-39jqUMhGI{GJy)5HkN;PWyci_zS^D!P84{W%yk#_`kpx1cw0s z`%l{Mhw%HU;NfuZB{&)GD+Ip@d_nL!@cjMH+V520D8ZkA=X=4upu=dB2Y8L(zkuho z;1__OdR)sH4xSo<7eQtV!5zUfTJUMOZx(z5IOqxOw+whH2=3M!aS?nGxWC}9&;afg z{3`GZXiTZD%?D4M;K9IM1WyEBDtHF?&kEk#7dk(w{eCl0sXBrO_fqN`!BYk)HC=F- z0SH%cGu26 zl`c3Jc#z;$zO`FpyiB)dxYT5a8D6@7ViB8{|D}C1V06QMet1Uyz*}?rvq?9!J)ui1(yZ? zV!^lIeo}BPxIgh9?f1YgrD_WvzEi2Tf89|-$9JO-75oW!%DyDwZdaKQ+ znSTVoU2qieM8V6zvsJJI_@3Y_=TQ*1J@O72hJ2+9{iI9zYE>Y3jQ4U#aFc)8+7|vZ~^>w790t@KyWyCeiU3D?hn7F z{Z@nfn}S~jZY1~wc)k^!2Ru#iyTDroUjn`-_<7(LU)M5YAtzGsZn$R%z7ITF@MGX9 z6nq0%y`lZi0MDC(JFG_e6nuD%QXK_H0M8Np1bFrf?h9PDyq2TZVqPG)DfmAV{5^1Y z!AansDL4~&hv4h0P+tUJ0RO)#h)m!x!6(7rOz>skY{3>9PoC*uL0i@ zJRJCyx3rv^kQpWTS-7_qTpI3U1gFD&o#47~KP|XA-2Yrj%h|mU{g>ch7AVzJa3tKj z3C;t~64KfWV+D@_-Xpj%FxG^5Y;yv*yxA~Ud2G2gh1;C}NYxjq8 zG4~XF0`4}!)!?_U;2ywB1b+wov*0}7ztqrj^58dE@S4TYLGXRJj~4t&F5)6M7JhFE z4hMee9WBQO94mMyjE1!wcktdTT$?R-~_=A;30xfLe4tDNr=}G!QaFEPw#0teSmA57(7XWKSNl91-FO$ zCc!Phe@^gN;3wbLa-86KTkvCWZz}k2kkeam6SyxI`~mPu!S#XvQcKIZO>Ldvhu1-l z;B2_}5&T!U=Ljx@`!T^ofgkxm%XtwzZwWpO_fG^*2L7Mm)!ChL@XPjDsRRKfLu2MeAu7V~t$!+{@-(0+4(YYScj++A=X@Fu}$fgh=@ z`EL=w;KRWG6WkMcx!~`CjXIkDYv5Xfhm2DyO>o)qNN2&T;l5jNci=}NwciE6H3T05 zhWQrP-?P9&1y2IrAb1<_Ey3Razg}1Ke*~N&I23r2;CSF;f?omt=ZBiV3UDLAEr16K zz6HEi@P%QR-XTYsU^5F{H6&m1kY%}2f(vIa8=+-g8w`pbvahc z2?CB0{3!b6PJ(yAJy&o(@L9oE=b=4`(|!}-{-NMHz%2#813XD^JK*htGk|XiZUp>d zLoH`0!i^F9C2)JeF~BngKMlN3@G0o4esv?eg!yHa5nH*!C}DL1^*M_{wDYh z;8z=KIp2b(k>IMp?F8Qg&tkzBfv*Za4*c@R+V6DWXu-XK+Y25EJX3Hk@NU78(EqyN zM!%=ww-pAB3`@L1q>f|mg=6Wj^-rr=h<<(p`~p&Kx-7o4>b>m-6dg8OE{ z@xbbH&HtYtFuoQ154fibt^zzt@Sh-Ozu-CGfBFmUHxKUh1)qcaP{EJF?>50N1DB20 z{6oMWF8DL>w-Wpm@N~f`z=s5n1Ag{P?RPcgd@Q&g-1`fD3oi2~ zxI5hckfi;#g3M6CE#Ur*;IrVLA-LHl^izVPfS*g&ek%gwh7H#9ec&;IdvC=&TJT6< zqnYOE1fI%*&jBY1{x|Rl!Lxz42<{JjOYm6W7gMyHmcSniz60D(@RPs=f^S0qdxAH@ z{nf9u-|5gNRq&^9pD6ePxbGBPANb+sn*YRRtVao60NhFN-?pGH7QAc|<~4#>!o5bS z_WKm%v=@8^?i&TC0RO9n=6N4DL-0W0wSuc7th<6A0j`#&`MU$dmXgyv33$5TT?p&E z;C$eU4$YGb>=1krc!A*Kz*hw?o{c$jy5|3Gj#7;UKMtHN_#}9i3qB6_3xaz9Kliov zI}A8R@IB!6f-eG37yRX1v>SrI1um1J{niH0+k(^Jo+LO1xR2nfz)J)_4SY`U2)cjuoHx}F)I9qTR;MIbIfo}<} z4_q-z`;D83eG0(?fIAB=3p`!$IN(CTwEof{jlJ}z~x$No*dvBg4Y1s1Q!DL7X1BG zrB(?pH3j{S;69U4R@-PfFTp)Ua3Amw6Z|Uh0m1J8|D&zuuL}NH!7G6K3;q~*i{Q5= zp}!V90QmWDwBP>#*AYApI8*Rhq{9foW#L{R_$lB!f@=Z)=UXl3F!(;LhNg zDYzBz5y77UKmMKen*tmu*p75)BiIQ%UGRSJ>=t|q_@VE$-#Ne)1uqAV7u*8r(^qg4 z;C#VPB29i1+yl7k|FoQI@cX&oO~5?`M?!~O!S4c}6FdukUvH=VmV$dz!H)wE5?mJH zt`giH?w18W5B%Tu+HXCC)ll%?!QWeOZOC6MxIFmJ3H}f~FLcm;8^gV};1uwGFSroy zO9hVtz9e`yaD|TAZ(HzZ3cd{Y>4H}R9}|2G__d9a-jybJDU z1wRV+XF6-Y$Kf6-_#SY&;ETWm1?K~=5j+FPxVQkPv%jvKJ{k7mzD=~i&{PZ%k znS!4M&s4$Xfwu{MADH&PX@Bz<;D2`2__^gMdxGyRMIR^lQOF!9xIe;LE4UW;j|&b0 z|5M$xoU2RFehU6G+*=7g1@}pUYa%Y|1hsJyeoJsW;7Ag1UM%JN*T6RfSAfjtdTaN8BCK%1lacS~f=`2InBc{5 zUnjT#?xzH20GI2d^xE}H)UT`#cItpHhe3>P9J@6jEdw~BiK+Bm3Id2O71Kd9nJOQ|+;Aen` z3vK}Zb%MVHz99Ik8K~C-wakjsP_G47g8TP^KLB1T_}A&EgMtqO|80==`y~8+Ab2JC zTMK?4?z06q13o4AQ{X2DYrp-0g9TTeiT!=SF9GKXt`C`)1YZVzeu#t%o;bm!fV&Ak z2A&+jF9M$y{4nsdL$%-A&_7b}U*Z0(;8EmP@ND4Af;&Oa=Z9&(-@rXq@WH95M}oHk zj}!bMb zvz_37!+o0In~+l|_zdt3!BOCOcBJ(xpigtb-@|>7;Magx2tERy!-6|Oewnda&NATt2;K)A zBlsNny9=iNBv){C;A4XOgZ~fXw4AZPl?2xY{z`B|;QoTC?^-6fJIdfG!9#%mJYLHg zkMb2ExE^q_;85@k68sVHGQs}cm46w46V}{cXXkffEHU03Iwj9(bAHWAJ-K z@D~WT+(a$sF5KS|d;yr|CzPktfjbIb47^zIPT(Vgw?Y2HlQe%V$Sg0o3fvnB{s#Cv z!Eb?Qs^D92-y`@C@O{A!@Vq%$%X|vhCb%DP55Z@_zg+Okz%;fao&N(KnpY5R0r!st zw}X2p!IR)VM{rrV?-5)&A7g&OPXNC-Rm(ZO9{DBsI&gEr8dzY6{p_^;En{8->xg8Q|^_*}37_x}kVz8n3E;4tuP6nrBKZM)#rz<-#o zWp;rMuL&*>_h7+;Tf>G^upPLK;8k_87AJUq2G({2J62&%B)B%(>Z5{FbCF+yV~!#$ zv_YKaz#j{q2;4*PjBM0>!B3$q=L;@afOcMRrL~wVAqnlaM`(9P7Ux>5WF06J{LR|?%f4f2Y-&>d*C@Fct3C%#DU`V zKk!!-d>J@i@RJ8%DVs&*1wRLV+E*a?RpCBLa3=63 z!8XXhDR>O<%g86-4}^P$;N@tW2MW$c-Yyh8X$jV~ z1dl~KbVl%mt~hp;tK~Nvg872r4QT7C3GUVsbw}{Kz)r!>w?UsJxN>XMO~K#n#rRzC z0pM$bTkgZYaGnmU3EZm-p1B+OEBMttSkn`H9qvm6{~7N41{}DVD1TO`S z5?lfD(*+ygA0hY|;3a~82LDmPSzlrOL2!O2^s`H~{J*wGpCb5W;4s0JI>UBO@OALF z7u>cZ!V)|J?sa zL_J91-*gLJWWo6wgAIE|7JScwUs=v_m?xHTJI0?`aDoMYWx-!F);hGc;BFeToDmj0 z(}Gu9@E!|3W5H^LJC09S@Jkk4(Soa5aHIu)V!=%osOrcP#kHm2UU{TJZZ8T-So*EV!8ke{aFG)?-hvlc z@KOt2Yr&f=_=p8xv*5cHTyB*+tiN0EzbyC_3$9|p?^|#K3;xoAn_F;&(&&%ztBBuQ z_*KELDt^`QtBzj{{NBOuUHod|_a1)l;}?uy7=GdS)yA(5ev$aq#qUG>qVTJSUw!-< z;1`YGNBG6y7mHsUet4x?;YDbLmzfnFSXQ6l_bGm#;nxJe&++>Lzc2B#;nx&DyyK}7 z@w4NXgkLg#c(YOAT|@O1e$Da2Q+uiferfnQ@Wa1$^a}wF#V^9*W`2t=;`Ral{{MU0 zAnqz2r-tkBb&=62>2}MXeEo#`t=%GQ?1P^7b-5Sk*OyS@P6gbi#4m-prA2E0>FIRe z9?8H(oaR;7zPaS~ZMyTtHzQwG;Dg2U8gCzr#jgDJ!t0C=K_rSi!tI?8FG^aU)h>z2 z{X}<393GE#m&9a!_q!w>_md*tk`z(HgLkgC#OcOV|AOc;@d%H{vjg;Fd40PiA9VYh zJVRa*i~CJ;uN;2Mn^TIU@4{ws>F2jg49WXDD3LA2;`&FS=anYnw_E zhN5KN+>iU1lKC#cl6b>QdcwUV5AZ=QQ) zG&49V)O+I9vt=~Lg(&naOQ<6wBP%`AJD*(PgWE@tensAQcl!3?^+|ZI%F>+ATpUtnPDC3dQp`qw!yq9x{5V2W1o|58Wdua0SCYv7wwmcpTG-%OWt1f%gOH;wXpH~IQ}AI;njDa`Oh|feA~YgmS@8KDdE29 z(yv(azH+n@`o&Ctvf+X6%lF6UzSG;U4w^L>K^I-5Cg8J<5S=;1QI zJh%ldAf~{If5XfaJa*zQQ2mxq30%_B`fA~jUEB&rM$2P+C4~sa#XB|9W zQk>b<7{&9oi@5%g{;Qu3E~cdq<|w*J(3g12gPLXl58|izqmMqcLkpv0;q}?cKs+M# z^gWrrq9nl-fB(`?2$pv=`L6H4*#pzbB3~zfmtyJ8O2k4sV$ZjXeQV|J#fm=q7^pwo zQta_WvLAm%A29XJ?(uqR0Ez6uA5bcL_y{bStbeJy;%}_06a+MnilbKz)~=!1U(_uY73Xdi2)^4>hdjqrJY^@O-C~c>U

KSLQtY`b4`JKQDcFOZo?C_;{BIdK*H1TycAu*AAzS$9IY_gp%uasL!iD6 z>eREhXll>Eiy?Rex3y2^z%?P!;0{ozTMO?qP&0laRkQP5?f%paNS@cl&LUwpNJ_#Z zTY;vMa0e8+_XV&b!PDEZC6!tpve3=g+E1W-ewEXO$h;BTN9a1-0P|)!LqDS?lxQ!W8m8b9V;|Kqwgnbr15Ha>AE75QGE#UYq2$p41~*z} zi9zepBniYSw6C;?#9Fo<%LFIl-Heh2y7=^%rvcu>^*8R(vbhJvOPHjFex$9W^wRwL zF$f=uxQ0YNcqryj&ykhkV<=Y_Z%ml6k8wCk9vw#6Z7p$wzD_5foRlmkV_<_rdM?C? zm1PvJOhw@5~>yeEaQcXrQKHC>z_cDSmxloE9t&g?& zE-{}wE~NS`{^d@D>@}7RebR`w0tm! z;Po#&r`DkOV8UID;zQ(ZDPMBjYI3 zUw+u-(PtlAdMBjFTX8=4={>uoBwrHLtKf1&&qpKms^ zsqj@;5OzJAewEI*uzEI+x9t2d>n8wxGesABZ_p3Fyj19$OWr*6&7`Zb*R={uQ;xL3 za6JJXxFds~^YSCqRB2ivIhrVd+1@pVWKUhYatnmSrJ!&~EV$4q-&TT7d+0X}sg0I@`M$NE?C1vRTc@X!{J&%^*v!{UptRw&9s-Cw@SJ#+bq0P{3%X7{og_13nqtgxlRlnncY{`gVWA{+@>#TNvu=?b~D!%B+a#rr_i zh)rqhH)@UOL0pIoL{YK2^&K0S8Tf5zFgr@8+T&AFqZE6q64ndr#KlF&YfD|7ff#3H z+OY27EoWS;jpxX?-xkGY48WkR+~k4`8)OJ_ZWUqVLz#peX@qOOV&q_5Tm$wkeCyQl>>bvjbf`%~F)WM$TiEm5Y$ z6P0>c-22EA&p-l7IZrPrjSVpBlT|A_mc;{6Oc$+y^%7%Z(-K3{AdrA!Y^_K+Dq;b# zV{n&}VRxd@OG!$B6(Ti$Fh20MDNE^GB$H^Y8k~$H7m;a6#n%(ru+3$3M3M1s+*$%( zlvle}%^ZTasi_gbUSUeA4P%L7krlqiwvQY6*vCT#B1Euhx(6K;!|;ST21ZW1=n|fkg!P{i@)P{*Q5V#DwzQNaSWM9Jgx90hu0Tq( zfff#b=FcWu36{_DIZ&f#%^sM&v&qZ(6r0i_J=Lxap1HRxA(66{IPwZis{R=6++Y2ol{PV1e`TTg6?3vFgkXpWIBb{*Mx%WFk;F=NpQcCmhf z4ub`62G*%C$k&3sq>(8Ryl8*h_~i?4ozTpdmdru>iVi;^kMSL;=$kJS+e!g&g{Pw1 z!T`sW=9o36r15fDWSE~2Nc`_+M}TY=)6 z;w;in?C3rMO65TLbslSl1BsfO-;W}ikv7P@EU9=TgXW|OhcnaGqWE&3g&9X7$lgrX zGhYhe=r%2ZQ@ng+6)k@di@P}ZlBrj~0w@8p%HX4rI(?T?5#Tq!Y2wWKqf}zLmyy}j zZu8bfn1u^j0j9saUF&ORSa7b#XB(HzQvc9j?e0k%(ya~n%8OPo{*j59A1s-e0Vi2tR6t= zB7uv97<*EN-PsH`L10+m#n#)RLrhkx-K{+T^#F%dHQ{a3)A(0^(l-mT3oCBY2F<)b76H!pr7QG~e2p$Y~P z6{@N_ynwj5ls|wvqD;VvB>aJEBrCfmE#!MiAz_WMwZyherroK-^4CI`d1Ct3H)Q-S zzj8|CclkwcWzR@6OVR@>9Ti;vcLnO&?|(4gnwuLYUuBEhX)`lu^~E2rT9kCSsE8uWONVa4v36K|MCFY;kodV0;z86P!GTB6ZRX>%5}N7Z`#7t&cxoR3 z$5yjPrNI0-Acedi@GPPT>Io;Mv=%-EW5$3kf;M_EhV*I#ZWT`FRs3`DVHN*udX5)x zp|&<>e$&^=?5_{O_&qI=)ukd`&*^hiK|5q*Gi293e5Nh222{6A#sEvF$xgZ9b#u&` zWhQfg1=Ew=f59~Q{TED=lNRBNC!6~!ruYzAS;ZkiS|88CQ4?qJi6h_dq?h37Qbf6` z&*JJc0Ac6GD=Z}uxsG+UfPLWF$P#xYEgHmILus6X3(@=s6K2nlvsksVYi)7$T~ZPz);@zL zJt^S8a?|7vFjrX9z$7zkfMU&KcMp`+7{ln(TohX>z}}WsEafJiJ+NAVaHEb&u{G1E zgdW~fsN8;B!FU-GczVE4G@VMJ*TJwSD|Q7V6C7#rRV%mlvWjJmDinDjtpik7FG^W! zb101SGJ-RFSD@nF!c%SOPJ1GbonU#$cNHn_InG~&)m{Q<1T-V16xk@0uxM;eSM4F7 zdZ<|&!LrBOEQd0dnrmILOQ_MrK+1WY-LnJ?&H~Y%2DLsliMGL1)qArNPXYM&RC4pl zK7BWtG=wNYG50Nl5w_;`hGEfUe-1M}-XgL%6;sW-#~6EpJ*6dPjeNgJ@gnElS8PHv zdm@(V=!jifR`ET85`1A0B>~|`Ftm@1_MxSzlAI-#4RYE@p`mCX9`!9a=u^||sSoV$ zcZ7%1B)-G|1;Z1sW>N0saYeF7J~W`4dr0p<;z@&3SGxL1oLEOfb9<&!64kAzk8n&c zA$)$#*(NPPhR%Mo(DbP@!h!g(1y}%Ku@4%R+i^agG!DcM9k_+rgym3dlr7E6o`O61 zT?%U5FsY2jsz`>n@nngfqAU3-#YI8@b7+<`6Su*nB*Xl|TMJfVKyeN_Dr-;lcg)C( z06uULSmWSQnEiMS@j>&2C0RZBtoNP@SiJ?9(#3*JE z`z~_vc*3W}oK#wk0JJ54E)D&(pk{IL@Z)vV=v_ZoI5BoxD(!kdQ0!d3soC|ql9W~2$N7r@3jBN6KWFpbd#2FEeTdn+J-eUTM%dgNo@cz5x_>GJx}}VF7q5K45T_#oy!m zLE0h-I4XbYJN7Sqdiw@FdL@FIq=`&vn+Q~ zh%X1s`(RlDe{=~qbwqTJXNK=Fl=d6$%uH#4K*%$-@p!SL6|OUnYlQ`EEy`Ekd6z+U z_Iw*SHE@38th*T%c0XO9)(>9r&<;k}97zXS*g%wujBZ(3w_08oUA^eV zakl0{Ai=Pwl5NVo1OxrO@7Z|grG`5YMRej&!3#yd;By}7N>&;fm`xAbcE8=P#Ulo6 zqOac$Ui+fHfX8{_jV@C1vbtR~5h9&htCp4hCIW1i=+0I)0!>K>PsDixY#XD!Fbk*u zs(|ZyoLra8+7n#qA03>j7;PIFDjTa0#HKM z?=S)ne4UcVX|3QT8jJe4%8ynPeAH^(E9F2dj|HoMS5RGE9zZGc$hx^Ws0YeE^(wZ& zdM1 zy$4uR$=5%g06|0`SWqk|_KpdNiW)T(xzSJ*yCNc>A_xeHVn+e>nvLDH?^;*gwe8yL zVp*`PU02s$cY~;F@0#D|+?zmvQ1<)&-uM0e{|`J*xcAPSbLPyMnKNh3w9>-yXLXn0 zpXL^i5Y^ITkA{;)vkUD4tCe=5@%Kj2Oc>q98B0F$!hK*7;EFg7!K}$_4y7x$F~F_< zZ&ER!{M(d_sl%>}DQXJP2xBS(V;JGl&%7+?2CdvfMB%UF7mySxYY^ihxz!k0DHVjn zUbt0_MT2rG$vY}MBEIkmI!1a1El~m#T4rv&=0TAmZnRt2E;mvJ4i8$?s<=XcMXg>g z1ZYg$Y^yMVhM2fDIT%`#5Q^oMV2$fzN}EMm$VAB%?y-za1jW%p21B75_Zk##6TG92 zCU3cGV2u-Yu{d`eT_Shl1Uw6cO4<~S+GyT){>w;;LnNh(!5qlr7$78vu_P{DHG67J z9(hjhnqryt0hx!#i9dx0#b!axIAhJiab{1Ez&OK(E|Ux6W(u)@HmWZzF82;sMHD_% zm?v+R#Z7!TQwW=7N{dbD zNnDzg35GA35;8}Ce#!9slq3ZLL~9#vW*#|zg&;sWFq>wci_#s&x<{~NU6g`J0+Z9s z`M-fR>abGaaK%#!IIh~37Ou1mb3`%f!l;fvwzjkh7;tEq@$XQmL;iOd9FIOD)NzGR zo6BDTprd`lX>|)Ou{7_0EE=9Me~?Nc1gbctq)nit@acRdb0?RXrCoZ+qCK*O2$LZ| zyDFlJSfpAKLMbzxPem1Fr4&jl7YokKQGMcZMOhTiJ~MMDUm?sM>UcBeej%_Bv|ZQ? zs$uA|OHwwdLa02Y=73;v@FJb~hkdZ-@AEFiPd;t_?=npnSj4L%4dykpB3#VH7z##h zR~XIAWEKP&#RKXr8rE8rBmcP3;rHz+k7yBa8DFyZV;+c1Hn zHN=MP%SFkHMuC_Uk1Pd)M$lsNU;<)(aUe10O*6Q;{TP2cO$c}TgIOkd_csZ#m}qQ6 zNF0Y%7NBn!*J7_hQV|8qQ!I>P_9{~1*aQKe2D`8>h+ownX zm{X?J-rQ~$$_2oPL@qgV$5EQV;Y1qvQE`!#nNurGz|1?+O#lqNS5bzW0TLdsT$_M9 zBBrpCGeC0X)-^gYiEPM{sx9)&!2=Y^fje(0Jl4>B%$9`-g3pNPSgdp|JdQGD1n-5k zN%5@zJFWv@ro(h7>~Al?1bGY#X0Sv)fMg=T(txAwmk4GMuen3c3ou>8{Cfc?s$uXp zL>G1q6aXwrF`1MpK#JQoWgu9}nn+EXS`-bZimFUTJ?Gb3DW{B>*#bz3VyZC;hXEPu zYH_-SEEgO$${#ycV(}TEOY9YSxI$IgY_@pV68c6Sh{QuvK*je%d9bC-50Ndd6&vIS z$rd%pB%za+x-=4pmDm7s zA(io19$L&8(jXX~*^-K!hYE^|#o>M>lDi(TBwmfa4T3YzqZ;QFqInqnapo(4iIb#ALcGK%+MsI8dL@YS zLXbs_d(azG4~rqTN(Bcs4$!~_KHOb9pi($9x!~L_h{fH2NOgs$Y|OR^ZNDP+kCz4s zWkz6TgqWKm?P@Yft(a^OWKbgCR!WBfyQp;8&JSq7iNq{W1V+)tuv7pKx0}I<8xce8 zwA5Gza}8w*pBwHvC^aH&crP`u*(u|^BFvu=qZ67t*dxjO`oq%UnqPsK57$`ba8^oQ zg{DepMycl38t}`nS)^*qUnEq#DDt2U$Bz9+NXU+v;|20%CoiE=0Gs64Pz}s3!OE9_ z2@WSE^Rp^Np_E(?Oz4MTUpYF~5%Gm5IP=jD!1f1YqcLf4=1111Wk@^Z|8rzoZ!&_e zz#WBFTAYqSZNW%zXjEd*Fq4BTiq}iPp}JLy%1>aWsOKax6Ml=Yt_6TG_g7lYVl0?a zFm41biFw?Fz+DDn!o5*L6qAcQHx(^dvpVLlgEz=pK)+FmJujkzx# zG>ID;mBc-4Rui6!$tD0xQ>zSMB^3-!%oSy(<~B3lx+v}f-CRk)5~1;&sRoEZ3TE>I zUo`E-icT_rt2WW3rHS5ABhYOJ%6pk7LdSTFh>wfn_tp$Ed!C1p6bDEp9Zeg)`f!)k z6fyCY4~2&`Gf7~C!X7Yw4OUU>4S7IpnKgf1FdvOF^EF)9KqFb)mVto*F5jZ^E<%L- zaSFxUiOW$g@giUj3Rj%t0LVlv6XULuH!p6#;;-Q42$3m}$5IiO@0vpLATag%Mh%rG zM4L~Us2Ugi6~iFF3J9@Ca(;0UiQ^KmMhC9p61b!t(B9#Z!=sdRUtf_F8E$DCWvxh% zfbc}Do9E9FH$U%Ylnjk?$g0MYnBr3{YCWBi5It$>W!LyvbN6)`!HO3u7aN4@V@F}- zFphM`vZW$xCCy$$BbMJcZd{Zf=Ia;S)6YlV%g@)mbPPJUw8dfo=TsQgH?!VUQX*(S zi8u)dXZK^%F}I7|%;1s-j87;HT(77^T*UV8Gsq2XQQ3MYp*J#rsRdUU^smIbTlp8|2gB7&Dj5dQId8{Q!GB*Gz09}*|JQpk>!OUV(UOY$>s+5jL zz+$X8oKb{5hehyDp!|(YSZfMLG|u7V?l><-4vJBzpFRD(0Hn+v~=~;w)@&jUj2_*t!pztb3BjG+h=_0};vP5aYCde0`M;JEL zOpMtphG{?=6tuNd zSn7!|{UdNoF|utc88oW`pYpi)xN)O!*eCUL1>!=&hvidEe5E&3V-r~1h=uF4$mu~_ z1H!su$&nIi8}~044GF;*nsA|>r2tJpl7FFOlB79`PSDK7VBB0wKsfoijnf%)>{FO3 z3~D;5;3jqB5uU_IA)ZYm>7+#yJ`7W)hHkCb2sBol@iq#VTjc{vBKrtMuNW4HgZyxY zU;cRWhoGQhd4paIECWI{HR%u)5FHoos~(jAB}uLoljIQDD{-6;m6=hlrsHQE|EP+D z=iC&7^byjat!80kBoe8axFm{bS1`gbii(8|XfB#;;^B$mqxi#xFj)oj31(6(PLD50 z$vi0PM{s>H0Iwd0ESuoo4?r<<_CLR~unv-tsxqxWq=ydaSX@;I1R*%`t7hh~9=WvH` ztHe0z6cV)w7$xFWFk_~Nu_C^Zbh&uKEEJPh=4f)pqdAaEuT#7E&RqIOldWI>KJ27blyi zXguGg6eLTTGQir%V`B$ICC2Ad;-m!4Fv@NJC~(q|c~-F>GYNjLQVAcGtW@msaE0_r zAwCUorDxX6Q40lgWjvLkq>5sML*bh}$f78^fH$1Q<2?>H@f1_SCMg&&N))pR3SW~; zN4|tYF`*h95}4nBC{TY)K%x61qr*!Njf(g$fs4~7#si$lxTY&<$z~f12X?8FC{05( z=Cl+|H4hY>15SZ89fuZGO%zF`sntfxrE7#nXf&IDSq1YjN?irHwn-*bDXJjfj$4-= z`rpU~#7kRi0s1d$EeBoNTAK%4s#**1e_d-!R&E9Z5#7CEyis)7l+KpS)5xW=AoIWt zgL`hg5Ue0erI_y1teu4_kKu~R*%;=m9phmEh%01c(K=1+vZj*(%%Sqp#_e#hE(>)> zd3!xLBd(AsUzA}>BWa^yT-eIy+{$Al?uE&5PAGF$Fh8mAA~wP-j7K*%19_Gm1IDq@ga2nag5z*>buAZQQ;K=@?h zXh}JZ#^pF^VYumFv_QPM(Ru+8umLE;3B88VUm<;098Q5_Y72wrI|;jRG`D`Y^a*p| z#iv=o-BBP(bTSx6_rgXvlOikt1HtDBrTD}ET3iMZU65n`U<^azc|*c5Z@8HAW<5+I zP(=L9U2t^nok8ZJO}th4qs=(#^8;`la;}wN5=Wvm@pzfUm^pmbB({K`1fwS0W*8nz z?jsd9QIqSC%8P=#2KRLnS%VJLuPDjEK3C|!mSh7|~Ov~YW^+yz%goU3mVG$!@!iHfME`JC|mJh;2 zabki1{KjKCerP;yV^k%ECxpeq=^aLQb>sylV2PI?EHXBNKY}3=-nK;ARTdeGd!nEM zB4d-rK@h_X@xzAW<_LUJWP{#;ihk~#ziLr1i{haAgQJrzE$B#F=0tj zvBSbBfaiZy)Tju$Nu8bvaNs7WIDc-~C`^kJ5{xf}o;*Gmj0G=l7(0!@kxj!? zV`2NbA%keW0sZ{Db@OW1uWcJa>(AA1yUbo4X!+B}pC7gCI`HG4=ld5TiGa@R@e!TU zhQ6!c^1*;#gVz7C_V)1DT94n!UOR-iy!!Q*n4mpp7JZtacPVXH^Z?eisn56#vu7W^ z?Ek|%r_=JwX_LgqlRjQK$zDJbx$6Y@JcQ-@6T+ zY|rfcrRDuj9!vXMZ_TOqY{A>};nVI`9>0B=Q0}$->vzkqYL^V(a8BRNZT`7Q^PL{1 z#`L-6$Tqd{IWQ;Oug4!QSbUA`N8rkmN%MhY1p&&fZuj5-Jjid zan9#1PcufH{j_4i+!G7>&s#U{(y}YjKTRIBevgfPxYy@DYiz4`>o5PP)WvtYS4ck@ z_)~>9x8FAi8`ULc!TR$9Cm#;F8TvH%LA4`8H1D5${_QvG+hM)dUD!C`_2l;2dmZZd zRvFo+P1bZ(#MoaqTx25u>N@RtFY$pX7t>FN)oL3)eeckL-@IPa_tni)<2Na9G`tg> z5Y{s3!_LhiL-jqIeBU}NX!`lDe`f^l5BcS{(dznFJco8%d;P-+_u$hFo7Dffaj9*s zsXrcD{rcL=;~iJNX_MXf+>+!K?VAm0rJdMRllf&wEs;awr|-+XxOBQ_)j{zU7XNej zmsSC1b8M^GDGq+~!^z7-mz^DH{jt}`sXdQbOglCAm*?dN1?=q6wBMK?2Q*`SgQ8{b zj}}YbmhV}y*ViA1CHr-sQfJ}J9lFlXe_z%zrr%zNjNOeox?b7sw{Oh@>qoASnr7VW z8{7M~?9XmJ4&JoSx?z{RzL{sW`ETc~cl2&P?8kjqn|J(qd(hb)0ye-A|kDY<@o^oCph{M4B_R%SWqbc zR~X;k7$vA+c(cVB7bCH(#4V^d7sAmpQ5&S*pqi{}ny9P;fXt_|TrK`vRMyeQn{6)D z88PB=*41Q}L#rMQ`M%?wuM-yESbxNE+)r6E$K+(2p=oR(w$T9cA6(Cv1DH-TzM; z=Q-PdxLa}E=~4T>o$a)Do=V<(lW5jp&G+B@R5ks#Rf0y5OBUb!WwAqrYOytj_WUz( zaKJ9rmHMZu$$QioTy1oPbspVUJs$PSg$Y?5+F@zE+C!FIs5D=9=iI6-v%Y=YbW!M1 zMXgIGoj?AuHS0mS@?+c9uRX!e`pnktzobdOcdjg5*FYXLdj6nQ->3eld-mYTQs?yT z%Y*lR@4wlydiq|U2y3gCkKbLh{5UOZ%lEZg`%Q0GWzF+ti_e9;S^3)>n=Kn7ws`mG z`7va8lU=r=Y8Typu71~X!}|*#{6{qR@Okokz%M7}&vr^aSbNIEt=XLzoyBMW{_R^e zS-rGNweQAt-QGPurRrDfo6D2G%e)!7IAPGWHLu)O-nBWnGP=M2=lCJcyB+J_-1KN= z)zkI2Ewz3;;ro+aQ~V`A>wSN`|Mb(uxuS7D*6n)w58*V?@h%<4KKQX;!(DS4e`))5 z`P5n6F7LiS>u|d$_lof`9`o9KVE%e|EcAMqVprSeQ+^vbOzrt!E#H#xCk*drare}X zXvaOP4);x4S+#LS@^q^OS^I0etog^CI!9hEt{!yB|8)GrDdM?JXIwqF(xOR3pEU=Q zlrxSGcD?_@=`queu4^dkvwz~I86P(+32zz6Oced~p?&ri@A4I_2j1>tb>et*oy~ua z3=CfBu;6sFGYcLx>b3J}-0{zg=Du9m_TZzZ7v|OKpKkleLQ*}gkHG&!jkAyCb-Xff zZhf?6`cucgJ^$*zV9BmwC-3+@%lgNv!4`IUqgKCpPaZtlW6!Jy@&0H2hzqT`>C^7y zmD`!=-mhj2X)?%3-nnDt7e_7kqe%A(u?~( zwmJRg%kBy>3PD20t)BgN|C2B;YQUkb^J{4KKkGKfZl^GFW&Fs;!)68l?6$R4r;oAk zZx1;(Ct+*bN!M=H8oGJI zxY$ht{a$^$SaxoEL&bqxvbi@md-!hJef^KbxMzKOvD@q%{~ObTJ_%=4Iu|xLnc3}idHkAdGyD$7t~6ffwdL38cB?-9(_ola z^T$JX-}1RLXk^tt|D4o%+QKiZyk90XU4C_N(>LnA6`eoLJ-EWN_W9ZMCb>`gZr-hW zN&RPto7`Adzsv6lzkIx10o_IYbV-VJd9I7H7nZYFl_E*85DKMYiM@6CA=4RQl%pHqPTWZ9MvIT{}WFkP}t_;!tS?NqK9_-d5I? zEqaGYD@!WUhizqBtfh#Ja#h4dxJesH8c<+`$}W6h-nBAVcI8T|YU45R8YitPsX}m; zmF;=Bo){6j`p6}N%jAy?a}ePsuoGOG$GM)2;y)KSa@(Fws@v z>snMoMuzN4V91yoJJZ~l=2tRJSkF`<5T)fbp;&-$TZ?Hzp@1C|WIb@1{Mq{Y%`Mi3 zT}|qo;kjLtX4~1i!ITY)I_CsAEZDZw;rxkCk48D~sH0>3f4y4X=ApjNlBQ07FI>6L zbFJ-}of-G;4|#p+Nz-J%=;$FaVYB9c{;vLC?T^;mAq}dv|K;x1{k@-U-(+FwJkM(P zj$}8FIw^kke(?t!1`M3i{_krxS1UNUkNPyd&BNHAj#r$Okk;tO?|m=4*qb?FVYTJV zy9)iP?yvvb_fB4uPlX9+G z8@y_y%zsnAO481*W15U|skQxRV8!T0n$v%ruQ=^wp!h`})%ibdx(`cU^V8-H_b)1E z^gn4~DHI6TPW?eLb*;n&MdadORn@9W-)c|SoVz)3$!f>DpAXMy+2ZAcYP}?4imoga z>a9c)OFSfXD4@QTv!rTDmFDN8{TGfY|Mhi@)~(AA@B9O%*bJ7q5kMWQ#*&7T`m5@$ zsx#eXgi4iw;a@~ztXnwMKR1lMS|y?yxgkj6K=5Kp!3vdny2R1i#4xk6KyA)nxlxot zfwz|`X7$a?b#vM_`(}rWt?$7_Ev6jozjt%^r*d=WyYIXFYlkDjUL$=~Yn}|Olkn)r zTis)8)gRq5?80vg4nJEyDsKs_m1PK?Vpbwr8@3bzsIv>>n-Dlyk5_2us(WV!2LRPy82Ci zxcE(D$33p~;QY9r8La+I)^JYfT0!>rZN6c==IrS3cy5Z@qGdx~C!c-oU>nkD&Bvf? zwYR1%(z+k)6ToICJKx>B%zOX%t~JxvuxG32W?ty0dAqgG@fMe4|-;<4#$~~L*DDX`J^uPhhG|RQT9uQiX%1HD-rvM~vB(2>&-KDK19&MliJUpQQ zybPcJBTA^Gmi}>-Kgy!oFjTVcwAb`oHlFvDtkZCRA+C0=pOr~+)wo+7CPrn z?}N)C#xsE z33a$KC2aZ2hL-z#&j|>%e-Y_f->JJ(f0rE#-@RS&-MQfXNBRX1pJTJr_1)>sEnMRh zc6Sy^zS-k*PP|g~s=1;`jT2wb?%L<@=ipj@oR;lszvZ>qK`_6<&vmQ1sjhUizcIAx zPisfd_YdiPWLqowj`yD)B%Jwctj6&5#7uFm~)%6V<>eiHPu4 zlBvgXfh{Ccch{J@1JjJr;aF7eiY`huB09-6BJVOZ*P-KF!{f%eCeSh8u5bXuVVvX| zjth_yR6Ncw;6%|c&Xv-@zC_$y*1^fTMUgnsAq4J~7FyK-1*Z*Pu2_l`WI^JpE?*%~xZWzZ`RM ztR(12o%Gc7noDK=ZC`F$-eq=+%RkSpbnWo#q~*^pMv7053u=9BO{WjZ`j`{to=oye z)r7cSYIpqSUzY{h#^{@W?!Q4)eqyyQeJa*gUD~6m-@$+FmG}ntb1v*jxFhzv*Z9dkYKrVh!hIk0@I`M;{_ykp^5xoYSF7tv zriqS1h5cwSV;b)et@``L^!uiwA~WWH(S(3?<1U3Z^OSfPl%1 ziLG z`dypkQD|y3+A8FuOKFlNXIHThc0Q924X=0;=(ou+F?GvLev%lm;C7`k`wy?vKWq5o z(W}Q&+^aU-`1VP&#gp&FIZabKuj%V#eJtW^rQ_F5d&K)RS{mDVeTYT%A?5UwetPy( ziprtg(gic@52nBDbgZ*z>8Ph0_ItTpYgMOmgXc}A2kusR{&Vtu^#|!wTL;~*bJ?!K zv@HEOFWXsC?Er^b?rmRrShei7W_*2NU5lPmZ&$6X4jdxymgDUF%!^B4fcF{&k@(5f|Rg~a$R5l(Mluy*1jk`hdG8huJUT1G~ViH?-C zL-EuyD$2_E5aU-NpQ8LC@iY>JqHJm)nqN=m>7J-?qq$yCXWIRLQ)gBs=`(Tusj z0g}ol0QQs|*_)<;m5hOAjL?>W(*>vgmajhWzIQpdj;n@VcHXj_IrhS<|LoWug8mVY zZiIT&m>}Oet6X&5({&$wnXdMGQ=@ycu-Cs=cL*4E@RFZxl`e;~ZV1?K8^4k0&Rj6KwFOurwrotxW?oLFJuv3(W0M}C=8Q?`6qqSd|+ z%||{uUT$lx^$V=KpR0T!Y|u5iW|yLa%ah+<1m0g$&wubii@?zb>Mw6ItVhV5EABf5 z9ripJx=rrbK00{xhr=Q7{GM7Sob7zDfx~z9KCPGhZF@^Pt;$x(v?@Ph>?)a>{=cZL zEo@AC5R(z~s%aJtp)rkPvyBn9;W-av0ss2+&;uo9x^Xi3s z^tOLC#_3a9=lS!!K7E0LFd;9LP7^9&WHB8(Cy$DC#p)Qk6+~7~>L#h@inCqgab5Fp zS-sx9y0&aruX88cj&*&5e0uc{_H#x5lcaJD?(MBq__)?<*{T(FNl~p@`S$X44OS{b zdbxr|t5$x2^<3+jRXPAnYK0RVFql`3qkS1If#4RYimZoZcVmJR%I!|^{ioXP%T-Tf}R;*!g&>&}*nrZtX!*uVUz$q!C%IKI2v zv!h39g?g^)?eWJ?>#`2`+U+|T|CdL1k$v@wSIz{>8{4`c(ciLoD(dxg%I;}{+#)*$ zo?W}8d1OsLzdJoDFux5++xo74(;*krV+YlHU3FFM#C0ErZfntGb+>P89{AqF;)66g z;ZvtM!=Kbl3jVfws|wXL)!KYqU8&9I3pLZb9CDm-=RO$Znrt~IzHr|{keR#30^P0B}y0dLxjj%n` z)^lTRXU}P8DDvFK>LEpA zh3i0QVdL`4*|_?)OBS!1W`6-TrX_~RIR+cE@7TVb{%Bs;y+BDzCQ2oo%L2AqW3DZ@ z(g92u6O2muAMjL>nF*yM^>UYxAwg!Qhs4tZom6Xy)N6pmTUr}hinAo0^7x`%-0-CZ z_U-O&G!t$Ab8pzqljELl_Nu4f`+DZQDV?iJ6W;_Jdw)Gzkv&D$$g8fu@7<0A7i6ui z79oErLFyZW{(KAdbY^__Sm{&VR{-{#?_L_tG&)e8J zb#A+=&1>JiusqvV()soMW*<9$?(g2jrnLV#0E^(fBMZ2m^ z_S%GATT;5}{+=;$a`63(87&ve+g`NQYeJ^Wz6iCi>v&ZCr)ucx*tk_;At!b}_5EY< z(&+mYn*8jxw85Y4?K@Q*Icu@|D91NIZM zShbKelQdb?XjOyh#{MP0Ul6BE@U4$?4(2%|S{-GZ9XI*egkIkq)2^IS&%)ZJy0CI+RR{0+?jznVN%24a zjpMJ~hTpvRbn37s9j^?o>3rw?j!W-$k6rR(zj;l&?hCk`*5}*0{TbDrfaBfDjecS6 zd;50RnTb_eKV<%X-F{%zr^7r0eZ9_ZTz%(d+l5P0IxTO|KWYBM-Q|AR)-CJUAOGxm z-lD_G#&vb?F5i5d)BR$0t69UAKG-`@q;gxLYhL+t%^QEURZMPke(ugQ_Lk+{hF4Bb z>#FPXZuE?kxY1{J-2Y;K!&WNlyqQl=3471B&jPs8{4Ng+_jrji6^F0OC;Q!o=L2HR8 z%^S3CP0Fq<^qleYe_0imG-AFq)_L{2rH;b~U3B|y@0|L>JAL-PUw)G9ti8^kle)Az zbo^TIZ;elU-P<~0m3`%ohin3uEr|DOd;PnpH#;L<&pn|S73zA)>r46luiL*m+bud$ zw6yEp>z>D>oNH%RU0W%9r`wz7to=7PWpAy|#ZPajEPu4-35VpF+9UP$?yp;U`Kf2K zzPmMHpnh%kg|$;Bf7So>jpAVk+YgREBzhcZalG4rL|d<5)vYFRQ6lsgXeb(*!R!nq+QpoCOqi*{pq#U zLpR4PiT&>W6IDy!PItpQmGhh2THR<#m*wj3PDq}1|8?vf^OFWm;|b93a#_P26+O?p}-=G7ma7MvJ0zE{{wpV^M*#|&C7uyYAK)$z>7 zzuq0~l(=W_ZC&dhomOofG<>7o=H<1cc03SEyjy2)y(GM};g)xjdSVZs^kqXFZ*FaT zF=qHl55M@6OKPuLHYK3qf_u^WC*A#C1)i$s(D!P+a*yimoRPialHcjL&W~pA{oM7+ zpp)Bw615-Iw#nL!U8CZAuO2b;anDXCUtj*~W9M}b8>Q@OAM?ktM<3j$o^W5o+?#E^ zw)(>zr#p1NbN6?LU$!l|kXklrz|UNk)Y8i)4QP6>Y|@}?(%}E8NdxILi_WlQI&ki$ zng6@if`v`)crtIqyK3q;k{Y=qV>p}a^NoOub~kY&*!p>xR#HsIpMTtC%<-7OQ`MZ@ z7KYu_@BZP0b`Ta+n2g5lq_rfN5Rd?P;DqTD>)myF(Jpo2KpX!WIko4F*k@|uVF?8J zG%5yK{4wj=`0R)apY|R)@z2;>Pa-$`^}^|N^^CSLCnnE*_Vjpkuk%4W6BO&lC|o-Y zx|FqW=z|vT*J&=-*m`E<<}<6-_i?+vvweT(yVg}h-!%xU)9dZn8@5%)!~_O^pL%2T zw;7$D#*O2-TkGxG9*r5XrC+_|(5RKO-_*CO((3u>wfl31HuBtOJAY7%%d0MKKk&Exm}lD^l0^P<=CxeQ zo_!^%zIxCIvFo3W`_*eOu456a9?OEuBv2-SG6|GPpiBZ~5-5{EnFPutP$q#g36x2o zOaf&RD3d^$1j-~(CV?^ulu4jW0%Z~?lR%jS$|O)Gfiek{NuW#uWfCZpK$!%}Bv2-S z|6Bs+K0f`5)7yk}x*MIvMyI)5!%MoLB1g13{wL!|wXm>We*JoJLHYk>&CKra8zf9I!{QJ0VFGlPH9xAA~VF2qWZ!pHFc=}1-e(4^d;g8Wt8#770= z^d9=7Z^JA1ZDXKEd8R*}Uf77JSnRIjzVq`p;ztfl;7li9;*Y)!uY5ou{vr(Lm-5jC z`AgZ%hi@i<Cj(LEy&-ALwr`FR=IC^>Y&R5<`p3A1dSib~a z3}G3Dr{SCGG5zxDU0f_qP#rZiJ{IS+;(**R^Ym;CWQacD8~><&v@?F-iE@a~lNx>J zr)QJF14|4z^j(mimm!WGVL0}-IR4rJpZ+ooyIY85$CO7f%l-5RJn9qq+3+^SAvZA2Gs&h{ z={YG+3luI7=d(P$-2CFxRDMP=@K6E))jI~iurQ?}u)CkHB3I`bA&s?`|2}_?f6{*= z1ks|iE2-W%<7W!dg)ZvG&1kqjZ$zuu=%L)nw_)K4(QY`pH+ra`9#jEecZ7l_z-dBT3`RKpQTp_2iFh~s zyeY7VziS7Dg0W`eY9fr%Ij8wqx3yX4$MYBczOQNX$rlp#CV?^ulu4jW0%Z~?lR%jS$|O)G zfiek{NuW#uWfCZpK$!%}Bv2-SG6|GPpiBZ~5-5{EnFPut@c)hkdNb^!E;D7fRtp57 z$L7*kNHD{`Vzi#eW??3iy`$TR6%`CCI<^51t!IWIbUq)tiw`|w2%X4>ZlKWA<2D0B zkL6NKktHJyfy!RI`y#{M*44#FP6fjUO)oN<2{ueblB4dQ>G`nbedT@Rz2&_^g4y2Z z73^yT`&Fr(V#8?sZJ6{7RTH^J*}y7*{UW{S#kh)B994<~bd1L8JR;RO0bXBJ*76Pk zu9JKM*vZaWewRt~^s3NwZ9@39Gy#`W?g_=aUntqXIi}@<2o#!0O=kJqG_W|YYwvenEmmk$4Zv!-pM`9|Wxb

`BGjO;44V~u%b*hwdH^ZTZV{Jt^d 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
  • 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 literal 262944 zcmd?Sdwf*Y)$l))%)kH{&O{jrDmrSYv3Q9@ZDNSdzy!{~M1lfR1&u}#tyP2>#Tz6} zQZpW>VjtUTt1Vie*7oT|twq#c5`sw(gaBSZ+KTpeVz34;2x>jQ@7iZ>i6HOqegAm> zc=<3nXYalC-fOSD_F8MNeK~cPta21O91a&h%W^o@@|1r;_4mvFc$^N$yy!d!4~N4n zbTpqi>#{{p9&yUFtNKRh<&U;UzWTKrM~?r|s>j2>`{hMHJMid-WB>c>#TQ*!d1JI* z8(FsMlKD48KdLSNIp<3y9#URxu5W#8{Bkv@;=B zz%e|_T@G`Y!_n~D0>>PJL;tluQ{X5gvYm9hgVGRz0>^F})uCBb`1IJ-%VWI#Xaqol zkhEQxBkx~;YENH!)fKTT9FCR;Xc}cYzQ=R);DUn=NAPqT%u#eY1+>FyNAP@HI4<`u zNXqF;Y(UjU+Uhuw^fh0A3!c7o+0sUm6iuLw!y|!%!Cig*jQ|RbgjNb}_ZQ%%koy1h zKRvOhtfE~{c`LdL9eUDzs{J_4ew=PU&afZlJS_i-z5)mE7r`KUVtrZPw_uk|*?)SX z%Ap%ua+=z9#8suNASQfWE4& zi4M+kEhvgV!wrDaS8QqwdxCxnJ5>nqPt~>W0X#yE5<* z)UNpTX5bHafq&qz@bAw`<3C&Qx$3a+>oV|3>7TO>3;&%A z{LiI)*J0tW%)mcX1pF&k9*#bfGVo_gdmld>y!1Z>OaJGC=KAyP3)1vR3chplfrj>H zP;c~J^gQ}EH9q=Wp~F~Lt3BJ>VS2*anjT{vf8Piv=ag1ji*zz2mK6;bgcppcih8rrfWNqRuCerp=As0~Dzn9eT&^VR~Vw)sc9sFzNlf zL?+e;2PsGJ(ha9>H3YI4+ToHRVc(FjXK*-|eN)sZj#yXDADq7WL9n)??D8uIr#F-j z4zJ!Z1YRGKF0+=r@>Z|Pg%f+qq>j{_(sFCnYT|W6*)3_Kz^=b+aCr^OhlJ|~hgUzD zD__woI3&GiaQf=DA>rO3;U@-%8ybd`*EBeNb^G9O!;-=94T}edSMMGSzk2_Wa5;vR zokh`Ob;;mx!_fXShvXj^5-uB@e|6=MaBxUCkPWNxQ?45YQDYlco%CBEV!UM4FMnV2 zIgEObwkBBgzE)hAyvWlwwMaKQ;o|Zf7b|{ytZ4{7GE+^r&*q0ZqbF*tv$>+aL3=jK z5;ni)=%?_hSNOb?!)JrdN?@e07qK2| z&mxxYA)DSZL$f0U{@HM8`69-0xxAIhn`E_l&W6_uGVpe|A)DV0?;jE_%fZ|2ugz=! zsve*-qr#?N9)H{L;S8}>$B-}P6yn9I491^Y^Qhxc6uuSOF<3-{7$*=-t(@0Ge;UrvA8 z_3h5D4|~9uE`8I`_WFj9&$b^P^JJs#@=6AWuPn)yXV)JX96m7{Oxq(leGh!Tl+DA; z&SRJ7$rg~7PdVYyFqkE5{BqrDhcmr{ygZ2fkWS8&@&)-PP0b{N#ZY{6EziUEv7CM$ z(mx`5KhS*McU??XtcJ=oJ@V{_#GcBqafvIMy3i9!eCmo9))_~T$v7g430fxhXTYjo zf_ke|78LkaDtX?EK^j`?lBS@u^$)b-1*N(!c|yjvZVb~xlJaxf_~zxus@3fhC`uKQ zpB*wBB)ibFQfO>+)EYY^pJ+?Fl#sDq>J^ki)`e-5)AdDD;nMo3z}8l50M|LC^%)#% zQz2&#&kTMQ8^pS<&*c+v!D%qK!bj*)u|budL0||}mYuTvUu$egiv;0@T)Lv|22|Q= zI~sg+V~S7n6@X5;P$>Y2b_&>7h^NGQJ>T1<_-%d&As0f{K_|h#oSevxmeMAQl$_<% z9DqbYvdO{Z*#`Cv9I_?H6&3CdMl>|V%96dQj25dE}d zNOcvA>kcac)Nb#RJ%w!ym|W2`YBFL{uCiFs(*GoVKAmPfKq97$Lm@?%d2o$%LTlHo1d0B?d6wU4Y zV&poqh^8sf)htntE~mrJp~_2nzxl&LhZ)2}HJCfn@-^l4|A6$sq0|2%>5hY>S8NNd zZK1-x#Ept%+M19{TQl3;vD??OEp`G^?a(l7&9;uWJ)_puwzS7I5{GxZT@qrdK5FZz zb(wZ$IxyT885T8mc7;5Su(2&`_N~1{UPZ)_d(Lz51sxiNnd^F^s^r#z3I^DuS$Buq0?)n|AL23Ununl z4xL`7`v1`BA4&e*c!UnYUtXb4`=Qen{;Ln2{-TuMbm(+N|AvF4CvNpQ;=@^F(Z1Wx z)VInJAE_s9^*G{%EQzo%lJ38E1;4;S@~6`keH>qs{yk|w9;`#Oe}bg9e@VL3zxvSW z?@IorgQT+-m2~P!){c@PpW??V^j1f_Vv%a)skzmn6Bvp`j6N@`Mx#4+Galtn2Ln;p z)LUl-i$vJN(TN4|qoPL06P-B46E6z0GUd~ro#Km5Y_Dr=*B;0$C8;5+Nb0vM8=b2x zyXG{AgyO>EA6YGx&Mx7)+jvp*m{%fNs3O%Gskro0d8U?dsHx*LA}qLfycz zhhPj`F6t>OjyF}bcOyDjnujdLx(>6XEmR~=gosz3$dJ)(v$aKGn_QdFwd!3fb}CV% z=m|NyLq!zoT$G$GRL%WU`02^(OMQAWR$3yMmg~mekg)?yKc<(UXP<6-Vs4M(Xf!qk z+paGSM2&9Uc+E!KTD6LT1^u9IT#w0T#7e~uDZkvVA9*U)LSF?7FjSkUDvTAnB6 zKUjZ1^@-%sTFTj~oX_6Kerp2m%ib#*(%coq!m8r#hzZAjy9%j>AILGNz2 z__t`4ZnZZ}E^x#@)+uCZrjTwY27)7{qY@&nXk$kt74ev%{BklJ(d`s<+Z}Qp!2@Q} zibEb5EDXmI=_)&2xKcsLNQLuA9n&2;N>ufb{_J-8?DAk@7AR&9?Qdys#715_KJ0Kz zX}Y7p>8MKuo$(JNsh~wOj=LogBHZmPrUJ~ z@K4O58H$bbIoKY0ux({#{H>;Kb&*@EA0{gCauTFP%fbowKbfAyi$7fSt2 zhfcrxKGGWwogSC;;GxrhCG`gmlCI=o^P@}PRLjz=w&rEL*l0I(> zUCd7CO7O4B%v*{;5le_B>% zSY+3SLTiIkLUV^yWuAQx#$TeJSDEbOZCTKsEm)LvG6H#o3XCZ(20*4PVNundya=?L zs`K%G*lLJiUh1Yz$lXC<(S`+rRL%o5(*Xi~<@|zMgVNepHx~8Zv7xiQ)P#m zM9hi4DvKJMMS;o;TanGyq=^kqbHUF=$Ui3nQPVr1XXWLWEle*F=oIadJtO5R=jT$I z@rK!oxD(fbzNkA`B7(0_E5@OZ8p|y$s0QlEJ4#DJ^C$p?9zt^;(4U(#SN(HGjraBH z4lOYgVT&9VQBd}@q}1KYUK-+N_?m7lal}vP z3YIv#LM2!i0;PL+Pv@BL>AO*7*L`~R&iKVT3Lh&(IYOOH%f~sSzWHP#Bm6ChR?qju zy?Rn7Vw|3w>FJ-KQmaei42x_eb_7MHAqsV)=iv2Iicu_{4D1?tWU7>Ss!L9>tJGDM zG%q>B*Y~_?8>vDLFkyNs(La&sr2YPV-c{3&`?99@-=c%{Xx-@5jV-j?9OkzvDVe@V zE5T_tq%VLH@;EfE!$8W@kH1Wi^kAqYef`i;5FIJXPs&z(*Aj;Fgs95wGwvu2Mv~`9 z4@HbF;gm{6H6%gkGHk>-rSn1544_J}%90;AS-(e*2}3G=mT?jkelA!14ByQco@-Mwem8Y8shu|azcJy6@SD>fojy&J|%jZ+|8&q9+QwrZS5a+g9+PF%68PpluHR@8f$; zT?SvsWFs6AJpT)Xqw?~dgCR@@i|6T}2%q`Cg77|>k*C}X_d02rQ)^vt{$LX)yecIF?rPEs0hho{iQG|{?$D7`2%A%wr?E#Nq@qKM76IG>dJad#U`Vtx0~D~>UXeMvM`bBd<>}QSkp*mT zFd`~G_O1k~*Tuh!j|F1q=~5Xl_IeQQ`K?>1>Ha$taoMwTig-rn8yl5J)#`=y$C|5b zkPT^&O~yv6x4%o6Ij0na-X)@s0-_UwIx4{EGv}$CJN5<3o$-01GV1k|UKVA@%zyq* zXoqO(Y@w%~3|2;yOM>Q4D;$n}Q|q0v5qfH-Zhk!3;ZSgT^3wSd6HP8_Fq;92iWbq4 z7ZfNSHA=*-HgmCHP-z~2z_Mz^B7ht1bdsVeJz%yfUSGgaWRA92nsAxTHHu%P7-cU9 zZ5{i47gBVVFuv4b^dv85p?$zsXLnG~v-^kEgD#e8ZunGWzyh#Sc;<$^kY4@E+CSzb zDq$1NvVX(xP}YCtKY2;{Z@0s2MU5~|w)`{g=sU8}=c?$lBs%Fno4}w>9?~9d&HLC4 z>{|t@My9jWwIjBS!21%2UE5UiiM+%cy4-ttaVD1^3s-FBRk&iZa76<;Lj3HCE53?6 zQM3)<3hYWwEs+uVGc}UjtJAsuXl#K|)^<-lRyerJ{h-uv2(skHYg5gQHk=6S*pcQD zDpQZ`BryNZ9&lS(z|v`1Dg?FZ*;G?u=iM3en>)q)U?-WeT~Vg*fG}fbAV_^^qwDRd zexRx5lDnygNxsSKNJP#^vkJSQZ#Rq2d+Q49Te zH`@{WN~BcmM&@ zbQ=<(#+Dai*C0TQt?BXq6(CWe02~`JdYJEuEdZLbfJ>9@v{;`+L=1+U(TvzgfYt_ zolq&GC@3_&yzkPlXXlF2A7LEf$b&gTWU(~oU9617%B;1CLLRV54NcJ%5o1FHb3sw| zXt!k4^TN7;;8y2n(L0j@*fcb=;fQyY`k+Hf>rfRz#k`5WHw^Xl`g;NRc&aL>5=PD=xkU<}y*E zFJkPpnRD^DIAf_`(W)rixhy^M)ttc*tXzz~k5uoB4U1M!4J?B@cM{TCE)@}iWsf_=xC;>CG3^p-tDw3t9#{GKhyYVx{mb`7XKpv$VX2_^JRu>_x zY`CzoC2G7#nE}gxE>nCm#DJ1gUad1m!(BjP=!UE#c%M2B8Hg1duV+;I!j-{s@e`EO za!0r_G%kLwJ$~TQomu|3=QK*bWbBLi%D&iSS0bsp#pW|GZQ4-mF)zi)j8rdNthK(ZrU{Twq|jn%E&}Lf^THoP zTG%*rdFZmx!pz)O`q#FEe5~!;3vi5t13R>ItXT@@)uT`gxm7uxNU3)}AV> zH9|$J!c_=Ry&`?4D1)w-kk(R5LAA#0qHxuP^yk+IMXP3%#TF{@ zU1v{1k<-U1({8IRi_TH@T<;)zE={2fy=NTuT*ku z0d9StUGhXqR&mElx8}(=CzWOUw+_9~{}q{^*?pYWACaN@Ln5RdbaZ@rwkZL6S=Dh? ze)*ZgKkOf$?d~HF*3U?lm(k6cZkG1+EvZpGnOzS}+b{Y4c+8EP7DtTzy1|U~9mTC_ z^e3SIs+JEKk4li!UyMh+`gX>nQebG@0JK}Q zD(vv)c9)3RYH92J=!rn?F!`Gf45A{#&w9D{h%P@`9Luf-_xRd(k2hV+AG_yeX{C6;`nA@1Dt(k*eL!pdlWNCI+o?Fm zcIeIYbxjp#;+OdhDIK37Ez^}#3|As zoNRm^QRxu6Yq}tM0Uh5Z)^N}sVyzou!*vVeUfjenWvYq?P)P>+51toTfX-46#Z}^9 z1bm7oB65Wesjg?4J4yiVjKTp}foS_`Uwl+F>Bz*9j=R}TA5?yU%*BKmGgGpImhZL8 zug|YQMyg$GI$BkieoxmS_6SOKW@=1O?|9p#H-93hjF-}V`hHevPu3^mv}xyc|M9ml z_Bv#|h@RBXL5TkCyMyLhQlxFTW@lF-5rQIK;x>%bWiz z_9^7>LyoMz{ry>X!&=WkX+)C8M`SyI&|R^tIHznkl`>HCnV0@*^S*O)-g3_--J0;w zc4ua_75!&Tcyvg(c}V!yL18)^F_*QMLUXdynLyb1q}DJiwndG%D%uBh?J57h)i2~}S-QkH_Q4KHY+h2-Q^y+u}-)NEf zRD0~=TI0o#@p{DA6tcGV&()0=spHkB@(@ULyO_oP;LlW5y+1a*Vq4!om1*R^k6NCS zN{n@h-RDp2(YL>>SG=R&ecB4jw#&5q1*(P0M|gd1eIQ^@WkL(`d|NWUWf+U+B~?=( z_@&&{Jf4%0M%{Soj-^U%`b!_^|0815r}jU!Y=~c=OarF#zf6z|c~GgR&Td!lca#R0 z&|@?qY&D&pj}5c@uWqF=$!a~(iJ<&ze76)95Prtavp43XEp+AD$y=mUYn0`(!jh7O z4xEi&Wsiu|$b5MtU5|8A(g}>|nb*ZGO>8JI*WN2@lwEgQ8u58cmE}Vm9*3{*`M2}y zWAXB!_33tf)5m^UWs2UouD-N=GX~Y?5HIc*)?f?F9VlJZKD>)xR^{Q__r_cK?Q3oSF!E6zW)GZ?6CYhUUt%FaoNytlK7N@ zq36aG6o~LuImBUZylnaVHYpiunCAA==;U{`*8Rd$Cn9vB$!AKJ2xe|7PSu^*-BS7_ z!GI(g1Lk`uM(P(-WUmq^ctJrGo{vX;J|0FC#hJ3p5y-MA)LBcRpe5!DQfRy7zdDQ4 z`3k4_OvBD1&E0C&8C^n^N^~8Ma@AzPLa4c@Hd#7NYOqNTX)OP16vqyU+dxta9j3^jNkfDWKu=*$v5%L|gGns^DU_nEv00|I zJ*0&I$|y!tXY!gqT_o!^pEnCTTis2Q&tpTKwWZZs5zrgAXlq<&PF@fj zJ^75-NNbCj>#ZABzC%Kn>r6_qIaynS)tzSd+z1eTmR)~IdtHOkM%D~>h7zAUwR>e& z)&Qw)L4nJRbpeD z_INg2U!Ys-tPZ(j1>`yw)mBXo#tN(sy%D|ttgBu!j9mqM;Nb9~#N=Qgf}O6l_6eUH z-^5=B`BN7bEJ~akh<%tTHIkYflr5Egod85p*p(rr(d4z9opxcKW$6JRaWLTAmlzj~ zCN26liV3Q{EGo0X{MDHUEdl#3Q$TvC4p{zT$}slR3bqJsu7w9s!>{QRw-53!&{;>f z73!%=2lfBHm@C|NJ66DE^Sx7~dg^YqwAM-l3HvTl`HRKLvxKQfPz;ltQIcN2O!>Eq z@x?mwkxEdejkT1&S4D3$KTC>!h~0oqwjmwWXGdK?)Jy58*X*cjqB_%2op#hQM6FLp zJ!waIkW(xL^l3sz9VwY_R+#Bw?%?`_1dhpq3?$lQckbnx|qLcPl)? zinf}jN$+f<`WD8(vx0^(ES#FP01-RmHnH%K8&K>-u`SUI<>nZcsMQKYlyYhAO)a>Sz?LpevHYJiRteyCGJke5NVQt=F%{dw zsp%e7x8)yCK2r50GffhY#D|Mq$L@a3e18-fXlFF_aH(`i#JChj{zfoj{7u=y3(%Od zzB_jw?zQGU^U*O8qmQgiWu_=eeQ^!GXkIVrq03~Sn<&#ZLZW7+VCmR;!WG-- z@GkebTajuRMO!2CFPg0J+KI!({4k5;KjHNTDa@h(WD-;^=+pp0NDWAy>5OJgv1}q*@uSS&V}LLf_M+xz zlg09%1Wd5SinC&dWfUWa>0PIvg42||7=J~q?gHnq`L3Wh#EFI$@s zLSR|S@*hp3O!r4wqD%t3?{!#OXbT0sZ(XA(I7#6C@?jQkByhED=k<@JjLRggBbWB? z^@7%DnbIsE`Q$A^$zp|v%r|4xX+p~Tq>vLE$9v1|ss;(O%($1?u;u?7MMy&t#O@nO zSAdrZAOk+>ox@xIISjW7j2EgHBY9Q-o|{ACnj9L;shSBzljp&m=tN=2C{JO;LENgO zRPGP*wRum2a9*OOn~{y?-x(XF%t(p7v@@FWmMFZRDQzNx(bP76qlf_HrFwWD;$g0r z4KGP|t3;`3u+}2GKq~g>$+Jsn3me|fj~bqEYT8J0-T+w0nq87~FXm13><<7A8}7LT zYvF^ajzJ{#p&gpn+nBKH!D1rrJpmY_H|aiwCqCp5YnkLchEUQy&d#s3R8pRlyB`^< zSDo!8K0c1je`hq}v$G_N$$QfMiHh-VC8PY-&AnO~HQSguyB%85%tP7Da zQueoqf*$nrG$>TD&GL7xQ(|jk*yxrudA17qzn1N|D?Sk3k{v#rmm}8l{}^!mVJYuX z1SzRvTQieciq)7F1$}) zC2amM8H9eV;;0eA{!(cqRLvyF!RRvpgUvr&rdVn#wFsNno1N02?iWQ{%G?11_djX< z7_2-C$un3#>RRwc(T{&A6U25XOBdY|HeUXsZV3rWSlOZ(VYR|V$qE^WX%+Lsc1cw# zCVT>gAw^EkNF4C8{Lc#j`HCUH=0@1Hq8+LHfZO4yI{PcMVkMq!@13`(=A6C*?BStR zKEx@}i-?BFZs~LivtLih@LwsQ!e?b7IgXCvY{sNk-E8y-bK}A=Cx9!1QU=ln^!-Rq|S#MsROL@CFUeW{&q znf-_j%|2CS_uJWDyFz6@Kb@V8N#m>Se3OUft5Es=@<&zekL-L}Iv@M1#*Z4B;Y5|; zmv)A4+8O>CQ+UQ!(2k>3`n@V$DWPi>xy&Q*)Yz0N0m6J?tfG{(gJCz92X=*bnADc? zbF7i(N7-#`cxR;F1BQ1ZuS!2nlQ@(f3*)wzuCmWioShDJD*M+yf0w8kyF_|!%^Svn z6(w|P2OOHoC&%t4Jn;vaXm0Oz_cO1Rfo`6z_-8v3=JD4p%kW-C`>4)s{_a&7NOmpx z+ws)-*_Lfu>i{_!QhqXos(ZB7-Grlt|04!uUF$ZjrI#>`O4$U9S46n+DCHdugI8=* z_Z>Be9#*OCv177=o-CKxS(wbqAZ3bDGm99tV(PUA z`Nu@fAD4nCKHLP8TRAzA8hV*ehFKa8l7ds-kYU zbrYpZCw03&OTWh0-{DPAudo~$R+E9`Zwe*?X}C|9D&*vcyHwr=NtQf%SblhvT`WXp3HPhJY4&pSNa|mQkbVg<`bJ%AHs|yEJ2D*QC_X=C+3deKh|dq+_y|=2iI~T|IZmdcTI=1^ zmzw3l!?%_D4zlz9KERNIu3*l9ZFGUNtC|cBt#rLUGjF{&HJ^E4QXA>O>Z<8Qu z`w1!$>d8|~MgIaRuRT`WY{wM>7kB;E*x6&jj+%~zae|Wg9 zCUJw%ktc5%}Tt?MO9 z$~~@5q3R zAp_<}fD!lxDZ~Vt={q(2*uc8ww@%Cdbr2G_F+N9G1WXUese@xPsTuj3s0raNaI73n z2Q-O4&6;52={{nSD5~~Yp>FNY$!q$mGEw7IdB%_@N*>GqYjDEgmYrxADZ8&$MhYis zh=^EkXX=ut@JFKNWy*G}l>9|1{|%Y^Wq7^atH>`kzbn(8AUo*u^dQN5uOr#;-eu#b zYqc=<22SIpyep-jtO)^1OVnH|b=)b@J$Cd6qPyL*h(47wSBb-f{RhBzyZ=QaM91_Uy(w?F zY+^i+V8aiH^0Fq}q~M>;!2e5*L2YUc-A7K~NeYl1`_qWiuq|Xurvj=PF%1pe!2Od~T5G80d51?-#^a?+Xn~$e z$?!Bk<1Dniex;<=-R1%iV>uN>oy?~gX_5aswCDe+IA9*>c6zKgQQ zUrgEQ^^Ga4G`R(fROrW($K4!Cg|<(gbjvVZ`$_lGv-H%ILcPX`l3Etk)@+#4`LbWS@8KBV?npku>5XGw(!J3cG!je znO2&gUXV4x{wi~#cxr`fx@e+`6j->GT{Py38G;i*SQ&6i$mZ84GU?s5;z&8amGXa6 zrojN3pDnQ&ALwvpydidrGzkxOc7K?xRfs=nkoliks`xhgF6+sL6hr%Z->>ufn(Zgl zDwg5vL?`xJv0_#aKJOnLZi_eQ#v?-Y;3O-wugfW?3O1U5A1$qEm9ZHyK1}a-RmMrV z__sD^)_-u%Yb}4HxkJ{N&)4s5bqL%&Ue0F@Y+EZXC7AN z;y|lyI7p#tg~^#Kv|9K;+8mXGBAvn<%#UQP>F>f(taZ}Pp!WSPbH7)(V2e4xGssCk z@%bw`)x2H=u3GfF>{8|`xwWq13@VC3hr(vruX0_G!3GAngTwmjQa~^QGeK$gAe6ez zt8G-e`<9?~l>2KCCbp6jeKKi7qUHrWeGaYVH_{8T(3at!F89K#NWt*tIt#7C9VG>x` zZ5iGx$b`~zn`>D!1bjG0CW&&c6>zC*H=)FSr(GE$(7l}q7VKKD0YGNn0d{7cgf`TO z&(nq;f>~{NDTQaXVdi|JvD5IL161|KSgAeF-?Uu%PyB5K2ycd?xfGRSY)p zeGSv~JaN^PA!&OF`v}awihzz zSKa;M@#16!CS*JLObmO`|{cSyxI0x`Y8&?)Nz9w_ofs4UnPSsCU1#mW0|4q()+8mPZ|J;2}?&}axay^;x3H~xQ`gBu`zS+t% z*B%?Gl=JJjwt8)MpW4O=)5$Iln9_Mko&zg0p5<cqw3YZ2&6#CEo8C`0) zFOr0+sUGIUshK{DvE>d*RFYE-%zYOM=vV!mol&nt7CtU$p|${Rp<+r$|a=1dCWU?#S?5~Y?3onF`T{5kQJ!}8{VDI_s$6}{XT1|_yko2_qLqSFC7MW+Scw27?_={+ zy;_T$f-?Mtk|vf)TD^UY7^sCc8Azhn$Z!)G4F4|Tm111Si>={EXLNdJlb#&^0?|X& zmizN*Quz2Yq{`&@RRWSMAPmI2#yF~Cv`LAXE!v|eh?(nXl z3SM`cbJZqsw}cttT8p3lLdhX;S)Q!e#jRZ3(2CCPVUq$o6NI3%0S>38vYE}lMkJ5W zjU5ZW=3R=A-BDovLxNzgwX7pdu4Bl>lF+6nkjlO4X!M1Q`~g+#k8P0sa}>z^|E*Sy zE};N9Gr=M13mei-fm;(bGl(u!2D%pBk*KLA=!_R8HU`u!Mq4a!9e`l7bfmqgBbvFT z&Nzz4dRf6MH(nJ66%_*YIiC&7UL{z8T=c^R%Zc4dJ)hlL0N9WM;cZ!4fVC8=p5Q>Wf&$e%d;PvRC^8H@& zAy-v;9$qScIZ6UKQU)s0xCIIbarZ?Gklf}?c2PggFG^iuHUt;{p>Qd;3#iL4Kw1NS zZ{dm5xvh)q7yBLpEN^X<)J{odO^<6z-w+Vs=Dx!de5b-&3!7WuRkt(!Wb!TX{J^h>#+i3owQyDr4lZ|Fg z1J%Kb|FSy*lZPW7=G|;l+hjf}fBRmLzqT*ayq#GWz$aX-bA5yeXgLty@7Ak(Vq+@Q zsUdH}rMM0Mt)yicckK3moXLB>op-polmh$5nI{uw)5rP^sz?`m9)_DUU(ViADY^mQ zdX19J+~1p#K@xBNgw+f-!bzqh%zX@I0>jNmiS6H`(&I+_+Nh7NXz4r5am;L(VTHdMtf= zk6y7(PuA|{XKFt69N>FDbpu+lyH2dQFQgTFYl+Xmgw%VnH^Q^aJ(|Bx**YWQJ+VHx zu=6=4pbm3*mM2TNuwZT7CGt^B_rg0YD}Jlq=w8JOL!(YTihRvg^|efGFJC(f+nbMP zmc-{oC$8i2Mtt(KOZ38CUfK0t)D%q>@lAv=rIG5bY!9DN6s_)9I+Ej{g&kZOvy}P3 zy0ZQ^VTjgkiuJ}MZYy=%DNHyLMjyr#2@IO*?pM1H@7ZZir@Jb)LEKR-yW+z+J9gfD z#SsV!8>XqIkr>B4J7cbvZSm3fY&3OPg~vbeIVr{!nUWd9{)^8iJ9GS(^a}r{m@Ax4 z<5-%W_{5T9z#GkO#*mEj?eyC&(#Y&f6C95@jdEc3E9W>j+S~=>_-Yl z#cD0P={Nf1EmFXe{Q!320CDeT;_kKM_7is?6Sq8D$iCQIR^wy!BCclYi3wMOh#)kN zC&yAPf9)d*3;)^YVH?SzI(58mA}8y%h(4(3W_nsBz}q+K6)YtC&mz!&JoYyJ)KF@+ zQyfi|s=lP#BY@O2kwLYV{gJEC!OS+irNV!If;6kpgz#lBbM}~Pa-JJ5iPw3!Izp}e z0DeD4mF2JafwbEDOxnU?>i>|$2*ES`%5^&4s}%6Qhj2iiBrThx!v7|$I<2pV;`8Cp zQt4f!8}7;)@J)G7AjF|aeDk_m$*@Gb_n2aFMsRJl8E>rFs#Y4#)HR;^9&KVo26uJ3F*)x~5a{87LLw}Di@+x!U+ z`8J}&am$1}`xx=;z-rgND^jDZuLMPu)}QzoY;!(+yDJwU?Pa7);Ru49IuK4ZH+*ba z_w11EgTZH=iJF>==oqc_MUuMRrx6gfruND+v}T#h7(C|b@~4(R^x7Y7ir>e>+_q8a zqEpPLC=>7i={MNv7fbrNq<6b_T*#eK6pEuB@p&7^uw`@n`KC__L=Gh?@oO?oFgq2R zF_cHw^O6BeTip%jEub(?@>=c*;HPNRUto`OOiOjZl^r(FLM|GxM?0S^XSAp73Xz?G z;5`Qn_dh_TwSC1!R!`EslXvs?80v}`tSDCqp7F5ROJmHL!)Wr;{7FnWhIohhj*xAm z`2sjrYzyAA#eCJqcK8r{lu>9S@iF;8LT5=OIX}R)0wwAiRo#=mQ7tIujU4Gmb7@t6 zoqARduW0WZpzWzC7JlekY8fui;Mzv!#Yf8YTY9H|jOBmt`-qnv%ty*)qVx>0P)&|J zZ#Gx|hdD?#>7o3jhzvrNl(3xfN(wzo8B>z&G}5wx1(ZS6Rd&^}(U}Cxe=`*$@7)Qj zFdod-d1_vrF>bDLTmNGtJt8&V$+#)+ACrMa?q+fCA6BtTQW=X3kL7e>(Ds|<_*r!x zR|kn5xR*hKmG+*(v|G+P!XRX~nLsGtcON={e$UBcHRb60IoKR*zT+ArOzplxuubO8 zTi`BxK6SB5QS+(E>eZf4!BM(x-`~YH6WIRvXvW|##)6jdolbw+o)CcY+MUwuRDTBEg)N~K`1p91sZDJJN@b#;@V@iqk5CB z(%Mm5sn=!fWmfAk_T#TA;)1b+_vhyzWhV@rZ)r8{!<>#-Sxv*R0!QqGnkDi$+Wq8# z&)~|yEv5Q%PLL9`SXJ&=Hik+2w`ZYM5ics@xpy*BgsTVcIJshj_86Fj4dDlu;3ZgT&;M=2GRaUthEvI)J^#-!%Wmj) ze|WY{p>FrTd1IJL$}!94@b}nd7f_h9m%BLv`lw#<#=#&Al+t07hs7ru?sjq{dOY3k z-|zw}abV$ByoL6424Y7hdIH_HtBwVH({|#5jW!8K!9gFyK3Mg%q!Zf+)emN z@g{-f|T*k(iPkBxdFlF+Rdg{A7NW@S>`&s_Ul0rF?^~6=Bm87P2t5gLf7bl3% zLap7VOi3VEhPQk`UIJoN6{@0JdnA#$YQ3Ipm1+bL6&YIlpwyaZXJ*#gCU~ByM7Es+ z+0%eTs%J?t$a9WxgXMqrep#o*)hCM#fCO0_9^l}uyz5fwPXUnHf|mckctr#iVS#&6 zdZDu4Wb0Qiw8C*KBwGl5nZBv{VIVzU3Lsw03i{=RNsqmG+CviouI`M->7i5|-mKaZ z%YTzpV$@2=$2}E%sxOg7=DApI*_SKS!tVkK#`v zP?4((lb!QK?yCd`cth7ehRfUwC5Q|Xl<_rqTcd=n3IARtSA!6a6YeYGHf7QMX7{78 zMZ)7UZ|GAm1!Tp}M;4uiAIat)CHmOgG4*tj9W|4A^dEsnlupN3-6M@m$ZZdAF731B zpi(|cc4qR9we$LhFVFs&xHXd4?^$}{zK-FsF^g8+<2=_@96yyqRS~O) zxcI;M6m6uVuP}Z*R!K~=(~Io%_+A~C<1y`t9aiTmfC^aE#8q8Q376{Xj`47@N6seU zy{Zlu`{EqU3zf)gN&Ix2FoB4*A^vhCvHSRRC9xl7@*g8U8cF^)$;0~-63qS#wy9`6 zy0(EUisJXXq2RF4H2$x=3L=juh=)+_R2I^Zq6P zv~OLDd7?c(;h`DGW-WnUz*b?~*_(KexNa0p7ct)d;BnM@#Q3z6PmoN}e9;0X+s-Lk zanv~=F1bAV8Xb5y&-tchO*QUqe*HTXR=B}>EAU?q$NBtD;CBYUZ}3~fuZiC(er5dH z`StRv=eL{R6Z{7F`5wS~$y1Z>k#uU$j$D+KE0pjfdL+d01o;?)C)^e+=+xGXZ1Z$} zpxs#Tfwt1o$^Do{f%fbS`EvJ+kz9YYh^w!{ZTiTFv9Z$_Ndbn+?^4-az)Sd9zUts% zgL2T-nF&k&Vd+Tn4}(4hTqP;d-0bAAOD#&*w`K;1cO7lICs%*E>Q2Fje0G*jb>F*P z&a;_vs8^?;%>IE)J)~3Fg`@493MkxGKbrn<6-NuPojZKUd@XjQR>my*tpSNwky`Pn z=)?}2--c@~-;*vsB{UGN_9o9Lblg0)53i6T*MFq#)=r72{Pc6Nk1t{(aLqyop#svv9dB) zMiQpaM$r{Jj@!ob@F@-{T%;{BJl>-uE#{q(aycp92OH}ALhxoMvMyN6b&)s@+Uz*-aAY_9i z)JsCprguG&L5VDv$N<^Pi3v!IPxBQO$A2v)rpcWl>CyT0bPB41mymNnk&`>=d?Q0} z3WHh*IYhc_+LX(WeBpC@k>{N0e^Ko>FWJ)O2n5HXh;~EABCVl4EnIFXr@wh3JR1MdUgx%3rqNi(+4GRMQQ$%FXg6h5|zxXp4Y3>ixE zCY%`R<+pnge+J^O2vss9IWoP!kXkaV^`WB0Fr-@8src#h?>iiy@;e#J;bWdIo@4lx z^81kRsXRUWg8a()Ir#bb1^5k+uJeo-TakxfSuFCfrTJcB*$)ZR(DIP=7W>ex)QId#d-e)sM`i ztFXk&HFU~HjVJXlVNvhy6%%du$GWt8!q&Y@?qPa6{SUSc&w|_4ZLIl(`j6Y^@X1z` z5Kb3P+HX(`cXH7p_f1iovJcE({ysggWlM8*j$>Yl<5f56=l74$tB&T{8skC=*cN|Z z_HAMkT*`;W`uAQpj>K&)#~nvS8)vxm#tXUst!PBF(KWKJumeSh{XrAZug#Sg zPMx|tc0_Y!-7KCXn=7SNv19v>H22{+O)M{WtnirqyjprWhL_kb=AR{{p&UOjNHQLj zo%87n`STn;Jd-x!8niO^Nk0-R?G3HWdy0BHpq*0NPW(hd+9`FtB$TVy+T8?M@7op| zzpua-V_B4K$9*0#1dRC@_d$%wzqV0WYA#)Pr-_BECl_Ko-+DKZ*ns@l8N4f2ysuC` zX@b!?yt{U{Blr-xfQn%ZfyWXh7sGdcR%yL}`%X9&j>9FY1^U>@bZL}?*K9sOk z_G5NIe`7@v7q#u`FSWLq+eXkIsWE>7vj#latMtZcK1g4^*tNpV4U8*>vtM(EIG~tP zRDzAnUxV6rJMC$3x%3$uJ)}-Bzh3PdvhTg1){wEhh}+;02YecZokLs-XilU4I($q9 z7YiN5d#tw)u3Uh#NH3 z`NjEoe5Fwh(uBM#Aksb%<`Ii&(Gu+P<$>&ybHG?yq^^^f+e)|#$auAu+wOLmOW>!l zv4g$XExVBvQ5Ja?8?QxWgFq-T(h4O!9CnWL9krFdaB7A%Q}9W-zCEpaL8WWOaCvxE zctXZjS`+^(yfYe~ik@;cMN-G!9Vz@P-v52E6|$|PaTdK(=fUa{9?^`ct}5q>6Zy_f zRmipS=y3H3clg8ASGZPcp+uz>FAODp>IqSjE7wuC| zn_gH8iz;9A^-L1z<9a#eGCxO~&QK!2r1?ZPs`S5V_O4Y!C6CwAexiV!=BQWKC%fsTH)sfy+=QG4gI@=H$^n?II})$`{s8)ZHquZBJ!A0KPq zGd=3NEqKsiuIvI@-W-Sf>aJ9om(P!%ZC|OKeKGdz|3kI~} z2quVLj^&2w5)2ZSqt?w zT)QmubfZ;y4tQ_S8(VE>f$YaIS_cFz;+Jb{TE!)TAZ_ClOhJjmnNZs+-#MGFb_H|v zNH*O;*SSp*;|PHW=-sueME_ZSt65}yN?NN|Y%(ivAlSTKsA12SZCRK8d}omR_r-5x z^ZZ`(F3DwFg!rg$FyksPuadxGbFm7{WE;I4w3W|sZPp9x+2w+N8wk)*h*DNS2|e>h zwx3J)!%Q6PqJx6!Vx+WuPy5f)Uq@?V&ECGKCdLHSDY`qXC$?DXd@EnaSr+I%+ zN@S;ed+gXKw-Ki|yP@BIp|^*A;gWLMw=&_=Dm&m^`# zvjU&y^O;O%K{>O8FTBq}!3IjoF3L+ByPb>8lPSm?Mj=34PBm!=HC{%MmI>2`bWv7E zBH%NhWeBo{U#DzXq*#=_uGdpqopFt)&iGoyxV*A%;)nZcU5i4-ExOkFXT_-33ny_v zqPjv$VE#Ca&-Fsf2!HB|Ru5=*{Q?*Q9tk8KA&|J^Q=h{=7sG3jGd7lFWB`LGQpg#a zSFPUG_T_i3h&A8oP!?Jlo2&2tE(OPq!#UH&n|adp$}cyX!^F~e+@p6B(MI%BUr71- zB88hM$h@OOhR*b0)OgXD^QkXlyc{;_n6tm41_dc)iuW!5sFf7D0%)tN?`&p2xb|P9 zm)4>bPVFDhVSuOjw(4{}Ecs|>UE%tW)eEBJ;sk}!0kU_^qZx`8Vk0q3 zFD+-gD_fbC@osNtTBzfy7FHlJI5 z^G@UeD6#vcL`Kh9HEQ&rkK=NLPnO!TA>4b9M{FkDQ>tV?qwt^D$?RrzR}v%pUEJm; zlgX)scUDdt-ks!=Yf>wuz|rP)PsmpJ^-`Fzqhgy1i|S|Rf&1T;Qqm3?a~sWH*@da< zZC!xaREWA&9cK8cK{^}xp&W0Cnne_DaZs;au1v!(dd3}al6N+-Y*v1U6@ESOlAJQd zx@wSyyV-lzKhN-<0fhMm>BLQcemQwlGm_?asmAagX(KG)*!aEXZxvWvGrR|mlMQd? zV?;0q!1`s=w;aXaX!ci=lS9$>QNJ?I1}&G^a~;07!m5dR7P;ArM^SRn<`fn+-e&EA z&X{S}bDZJ5UlO5XkvW#Qgtv*XobwQRt6fFXCx-XBEZD$L1?=krHb}12HR{LK8NEot zkl}JB_J7XB-Ujz|zhvGe1kUR}b=aKCug!Pmk|t(GjZ=>m4|dQ9d-#s7%^I5`jrDx= zip7Vb0_JhZA=M2b5jmLmK%2iFlD+y4k86c1$zNLtQxcj0B>F%gy>3lrVXzCMjbCR` z@ZF=<^OBrk(wbeaKO);X645cC#MkxSc zUm9CB7m0Ej5 zDleA92boDkyHnTqQo;j9S-g96_5Ydlg_GPmdLUK3?jHkIk#& zmXWIbmXH*xr&0wbkcosh<<>|8M4!7iY+I1~E3dCh(Tm6z%RYM1-pvi?nZD0O4>S z$nnS56>CdBDRWbHdDU*DUz8zWT5JX}>NXm4U$1^iON4mbR?@jZPOgwQ$ zkuO=drFFxKubER-blsa#V|uyXID&y0G0>pBe3aja3hMNibe>0&!X?G|ol z*kIIdw|XLp^<(;5?y0@8PUe`k7h?-xO=B=`xu>?9H|7hqE2U*TZ{ZP)q^1o>xRQu} z$xFH1jf)k;HtDX~9;bOTOhoIU0c%E3Jz2LAO5F7cT8A+buDrTOYki+a8ifdoAuaxo%Ei!TF3!mM>c>GsxU^P?HJ#{y-h$;lY0V}eM{*-QJT7eEt zu$ao7qgS!z_gG5qKSy}t=5pF{V|;-S2;wQ4aM=cAf`&pDYLsIiiZbQ`kTZYAwh|c* z@ssn;5Mfj;!>GDl&oinPcACFI0&g;ZE`Pbw3*_&TGkdAJkIj#idhj28nk*C&PH`5{ z_8+Nx(D@8?R!}AOENRAAax^J>9Ao@#xhA#616!L%gG`uf+;)SRT<6v+&%|<{BQ{T# zZZC?74Y9-gKm>l(d|xcse4g?TRyLof{DriyMV+BaH@=)XSs2M*rijhpxE^vn#gR<7 zy7haEgT$Rh810Yq7DmDBkm{I1HmK25n;uQv<*%pgYg3eNqFrH%`Ng~Nl&zntImE9e zrcsKXT3!^czU4#h-V>zE>Ra|}_sMuO0}MgE5y#hO@8F0i-GTmQHLX3C@ z%sn6~VnntYq3CCryCmY^g|vS$$u8t)xk8#Cq}JL@BhCB22)-}u*%OQ}GH)iy(A3at zDQySX+Q8oV`40ZnJN3p5stJv=_Ye=HF%}8@zf|Mp4jx4!GNd#RE!p7UrAZ0L7A}Uu~q<| zXw>M#-NN-d;9fgvUxih5?uzB+Zy8Yxzlc%GhbWg9Mfd>{vOCGK{$zPqxgzc94M zX?0hhcSoDpcJ-lqlv5H3W>5KT&!n2Dn3w*yvcoXHWOtpln>n@mSTFm_dzqEV)gkHj z4)zVOIbHVF?Fv`(WUfF0_Ltk)RvbKAg`F*(Aw-5%2X`cxcRtQ? z!BG4&lk)O9W3O1as>`^KyK1>BrqwkrcbV@aKDah}c>gd&U)bRC0Q&P5kd$l3i_PsM zGf6{~qK2V1q9|B-t50i@Js?%Ld*Ys|YK^NR2~^)P5-{jONdx-|n{bl;f9$;pcvR)t z_??vu>jWecMCu4pqd^-DYQjXBlg!8&Gf}9ZxR$o z_O0#TzP8nF2(B3r5*Aro#ia_aXE<6>TL>=9_q(5SW|AO?ZQtv={?~VXUNSk$bDs5n z?&sd`y#vaP3Gi~!8W=71l`mT{?gkKxoXlV38{z?b<0r~Rf{(iGFC()z#)QpQVfMW7 z;khD0w;*=5CsSjgaemmGK>_;t51C7;<83ZlXP<}T;3lh*s2f^r$w-g4{Xuz81mF07 zt4SXgPF?J=c1lc-#DI)g&}t1!ShMi#t17Ke6oMU6j5CF~8TCGr(qAn#X;7Q*_4a<=Y9Obu&k^j{GF$r=6cWt)=oS zws8H?zoJaD64BfjJpOS-;YW^_z{1>gGaBnC;)F)bIgohZ5vx?9hMEDNj^k{njM`i{ zRpOl~3}H^u1<`=`zZ}qo)DRpLF&FZ!5_fN~Zbc?-r3J?(=K8D^>SAWK^^jbc;xzXq zm3YXB5)zpP#ysJTw)G`kH20#%$Pdaan%uY9gN#)PoiPLSz(I>sbyWpaM&)lZWuCv+k1P0T#nba1lw&VMsE^@XV5_sjH+ABu#%s8r}y% zJ2H}|F^1?br|-A=wdhu`OMyX&AUGc=#z!(|%47w`Z?kn3Q-z4aOUv0nZ48}#e2>hx z|6}jB-{L}^eEV2t)xQKD<0vj7v-eq@pd@g!2c*2mY%CPmvVPs?S?uP*k^)t-k z9Af=8dD45zUPTgx-p-srXRNFHe3PqZYM*CLJ?Hk;gY!7ug0&J!(giRQ5{Rza3R1%U zLXapK9gHIvYfP4KkwhVzE2~P!t1@K_-5SPe{ELJR+ z$W%_s%U4?^Ka~x5;6G&p=K01xx-YTMW;Of7lyQ$E~(*hYih~!Bcv~{9ahEAtUnMA*^)J-@+GXaRu*EZ42+J>V3vK`VBt% zgvMiwhZIDo^dg=^VlEbDdClv8Sh^3a}o#&!tbqhTPLnf2wT!>t1w#2Uu#jl=EAkeM5;c*P+K%d-e2!?DitNO7m0IFK%7cVC`Z533D<>+;xJx18;n~t8ud}Q1HJrB{ zsb-mlBHD>9rv$-{%+4HpHKoXxOFVfgzQL`G@OUqN6^EFiA)X>_enrVW9SL$@BcWdV zAmm13*4tXw*(wT8SW02qs;$Lef+||}w7f(5E&sXkj35hu1e{QUlp||jTK2x}am(D< zTR2X?osXfn{`57fOI7^#&%g#!=kZ_F^WLHffqhAmK96*lx@SSzN(2N2&z$Vm{wMy?0dhkaD*;7(jX zYS!;vRc2Hti{hOf*nq`flU_-zv2LS`H_yKD zCbnov(jAzZb=&VTKHt32=*O{PV#%kK^RE))D}FLUIUXp_^8o`s z9Z5FKgSg^m^nH8v*vK?A`F}^)IhR zj~2tuxQawc#<8*e1C3EytY@e(vTgV5y{q2!-kq*P2}Oh_G#12<$l3O!w3!i{xXlh} zOMHoV_afp2qk2g9{`tPO(zmm7eFHFb)w#&$>D=R=?i|{WB})!R z-1q=7&9s8a_Yp5i7~}z_J~EvdvNw&J8+VA6IFixVAFX+x++a>Q+afRMlM{=5iMJ2e?f~4%-WS) ziv4_+?6CYXJ?s&=#RjJ#D{9i!A^(n>A5-(6(@j#dU}Y>NTG)XtmHdMhYGEg~RP(Pk z(Ncd+q9u@MncC4ZT~C-Z1u_!mEcs&=>514JbtPic1dK3*{7f!D@x>8pOPCU<(fZR5 zvvz33@5+t*Z#rMwPN4e3MglLM^@Sfa*yz6uG2Vd?V0*8Ru;Scdn!PY$-q)w@MebbPCe z2q%(6k=@ZF2aH*m^mGj7lVownQOPYyz-=`GXOy#|iB$-T^U&Ew3|r~xT<^^rSTc#m zCAE;8bE+q3o)tTh04gDLykX9PSy+SAJY${B1c*{_N_nh)oa`x?U;mU!PV>Z+O1n64 z#yc_6zj-9VSaC3MmNcygl52!gn(EL;601tVoRaG)z*7THDCkA(F~SvAoD-6Cb7lrw`a34f$7*|@>}GMr+6bqGpSQwTY_)U?5X^lP>>Pr ziJon)j4iG%@ZK?lyJWLwUNm8XukkX{CVQW3CO%rEdMA-cA^X>*O4bvQw3;}jiRKA- zqj=*wpNPz8KRx2W^TwacU9g#y1a6Lq;VzS(YWX=Dftkmir3JpNl5i zoEE)B6D2!t?|E)G@-AkSjXtBgalG+`#u{T}R>wuIw@e0xLfS0y{B7_`e1Oe%==Z{*$pC0t9BGSpB~r1O$%%T@X0^dwCG}0@hgy z1YF&_ka*8qe|8#a)=hP$B`?x?k94emor6vt>!ZC-uGh`)3Ak9vUjrRI*=Xs&=4!h* z+g!~i7-2n)my?T3eplItxpH_br#(TR3`*BcQ)&vbbFhy0=nYzVbamO)pqk&?cZR|6kKS zWU7G}K@ITp4EYD}&i#h$>`b^^HYdk?B{onat@L;EQ}EkSic&7>Wfc{VC4YBqr}q+jRLuO_EoC-?U2 z=)V0@jGOc;scDVpMMv)~M3_DcY}%zwK>7-fBo@J+!9|T1L{CyLo1K^JvI1*6^oYCQ zotzyT?sfPxG|XHw*wIZ^mD`G#JqZ;&Qyjf}3q{uUR)AKTY(A%nB8)Xiy?$)d)(=%}`v z3eS+}-%Ull6uE)4XE!#UrCv?UzZ#r-b^L^xjlP9rIPg04M+?huRRU|gs#nb~qlSSL z%Nb4`aqbz;A?N-QEG^u*4_5sDbKm%{%)LYZ;)@|fub-WWH5mA_XLM=xQpEGVi0AGMJf1 z1)QNN>~iaUF73)#xVHRJ0b{&HtjTo9E>$+yVpq%_4l(Su#Q_A!^<0@3;R|#P^!n7g zEMm?tnX4ezr25n3YJh4;)Yzxm$7u2Aj54{dv4@$fwp#MeBli&C*<=eb>mPXF6)YS1 zPIa)9ej~0uqcf#F^}s7I?;(0n!s&3z+F;{UOnkF#`)3*h+xBaQM1CsPl3g&`osjxp zDH+v45lWczC)UiLTC;EOsv*&eNUEipq!)Thoe;nGS*IyA7o11xIuhLcL7_`wJtBy? z#a#xgHBt~-F(%P$Q8QO8ovy^CS&MIumQ#-UeqSqo6tAMTkTYz9bw3+8c1uCQ&11~w z+O$Q{E+bczp>DDsp(vddf^4SHRbU1_mS7GnJ1mYCzeaOQ83$XVc zft7z1_7;HH4}!hbbO_jco8J6?5BAOy6_RkhpACCifa!>^w;O$p1A7JR;Ln7;rzrY4 zU=OiJ&aPRHX~GA=|c9I)^5-dV55)T%Jz>b8vPFMk=7Vp1N^n>6P3dme6N!u}(+# zj`mnFP0{1^*i#-M3!}GhG@5DdB0X6&A>4Sv!jbyQ<7rm@sfaQqYn!$CIxcEd&;}h9-G_ipK;sI99H;h| z(FouH8!qg9EK2Rv;+cveQrviMrC6k>$60$>oZhMafm)nY%Tyv6=s@*0nP)dII!8=% zpwK3OmjVPrTtoyA95%njmz>t4b(90i0&eay(@`a!#R9N}QT7q^IJ}1Tm~+lPZ-yH~ z$!?x<-Piu{~IM9AnBVX+Tdr7Ka#e@j~;)3}A-BIB~K zztfm(eXBiB=Uu@u#rZ5|G}txSPCPC9DdpK>uZU#^7!OO$4~n4tm-dh9w%I=i93q96 z`#7>6V4ks(MAl(ndhkL}Dq|;%jV(qlh}^72Z)>O}+h$f)2E0>Pn{5`zLJoofg)0Ng z;X8Mo_Unb0G9McM=Jr@y;24nmjk2<_{CvL+YG%GWSQqoc#zzIC$Z}3F&Sg2zmZ?5Q zeul|UHOpz7h^z3p#}q#?Mkz7vk+LdYDKJP@rPOA*2}5fa8mFjV!kySfblm?!v>jzE z!sh{Y1*<~W zgmxMbE*^<3Fw5R{6R9Svas?|er;vh#-&6&6QE*6luXawMG19u=*8GC`*?|%oRINO5 zY5T~xm1RchZ8o;FBHRZ5w%sU))APnRO>HV#hOg=74SJmz?JNObkalZ&)kMe867h7c zwnCk0sRc#WHZ~-Xayl+mRki3RjFZ?{&%) zhoMuOrxLAUjvPavO!blVAs{Bx8R2+9mi^X5)myx$%!jxg#gpT-88oMk)Y7|$XcGBx1aFki2{CwK zVMfFY%91WT+tQ172hGW`lhAwUR(W1gSx7$jqaQ4C7 zrZdXF+6eW$|BD?cOW7M1#S)gH%3yBt}NZkER1a(7C*P-!?S2B8;_?!4C3zs`$Fj z6n_F@8@yQ*9Te=i8cL^qZ?qJyc#acK4JT*W`*Q_h%+<|yg%DD$ZSt^MT(?lH)jQZB zfEgiUZ?n!8v$)x4v-d{7h*Etgy|lhYuOo?H$aS#(>MGqE|3GaAkW}oyl*%mgDi5^1 zU4=#~7(H6Yc=BJL{Hd=?m0qvU*MAq~12}J%?(d4uZGUmmTw;_hxXymO6*l2GVH&;+ z5nmeYIMKd1UruMpXJ_q@y#8MP=^1XP)l2I)H}~(kDEfNaeT-(a_9?**Q71Y>z4;Iy zuoSLlP}G4bSzN)Ozrgrdw1KkvjwjlDJ?DRM#g|=}0RYi~0y7uuz)QIUFMAw#nf^u& zUUpKYM##$diH?A`WU2MWL%Efo>jc%a-g?A&(u!~K7K&TX@<0}Ft}LLgOFTLS@5n^5 z72ziOX8uW;JhS)#BnK*dM+48rhWe~r2tW*J#q1`K3;!pUY`_~TXQfZ2Qn3LMXO02# zsoCz4z9-t9nw>i=NEjsjXn#O~gIb1y7cbRnS4mndbL>xWpJ!v$ zjV+-ALN^eH8oRBB6$rX@u_QjHLhDP+33ym?mT>0#BqJJ2AsO1N$+8b(Djmro-yf&m zAM}xsfU3S~CphHN3Can*Oodc`=45RJ_@v;`Q6&Y$!qy9|h1gn>=NsEf)cNW$Eh6JT zVJ(j~Zp@fBDtp1qYUghRk8kht!f-YXf%H_YXY@kc$UA++e1kbdmL^Y%V`-ldC#6Au z+SnSbNkf*avqtE%(*I;uSlLsB;y%V2LbWc!+s|%u$V57@TCm+N=}IO+2bmFdEqyVZune5i`CY73$SNhu0q^YV;pccx5R4zKuU1x-chpy1A%(iK){zb5Wue9 z0TelHorn<1NUhILuf3jQujVAK6MHQJ9zqu{g;SMai0|JCr6ERWd84$};`OdYnwV(y zw@TYCag6v{JW;NJGJpPTJIa_#4)ySIJp=NP)`sB1rW zhI`v)NWjR2`$9Cr2f;m|+XPM*?odE)a4lhlWHT^DY}9@@ zO{7^MaLd^T^w=ARq2$ORgNVMvDtgI|>6F6mW zy2L2Pj!w+FM#pv)8J=arn6xdlK^I)BX?aDg>^5%fTlOR=>(y7t{P15J`N|f)&L=$L z6ZvncJlg8m=M9e}G>@!pahMowlFug3r$Yf^w%_ZA?PS!@W0TNy>1Wg`@^^98xrOkS@?zHB;AZ{r&k`R0b;kB-> z*OIpsgj1oyVCtLD;l;6=JcR-8@~$N#p+POp{0mlAkD^DE;x=N5Zmht(kCf=z_%+l| z=%M03vK|4Ly*gC4Hxz$e^WL*I(3U+_A~J0%vIlTo6NRjCi_t>Fq|w;MthTQU@f|sB z+nBM`lr4<|7Y+<~pX`ctZn4LwrjYA5cmFHJdw%Zy92U7yHi=+HmP^t zzM$Eex^PR|{(%dRr$?J(=`FTDkZf2XnkK3vr#RUoG}|Fa{YSBz3S;k&A-t& zRr7D2UmY6!p61^(Z%DMnzkY$tP>DT2^FFk%7>Q%=4B5BpSv9lqtWgoDuQ$rA+4J(< zohsekyVesO+jdJ2gl~;E{t>4rT*%HGxodMT0a!L!HC%x`ziy3F6^vs5l3iBB!+*E0 z;Iha#Nve^R%zu9nJPA~{+0%|#OrYF8Mb%lf@8w+a)}8rd2?;RE1(afa=pj*XZ_E{m zw{nns@ISjJv>Wm>bQ&(92aSO}kv{C6xav8zCjj7sH$~|1o8lMv)*X3Ms69a}JZDdo ze%6-wceVpur6YvxvmF*6aywk;Y=@5y+788Y^Ru^yAP7!nVRmWj?B@tOfIas<*u-vk zSBwj?Pa(Xv*ykQ*TRiP-3qt9tg1@pWoE@Bdu+D~Xqtjv=J1OMvnt!G?_=8sed-J?V zw9QT|`WG-+CH7EasD!-_Z3^Mz)qj(yoV=03@v?Dx_lf^o`EgV;jzIr?^43B7u~ zxnrMv5j4s^F{}^2DHX^*;am1eEBoYYToxGk}(c(ZitdqrgY zdkR)WXl2(S{=pVnFi;yAO6xl?vqX+SDVRb<|m zIyWe(C=$`c1aQh~B_K6RRv|<5KwPH@K~6lB0bK)xjmAn%Nm?O+cZBKW3;E`64kuIrphqy{GW+WNycC>lHATU_(kk=0Wi#^*}PL$j&t zOVxEh+g*r?3Tp;myN6d3(Hdm_reTw+q|Zaq4otG<(>Z1c&&RQ3VnDMf?TIf;fq_IuJR|wOsdjJP|AuhV- zBTQn`Pr*h*LNruADf}I>0-xsJD?Z&)W)*#>8&gF>S{5c1(P(=_N12eC@*R%8AXTk)d->kQUAB2smc@C~)br~;MLV?X3D{9-jC zs<_S8;(G;Vb|{G(2el;n;yWv%07$N`QRq`iAo8+Y5>hc9ZAE}8GbhOC^w?L*k-;b; zH?%Bt3ZyG9%;O8Jh?s-w5XZuoy?~5K7GZ$ia8o5hk5Rq7Eupt6X-Vd03|z3EM4%R$sr1=gHr(Xl zBY%@;;pi|44Z?oSV|cw!iX~w%LAW8&-y3plEE>M zRMR8cA70VotG)5_(1oR%ey^^pxeg|?Z66Y9>Jhb=E#+aw74+-ubrF-`JOO4e;4!Z9 zJ}LI8;Y6TZB&qmYJwRR2$!9+vQ3Wu1eYKc2500KN1W1MkdWUIz8q@z_& zZf)CN8}-;vJCmj?)k>rYEib7mNW~2dbsjYLc=~|MQb%-zsqi@(wJMa0kw^V22nakkva#KwTEg z9i)KzW;M=;|K(__3|4n%N(1TjU%AgaS?L^v{qvI(`1Ir))QO+x7o2_jeP`e1wn@@% z4`H@_ypjo=h!w5mQ+ppdjJEd`u|fJM)>)QO2=UYKbj*QOZ66jcV50Ky#DS>bN%bIy zLHccj&oQ$&K(_4<;n=u;sxhc-|5xQ_Zgen^WDf|%v+U%c`JDFT)R9haCB)OAm+-L2 zK{gGjfjymEui-h_w!3EUs>9-iBSAt?`YZbd{qCoc6d=&s5B7^-O?UQlrj3q0grp`y7jGe-F4binV<&gfPLH zqTuhBBb*hTC(OyeRO=zI-Z2i=I~0_%fsJdzspc%Q6#}k9^7z-1)sUxCnZ_k$E3kek zu&;mgVNC0S{%yCEtq@5Q;u5jK9kVXBs~G)*_o0o!;;l06TPSn$OTn6#Lx~v$0BR_4 zWeF^Q7cM6!D_?PY9AVdHGE^N(%X$ea)5d7F_X!S1iU4VozXYSzM&# ztf}V8aN?pJ;Y2e>?bxlTD#MAEorsEY8VwPiIa#DlidS}DP*gxv0j@T3Fxgnh8>>L!1^%O^csb|kW`19TwhEkN%xX|du z142oN#?--|{Tn4Ps(T)~DAqj>Rim`nnjgrP{6Kc7fh1&x{*8f@e&#?_$X1<`+9Ly5 z7f$HqqK;E*f)A_Ws6QlIrJ|VZjPpR!S2}i^AA>XP?Xq6s#N{4YGe^li!H_}lEWoO`t+qH4w}89GuEL?=bKM+7btS-MeCk!QAeOsn77=%{AH=ha7Y&(rAabZGW|KsxuugK*u`M@rfKE(**L= z#HD%RkecTxXgtK&j?|3Hr^NUkwrx_9MS|FI#@M1d+XAV2ptJGNhtrBuCN=Yv%&f!k ztm2Z2&ig7Pb{Z`2+>Iapt2e4}l0fe?0HuxPAj;DXWMZuF1~hC2$I1p{OIzwB*) zS=d6sZ3ivMXQ50rx7wuc=uc?J+x}y!Sb=Vx?o0HnoMuny4wS@aY`PFtjRbjoUP~6U z{YUo*SPhb^kGYCA_3j;rIESwU9H{3}9-sqF3X{52OB7^QAN&F__fq?fzGZEg8~8{OMEJtyx(?K~*8Qiu z9t59$wdN?`;s&_#zReX%t=|!A$P1)X=g5ywOkhdE*sq{8Prp)Hk$r0aS3GqNk_u8t zbIk_8VOi_qPbYE#K%rjg`SSB}KX1HoWM+s9n3o%*&8hO!AU|TU>22S^+gwt>?9zNs zNEf zygFL9kDn9`!`_H_9jTYEuNQrrF7Xi><{yry6`h(Q7s)mc*?K^GLTC__G#1*q&5EHO z=4$AyW6!{wc{XL?|ur#bq$vwO)YS{f|e^MX=pvzPa!PXFe8vQKlyEB3u8@(!i6 zK%d`SuEIcL_SrByU!#`%*gpR7HAYMAzw(v#htYYRUlAaDkk0P}R0i6l)xAo;k*Ey% zJV2Kg>lQ23k*F}d?VU`A_S7JLGk=nqap^|YEk{gnP2cn6(tf0`alYe`bskkIt)5cp z^{v94D;e3PpE0s??7wQsjntx~V8*wyZo zKH8m=+>^d~lxswf@)d3HKaQ+N={r(Puz?D2`p22YpvJy~wfh6cOPR~0fXfkz9=Fdh zj}08?LfLzmqgQ}85Z@C$C*r@Z9O?>5Ua-SQc;TLdG>=Nc=Z*gss{|^CQ@@`pie~QP zucNDpVfVsLGo)DtR%Bc=yr8mM19$&iRdnjVX_F+WtM{)!WX$j?Vu zBc|u^S@wC%FJMn&9Ws-T6k?n-Gf@aPZWoaYf%AS=FRk%8`iaYs+t2sKbLG(eM4nz~ zwB-BQeAs?|{i1w7FOhy?_)PaCc#_j!>>Uo#(}QeOdUac{Ns^*G9jw1}QZ%H;mbtT| z{zlB1J;W5kZ0qk|%D1{}N0yWjnM*)edkYH*rhz@7o>1k`7`Gk~uHz(LVpPBh zk?vI!7*`0qQd>)?_WMPZtfyqbSybdRYPGgSlM154#6D+usN-mCxb^HN z^ktdrfpzJgH{Qrqrj8#C$%ET3k8zarPV7AB4x{aci$l!*qBs5wgeoQ(UWNkkxwk;V zn)AqM2-%yc0|pd?vuy%!4oO*iXeFGhC&5=#Mg4R&K@w!$e4qV+5ks*4E0dzZgBDW! zTEs+nlvOh=zmSz^`B})j+=YY^&o3mde>OsBqPo!S!>l6-S5LHFnkv<)b^K$*yb-%p zyhV(vMM6o;VmaZxh^mQ8+=XbA&<2Igw_6$Sz_9;aZ(Kb41@VDzS=5J}Mdje)oNs1P z2}&9NDGxE*kjOUY$tz)?54XIX&ho~mSE|(wsMWoM8hysJVEyb#(OMDz z*9C=q6`R~yq8|~GR#J&9asy*loUNFlb3|DL+rB2#%zDdkym2vz&U`E@$Wi5vfaRBQ z$nvAYm*tmH0Kwk)4=Bk9qQe-qF>r4&%OB^F>PdwR4HJG+a514icN04>Fh^iuF1W^t z1uW?@6pzuzFGAGQ#AriPCgFhy3gj^^^gdas=8$kfSn4)=m)io$C*r%{s2%VDaMTXZ zSnr|bv@4a`;Tcgo*aNNK@`%ehC>$UyO6~uH(Yu00$AA*J6uodPr<=t0pl(|*$vT37 zbfh>sMBG$}xCns;7apq}=+xTq?>Q=B%Kn9^D#&dOq$irEgD+sA!uTK%v#SAHHQ7xa z<#y5U3zMt**nv5Dcf=s&`Gy-nCIIe#0k~2};)apeByOk-blh-^p18r6xS^WM35gqO z1NF7hLCSMKapOq)86r|z!4_p9RmY-vx|$`|1IDBt>r@`Af}s7v#T*VT9`Evj*3qhT z^lcUVR385Rg!$u7PL`U`$*lu^`qI7^dKG|FKwJc4Uo)-ZUz@y z34=mR!)8x!0KdGG8t_^#SlKAA+V})>al@G)G(tn&=SfH)^sVr)$V&-I2d*m(R zkJY(Nsfd+LwtO4?^tUV3x4&?{z0djfAM$NH_boh5CE+Z~`|1VJq1U~r>=Ai8HfOW#TkuT+p} z&{8Fs)b9zUTC@9J4iC&lo0?G{l`UDD~t=n)wXp zUl&u-I}OZC4hR(?4{#h;z`W?=yVbl94#>ZM!3p}-cay8KYh9=TBxC8N{`Zy)&3qkW zh9v{2S>OKFr>AuM{`j`Ad9+cR`gWF&;JMc<+m(g9Lf9}^AKaa@>-84s<9qbZ~EDqcUwAilWqUSZ4 zH?J%qTxXLRSyqyMtyE9Hcx*TlDR(Qj*LX=5zbDg~$Iz-%f0S4`~^Xvi5heV*OpAgi*m|<)BUw`!d%F&HOWg z;Df{mB+7pqeQ{VCXpK2?xc3qqvJjb77y>%>IhB2JXX@8gcS>@oWpB?NEH#v#^{{KT z(mrbt)VVo4k{a9e_JM4&iS-b#B+MX`TiaGMTp}9Pv6z>}I!6Z+e-hXFQ2RCru<2$x zk~-fL^6JlOsjE&5`nQJ610nOxkoh;w+q5cJvn5j8js1ZqNL^Dov4p5xgbg;|&e3Yt zMN(h#@T9iYzttO`gjAY+JJ88Jgh;};DP*oC?F>1CN#bV4?~!~K`nBB!>wn}I8_Qw@ z#p%Qy>Twv%AM$UGZWr=zku2z0u_vUX^XDILw#!8@5pS0Yf~I;-!l__FUD-#`X?Iw5 znfp4_{<<+onx`A5YSyL@??7R%0`{{_%YFmeB3 zS{>BeYHr4s7d5Ab+^=QV;4TqNRp~MD1FqdZN9P zU+Zaw2hWrk+q{lnm71F)I^=^T%H@HQ%@~n{X zOVg|Lpf}W|c|+6_tO_zQYDULVm1eFoCoY5MIX>in&f6~e8bYa?$lv(K_P^60J4M>f z*bGuLUk;h;Wiacw3xUx0y-0$A`3B;<&CxMNXh4NEtyqE-YVO9 zweEfBSyF#BnQ_$ymr?*NV^iX8r&Uw6s;6zApO{$UNU7U*;~aB=v@vQ&m~Vpq*FxsoLGKhBo|xbT^=fJlfif}n zJq*%(2y6h-^ibR9LYl`d?1wae%Q7DlX^x_w3TX}#Tq(OvGA{m$i#j>Cs7FMY|E2I# z?lcsHc_&X?l$N=b>wgb<`WE!=4C5wtM$5VRol*aEerE{sd^d+Yhe)5)v7tKm&m&Lq zjO~j&6=a6Yw-oaHjUaVFo;j2n@dh*C$+rYg_M%iFY!nLRc=yIJxD}MCnMONFIb5Xq zk)XpvAkEi!-itKXN~vC?Ij0wCw)91s!}}o3(RrjPDAVi)bsmg1M;{4o-qnjX+y9r) zrh_rhrnX*W`DbtY>q4*m6UcHX{+YS+^ec-1iTcJoNSv@L4-!EVIR%3dz4d}NNg68i zSo4NL0l@_CyzM7C=(15zfkK%z-Pici(CA;L#+7O`;0SAG%hT*td~pbxT^>6$`!hl^cM@Lf38`F>>vAFFx)chUzmZ#r;$T8u**Mj# zqbql!l7sfSn?G*H9(74J`ONy}`24qO%O>fXW0W1MWRCjq-YyA`wAQ=4*dh2kQC9>Z_+?((A7BFsf!3&~ zv#Q*ArtRCAJJ7H(=RKj1oJ7B7`q2{^9VuL=tWuPO@|P=yRQ9Gv@X?$$m)>@+aBYL#_|&mbnoeuJ&4oAajA4rE+m7W|}%xcL`>2yHv>P7A{<1{zO57xjCHPRixl3p}^5P z>3H}VPNNzKa2GlhMZYcw@||*_x*zOM(SLULGmVO(|HS1g98x~s$+mQ;SEzU_FH{f1 zlJf-WeHNKlT>}0SfscTKkCC)bicig4Z%#~d7WsnyEe;2bnoLdGap_D6(h4>(7V*kZ^LG%H813+X^cG!|OOaaq!Yf{T6#qV>4^ZgftR4+Mfa zI%*Eba@I_) z+zpeb`PVqok^eo6ni!jJL>**Gw;pV$y`?x<<598_5v12@`*t}}N4-ly1n^Q|t_mi8 zA$(w8L5f1DBY5~Jlu9OJS#RI-M?|S|ASK%6bh7UHmO`oaj>Dl;DceV)Qgof5RQXg% zC`6WRssj!-uhCg25x2Mq3>+){&jGdKJQp1+6QaiTaN$#O9%cj~0+YtOa+vKtB&Jon zq%Wp*wv9;CHY!X@U`oX^tJ7H;mD5=Yxov4#(p;)m?T$mi)2I5mg`^g_Z-K)v0f+8c z4$_l~wb;kVX#OHI@DsU5QSHbe)S%4gWK&y7YQfy$)2EHJM?W&cI=IdBk;6d1A65>d zM*dk-;f=3lR&&z7~- zrXLY=*Fl*7?@U<^^Y;q(GJoZ9C@;C`ltShUuHfcVh+n>Ias@ZiCAo$N(ae!?@IFbj z`iO3%tgs^#Asd#6ZsbW=$!ts|Sd9Kia&WaM%pdBD`K6#EuaQO^d5xw7Hg2T|Y~;$R z#=#;Yu=!IT+%F^Phx^~j<9??fyNVRVxTh+CiX8Z|E*PB$4F~-{NKG#K7nvF8e;@8d zVE%fAT3qzsZ3MJbSWrR5U;3c_XjFlHv<;KS_@OrDE0y`?A~iDx{nkU zy?hOXs+PQ0cpBkwi1_*IU6{VTm}hC=XGU0m&OZ~TMz|R^2)QE!3#4Y`nU{OyMCoN- zUM(%&!pI#i$ERTkk}s(cNN-7DbIkN(^Qg3Dm9~;GWc^#mWP_=TemlMgyO&z;ZNd_p z`-DwGR@4=PwSu`P)P2p`)Z*_bVp~VcmfdW2?b`z7rQ}53C+<+(fKo2QPobt*Rg?Db zfds17ygx{j0wCBCsha5h!747JWxp#FhcztHH^sMk+n=X5k{4(zfBmZ=9fRJcElRg7 zs**+_;^OVRg9Xzyb6XJdre3R&ZB~S5;k>J0Pv#E_n)j($L4{<#oN7YIu|^&`{gQ{9 zLgUxVC+3FW_*b;;89KRLq8w(kc>WCD5KK5lr3kUc2D<09{X=Q4QO#H|$q;=Wl!CBTiOQJpMli|XJ zvgLv`-E6_>LGyVy!{Mkw2cal4w`6`S_`K$okpJz4&GSppov|5bFzl_yMASoqGR^f3 zaWyA|;V9n9o9LVJy25?!zBJc+zfZJUSqoT6R-NqVcprH~J<5e%i@kQ7EI%tM@S)YL z&cVQ&*-hr!UKqgF_X&qh&4U{{FM)uN{{?{HX$1sV_XmR4^FZ)4BNiaQHUJ>-HtoQ! zp;7Zc?~S+dL?j+KdH?~=$~Us|J@c+`SN`t&%0GL^l@E^JpqU$4^Wb;_>1@!pznWk7 z^{o37vJwZc`;Td>QI;f_q|cwR>aVAo3%#KoiJ!>Y+T)L~-ml?Rz-gy&S}G->HbX%zEHC%{-QBL`4jx1 zP*nL?@wKOM&r7}>X6S%%5K)La_|^iVO5MXIfV={34$@4wRCnjCD z4&|)*p=OS0OqI`#Y&}Cyd&)2W=z!fKHqCv&^o3wGQKms ztdree5cYR3tne-`ieWaG-c^Fz6)HErVYjfWhKq~i@LY8HchFDEtKFewOeO)PIJ?4U z&3sA5Be8L@cvDdn&`t86@T%n(OMnDjkzZKLeMr(tW+^%Z^MPeg%VTWg%px}T-}JkGoh_TGonNl!=wBL{`988=}LFbcvVLyfIw&AwrN3=8T@t$X7K4Ze4*lv`rtKUVW97O4-K`s8!HbX zqF>B)OLdh>U4(c+iM6I&;NY3WH(7~{PdB^T*Cj6SC>dW?HLpwa{`7(}JTxRl5|mhY zHgO?4Bg|Jd^AeuDBS;qT5WW-s?KekcRB)>j#11HYB~r5^Qu8vFXy0}FYZht^C4-~G zwXEa_AmCvMpWTwNj|$Hw_Dr{4v%w7BR?-msPRaaYlsT}XQE7T5Eh+uTV?HWoYu#SL z?E&)&{(7HS#WF6UgfZ|@`09chl6gbQ1#4bN++8)7=Rw&C7?=jYsUj!Yuzk0Ahx5kV zwtc6$b^B|d%iS!hMJkVUrsEYh9J{qC z)mS3}4YgLXQESD}pkDLuUpT=0#*-M5Xy)HiQ_vONR+5@>fHB-wBD7vLx}OPjj^MWN z{)Dg>3Z^=|-6D&^_MMTzonX&cws_tsUi(s0K9Tv^ZqZ-6RJMFI(WT7J@V*bs^{9oz z{-?1;Qz%J+4C!dir%`Gnv}cEHTo{x+Q%g64=G|(_INo`^CA{wiH=nfFvF-GQ2d^gN z-hyESjSLrWL+Q@P3tj_Z$hc8aqY7uDCGspx_{<2iIfobK_Atw@nSbq#kEs!Z+xw81 zOThC2BS2Xsb?j|22+hB0KDi;%TFokgILe2KX6%UV!svRpA%YnC)AKJ7Qi5ihBRrO7 zAXvh&)vBF4S<%+iQHvsx-A~FxoWSjg4vkRbfsMJ8Xt=Rx4e4 zsxg^Po+T#HW8_EW6*viZG2ic$J zwJC9HT>Ii&egtK_kA(-_z=cRFUr=f79w*uqu`{)vdq9OY%KU;GJ$7StLG&nV1Fx|g zcS>a1)EiD)jD4|z1=*mX9;&BWeAc)>i_=QAKVt6jL)FtL- zav!)sCz>l!%@+|)+p!q;;CZ0R_&4XRt@joarP92-7WQz2rO^}fK%&yBrJJ~(7fD~x`S)&-UjAH69RZ)2 zVhcYj7spD467Gc|?{Wx}=dJ6`DRA;jl38+*ag{qq<#cWx$xC|S*}J^C!QXA1^O=*F zgLc=u0FScX8Fmv~D5!Q;tG6$7#0*cAOe0?t1fd*kTV*5zS&?M-8RSTpm5RycOQ!8_ z<$7sU_jXVwb2PoIr_sR{{hX6QXn$3$Av9&$cC7X`~8aW>q z)oImdS!&0|UpcbKKC4m7EwDD=h!rZ7@^efT6{wGP0GTUf_+_3VDwiT>NHr(QI5y^1me`Vm&1O zyH-64d*k98#eVG4Tzh=2vO~ihU-Koj>JDu}LaSBjk;xB;Z;-0{azXRt2s}|$F*`~Q zd-9-?&4@~TJmY{2S0nKv^%4~zG5Ta*n$c#XgLqhGl&o!#mE=uL%iqF{*8K(5YuItjO zVQLEOKT-vhc7Z*bi-B@c&INtM=Mv12MQ9f@}iB|GK}$L&T^= zvri{!yuC3tdnD)IY#*7AtNB+;;+bo@nh~+BZ9ay76L1hwfB;{kau5MyZS{bOoEdHP z5pVq0>SDS#ez&@qV|=}>{_DoYWHu$pX1!sMuk<} zv2D^fqUE@9k)O0$=FWMMnC9VL*46z+R;JH4k`C4R##sfa+xbRj+x-TPvTw5raHJOz z)<8>^w|!U_J-TgC(HW|)V%n1>Q;oEJcviHWR}*n{iFFc_8N2Ia+jh@v`%qG&uIQ5% zb<6z05)7J;W#V)GPC4AbAuv(-0Eu7P39`Y&_3Y5zKX?d9m=`%|H>-3U(=V1SFYY;4 zVW+)11g8%I>~uVIWNmFtl1Y zsTa2GztYf>Q?rw{E*OwpmE4-_K9D9$1zv_2Boy<9faQ_OiT$0`H1C7yPzR3>teUhn zy3IbxyL>&Av=ryd;OI{m@5Ehg^hDgcIR;g4z3pFPyUC3>Iw{wj&v5?{IkpV>6g@S? z$I++PJg576bnldBCA9xYv02@>WA7*eIfhe1>UID6==;)E4paBRlDhJFE!Pusm4B|! zyPVV}cyHp03vu=Q+Q8`r)||8Y9)V-&r1J9n)Deh4^U{c%#cZ~jD<;cCb4J%nl2MXJ z6h>wTJl4HP)^T2w)3;utPhXQ@1~|_3*nlF(8!ao{>^(~yS|#tw%9*^ZO)cR}oFHE$ zHS>w=O_zo}>OaK^(hd-ONuXhw5TS~Cy7=$zVd7JViI8qZIKRZKlIUhV;kSqI&vOkw z#5(ZjX36P8E{^P6=4esn3>B|4HD?C`jB)@HTlqoDuz#*2YJ3Q-5<&p7C5RM;u!O8d zbhLFFLT|T!^M4}UAp5!?3-MZfRqk}035G~$O3Xa!8;^-X6wjC{Ggoi@_bq&oD)B3s z3c$}yR(s2Xt2?CrSp-$UkUz8&XT@9Y4R+k}qe$YG|Kj=qu7Aznhq)(=`Yn5`-+h3xTlrwq zQ+k$YKyIp$k-Yo;^&H_!gJcb&^_! ziwf&ffc`N7d`SJ!eNY~&?MAVfa`qBI$ z)@b>D9N*hdu8?XRTC^{`Cx&KzD7j1e(82qy(^SzNaw=df*PX36^JV#Vb9OsV$`n(O zi>qJjz@~FPkw;`qsG#6ZGLsZzs^SFgts8qgEs&Xnx6^nl@lw@cv10c8>vuwZ6$?Aq zoe|`m=$D9ez#M)>|bvTZKx-=M646Kc~um@ zYXYbe50-qNgd@)yNU^e|(IJsUz>}Ktab|$jAnN1ze)-(I&Vyf}$DCUdJ3xN$E%Gcz zY~Iv&g^;v8g1mV^)$5}lEUQly6m**N@spH&wX7aBf2XlpxxDbN@`y4LXxL76^qq(?EF390n9+QhQE^g&Q7OP?@Qd9Gyauqs1`&?pvxx@|GEQ8Nh%$9eX zvLxiZ&Lf*n(bwd{cc!3@;@8_&+y^JHQ!4s|1XX}kfRhBhO1q7m1D+Y7+UelmeMn}h1%AkfVC~x>( z4;KcK+D(?Mtq9Qyto2O*RNFb1{eU}f`|o+A<=oP4FFli|U_D(o&$;Vn0PeZitCLeL zftvLI%Jy!(u8R=NqbY;Wqj8L0w_jvbo7LRReo=vw<6ctJT~^gkY5s@A)tZ;lf>o zzy+YS%_xf`iX^~y;EZ?L{NtkH^cYw^F5ta;4d;e^b~sxm`@Jk#+`-A$<$bb_$ISbE z)6_^u9Q2--f%snKvKoCj(00IQkjvu0!07!_$-uQ!vIRDxzYT#i_O|)qnYPi1Howm( zZSxO|lEP-S{hoUMD9P(EvZsM6EM0I%XYT;uP~@%T=ew#s_1BGb8y?@ z!f{R|1M9Z&gfXn$YT4yD+0*!dwPjZb-I6V1bG@A`<|q$RDC%kS8UDtB#@Wn8jT9_( z2RhhVF|~J$1XArY4KiwCJzOiKl~XrFx74kIIZMw8aB(8R8cI;6iLwHOz}}J+Z!8DA zgTN+gmn(Aoh*CyT+i?gk@lRx%&6CrsQL^6(aheYA+k>KNzKx2gxYhemcerMK*uQSU zi=q^A#7UQN*q~h?-O(Lhfj9SX$jR%(FHa&ts5r;9DGeL5_Yf^EINCag;!mlwvEDlQ z9)cE-1C3bgEmV>kJMnwCajhuf30gq`@%hin<(c>2N3i>>o*4Te7jjUR2I|ix<*&q} zm%zriAXI4$2w~xq!zOWyF2mSlO$BZv<}Cq7A_hgGqtmeVitx)hkcL~o8Ag<&#X}PH z*wXwIK)4`P(j&3wq+VQ@a@AU;RHelZ6h=pyp@2kE7@I{^n<)JUzczsfXVlXy7-56; zS~L4?4;zz1xV7H;1`Y67-deM`HV+^SGbv773dHaYlQ{TN0pdEQ^@jTB-xEWBPK`{h z)Rw6D5jWP!eX1BMRe&I`P$6HOlEXoWaIDK? zy$?!_6#v71P{zRdB}00dXCMXh1FpAmEfdK$p%O_k#Kw{B;>;@9a##h#eSj1++Qz$F zpMNkYq--8J6Epz%qq*PqN-vg3ovEdeH()ZhzBK)Hw;gx#WRH;NOl80v`XAqAnM$qy zBYQhWQnSAA&UN0I2fa%Tt>A?vo0zk0xLkM3wdp*+!nrQwxzw16aG!a_!RM;mDsQ)9 zNSy4F<{;}>zasxOTaU2y$Raq{x{4?LmOMw}!S^~9^znTTgvn0HXM?Qmlc-2?c$(x_ z-WDCGOMc3L+RtbHAEOAF(OQ(a!2bcgZW30Na;_^Tz&7!VO-@Zvt z07ILy727#evd(wq)cf@O{177`kGeq&F}xi#+2qmd)xb$ zw>Cn40ny~i9u+*6IaaOX<5fbY5)sL3ZD?RgAm>om3$!j`b$i3m9^qWp%|h9m`*pKY z_)Vc5GqcO93ATfg1-+$LYXwn|!fq`KtEa*q-M=UL9yN}lMrxxPDy!)Zn^V}e<+wmo zAyu`eG*6f>%!fw>Y>QW%*Gf!=B0*5qy3&=t6t^Hn02i1#qwA$Si$>_Bd^p-%&(}DZ1w1#njLlJg}w<{HKO~MXq(O zl{^ImDJ2DeTJIt}b|2>CtQF0uSltoSy{OhBH`N=g!FLFf++aOLq7M1{Ah0HXm$o{8 z_aRS)$=BQM0!zU1PG`;*G7Ewp2CWp5 z)LOyq-qJI_Ra8)9oa`h={f|c!SzfkMMB2~flA~hYhh3gtWL?&g&8`$7wzcJbhdyv+ zDv|>S%1(G5L_g6n9iy!#gx@)_&}m5#3pr~_#?(knx;a_m(K;k=D$K$F9px(2183;- zeCd%d$I*)Y^v299TmYY+{;$_QK(A4GDZjoiExk6zl8<##$?)ivkYJLa^GmdFf$qGF zz7~G0&HXR@X!d~fU#evI2?X*fIH!l7h0JlG?x%uBM9&(IS)K*n3sOVM^?h5y@O5EpKvg)>&EI`4svQMM;4@>hj$wbe@d&rbY@57e`T zcugkpzDCZBy;|a=P9*>{C!(7<4?5K>eeinxy__P@(BPBH9p1)i^U8zTJpNE^dYBp7 zTy}7qp2M|ycD_wdu1y8Ej%;`KepzdFK~XNG*(!6h!z+D(CPao)V3%629*4-`k&;ER zosf;}h}?elDeE$+4^#@x>EhP8%purwr*+kwYN#5wX&aPmUka;LRLOyMU z^s^&0&TJZ@n|IZ7qbQqH$VV}y$`cZ&nlr@5;4CWxWgyl*8YA*9zw-ZK?Ooubs;l5m0&2`?ESSTrKhV4HM6XJAIoK%%jVqN36UQ(LR0WJXXC2Pe@?j#F)~w)WbG zx3yPW+pD#fm#;j4goiv;g7^q(l@c#O(|m9chwmPnrAq!yGNm14ov%# z9QBO$L{92rR=IbWy-&y?GBTpOPwo*>?`|IlF!!)vG1tD>2oOGP*~c;Ohmqb9%ZIGX zV^FQDnqr!sud!*;|0%L@u8I>wQXO;i#?l0G?b1EmhfS?A#8(n|W))3Yu^6G#sRk5nC?Q%A zqHo35hyjEFIZR!FM=e{-`{kX37#rS6^Iv1-7n4Jkzc$vN|77wh*~6;X!vb>G(#H}_ zjkS8Jh~$U+%FYX&A+*|#{@`3|J6SCI)Qz%F!D+8loOYG>I*<)63|8ITy+nEovtCjD zv|x9rK*?paHA8Bm+0V%`h(E@E+8QFNVAO8j%;8fd)8M5_42SOA>2q&eHQnouuRO~L zT+77mq^^BL$8 zr051wajNptLsf~P=OzpCsfQ|to6m6;i`5Na01`W_Js}H_0>i9{BQT@9pAN{IEiyEjx~KToQ%xVurgy&*Vpb-8 zpkopzB6Cur+-93F6cBz8l%<)vA7-qoA4^)2_L0c!w}&U7PLvcm5f9s0@oF) zTPuXoEvtxG{jjv~UPT9iDUQgTN)@?I7OZbk3uANyd_m&lL*0>(~5kxfd z&rZW}+Q`*X5t^`c^t3s++! zs{+N_=rog~7q`pIVDdUoW+yakV0K!VoxPz16;~yOowQb&BJm4WQv~h)>cDoA*gr6E zc+kL}m4e4j>Hl$Hq1u5#F}-SB8sk!9O3iO5YhZp`nBTpjLti&8Lf!YzOz@Oc=LZ24 z1{r|rijQN;6^%QIT+BXI$;Ffi$cQ$`(4lLm4>|>&!bZPi37a}tJH!n4a&aZnGZ*Cs zZ|YxH|6|`!^((=HZotW%f>h94l|?GGyw2CbwWQ8TC+X)Rs~)SL%3z=?nL!N?pHRcs z^<$h>UK5Hs)a(Prr2VT;D6Q6olIU0I(Mpu>DB!JQze_Q?>dfx|NbrCh_fy}y`0XTL zueui{>faUhzXH&jQT&nG8NHN@c~2%}s8XMkw>w#F@)mvI3654)dV@eeHgC3ii!GR1 z6KxQUn!NJZBFimSS*X579(`1u$aX*~;ZAqaFFLV+Rx1n5pIG#>O09F5%`u8sVk>UmPY*-Ekw8FX<6>M*Z)p6H|+9tnKCxr9Okg zMqsIV?ZC@IvufaFu{mepiVke2cz~w!ac#uTsAA%X^pTZOCTq80=BvnzhQ3I~C&F{|l zS>|b@B!{Reg5!<6=Shc)ze*);v-Uo!87-qs^Q!}WO%}|24vseO9!ePUC@I1EO;u_qop{f50vNLVku1V^oF<}# zXKBY82QqRxa(%0${+3kpk6eO!qC6v6@CS5^M&5_lF`Vvn*NRWxwT$DtHch&AhLQI- z%1YNN2X*accm=vP%<5Wbm~>(^ok--!lcqX}9gY&bC=CZpgo=_4=OC-@OUj#{j%2|) zzC-nLf38>?7nm~@_rfuT+Bwl=B;sl5n{_@h@+L`sAe?LFA`6M+jo>{kjZ5FbtQ(F4 zE2P)2>*ZMhZndoDddJ(4%aGG-SNQSObBE*c?|!w+lUA8$rvvvvXBFKTv0g=dg^)L! zGxW)9@wmo`FhaQkN-$y}@{^lGL)0mTbcQ4U!g@VElj&C{1YGji`9j3w!=*h$YR!yn z&B7%?*278UVriZCQv52`QH+T9SOWPPCJ9?B3<3^e^#`~YqP-cE9JC+HGHX=*LkcZ7>WbQ_vl6WWk)S*k#wK;7 z#($bGPpj5;_!^cF=xC9!q(076k5iS=0S-HT4c~}=kGBmssEj+!_9tjXPTT=f8}BP> zBE5V6m_6ZlI>RkX8}fY(i*4~~d>gobL$4nqTJ33Qo_&qas}e6r*j+*-dpDFw`Hp~` zEcpk_1t9$Ex=SM7c18|!P8bpz<7*IOK86>V+$s(pV?SK|&pzr%Q1Q8Zslb&Y*@k|? z;cqx2F;b3L3kU85ZA3gayrTYe?Vd)e_G@cf0;$Q=uqIh`4)w}iSO}!3eXm*LwYAhl zddqLM)!14ZvXk4W8W$aVjW^Qt#cexkFnd|yJFyH70k=-qaoWKc_o64>=GxVU5nMBu z=nA-Z1hLSaSsuy`w|u){L}F-PQv-o*`@Bio9>v+$+sNj*(_9U|jt0bniNEZOfadGt z!p<>GMV!HC z-OirN-u>ev8*#de>&>52jUfDm;(-*ir!KqxK}#X)85L{5-5WxHcy!6ibM$pN`~I+W z*06nlkb*Wmqdv!K@vCKTYWek`g2Pr8>+8N>WBs#Q%sP3gZc2Hxwl-+KhVN&3jW>K{ zONwr^j+0fX!`FbMhJ)8lNNQ>^sJo+#Ls-fvf%TQTk1r6tDL+7{c-eWX1hKyaDW9ca~h%7maH7RZbE(HQg4sh{JBG-gH5pQ%D5~G zg!S2)$S{lyf~qP5!G}Ei1$(f-d=}0zA=0rX z%Em;=EXC%7;Cs#ICMT1dZ6#z*WCyA*vg@{v`sAvWdhDjmuN098-!Gd{O2`CG9ZXOg za-?_lTW*`J`WJE<%{?9Nwma>{(Vn+qcE+ML4bMC$11pT`ofWZ*GIiU_m|K`LsjQ;9 zP0@X+{Jw7}K$fKdnHJT{@~9G#nwV8s1{Q?b6D|{J+6imkt7LR6Z@C-}4W?o8(`7Ar zix42W1LNfwAj{u$xngyYT?Borp}8`vnw zuE_m1aalhA#m5B#?k?@uobJC^>33UI-8mC~q})dASXwbz>1mkLm!=iRwMbKnHXw_F zP`EP912BvU&M6jM;qw4p!L=(TNmBGBhR|Gy3o5=7h|6=LCOu*4LtFrx$R?2a{0-CX zCOQv~b;)J$OUiWn!b)Se+$0ofvR9(Wckd!THz)da!oqPXz;eojo78hv0Eyl{v&$xJ6S!uBL}m1=|Ku( zYvExOf8WCOc+*QMBO-5jCX&`}My36-i6vPlvGJRHwq{ZE+YnRjxma?WE8kOOrY;9g z#FCi}TEf|ct4g15P9Dd9aitLl`9L-?mq5B%Xf{H)(vr(tZxN~-e=Acg7@C6biv9ar z6}Z|84^~N-n%yaOFmlWr%!|!=F4CKItHXa@H;&nBSh~+EHqO)C<_w)>&f*+HC0swo zZvQfD=Y=fZ@H?5l1~fn%97jNf;c$UCRdC_ls#AEhxwUW!#nc?+*u9CTC=n@ z%~XJ5=AlbC4>nlOJ#Z5$O|jSR*C&Zg;ul~sm|)BoplNmgK_k`)HuE;n8=ng_ zO|!10XyP=>Tsh7uLeg(1y^{2#W9<((4$m3+rzQ?Jq3W*H^G8aMeiHAT*JPbJ zBkHUP5uK3|vAk}~9@%UTeIhf6`^{E!8A`0K)zu^Kf?$fT8V+lm!y=RUvrHYft25iZ zju;xQR*xl*h|PKmsA1q3CNWsNUQTT0BF?Z6hzU_F?*}BCZ9g-i zc(W^@rVq#{V|qifVAYdyn(Ibd8Z7#rR;PF@u=_@JVv@6mUR|iimfBy+aPe$=tGrCB zD1-Iia0xMPW_8`nJYF&@&JR22bz&(IaVLXZWasV+Uf4Z^i*LJ!II&kZa z8#jqA6fXM?(TB=jp>RprRfry_Q0=jphl({vHB>#%M}yc-MY!QdHTW(Kj%&s>YASn3 z78ZD!w+;=@FAK*-N3wZN*=lv8-D%>zI}FeM^{kU*B6mGxESYeuM;3*aqw~)I1!Fc` zKRJVB-ikp8M(BG;73UKMi#0+!4jTjw$O;Xic2DA*$WdG9Odg$y$&$d8PE#R$;GyH= z8hS{foPc2;(&j0YWX^F|X%nqNt3O}mQUZ>&J-$q{+9ws#tZ_r7>lYF-ZP}$xdw6_!|XGxmcoX2@W$Ol?*Ua*XqR(B{#j>}>@1`VY>DYo@Y1RJW zsT}{=8U))!zu)A;0@u@UXY*!%3-ThKntaCA&HiQx5(Zu+O0Me{gMkmEVzgZ%0%Zgu zLfj=}+GU;tO#_%C%lw^U_Tz8w=bsaW+FHNqoxC}GXJ>|dt85?ewdxW!K*I)CCwl1%&Q33+ zZRY!1)wYV&NM7U?rI%Kw#`r@$0YCx3ZdB9%AXL$bs_FE48L(@<+-v#Dp=rZ{y&^U% z>z=-(@^3IJjKlMd`+Az*v!NL(*oq`YZYLyC2%Lysx*OxOMocHS>RdKaHwDOx%Gt>& zIzEP%N_VqX_Xoyqgc$y>dtB5a>cVVHR)e=EYAcQBdoq;plg$}>3MZyZ1)KjU_#m>; zkPAPL|73sfy)tyFI-Za%S@1_I1MBPnUA>Y+40Q^nX$#z2r`kxIE~b*H7XbyQX+(v& z)FoL_DQ>a{+#hK5Xt+^D7kr{Ct2KHyFCb>tW4%C+FY$paM=}8udI$#>6@ve$N9iS}%oC_IW!4rJv8g_j#3B4hza4yZ^)>t2rGr zD&@a`-NsX~E~#_tMM|tC^ol{rg461bL_5D68oAE`@Y^0my3!R=PYCKMsJkfrFAIl^#u1;b~ zA#tT@zf;ZleMOJbJD>P))D?w!gmV{L0gtSb`z*PPm=4HGKk?_efviGB32LN>C|HD$ zhBwp47lUB+JE>4|QK2y8QlHg=mA~NO6l@m{k~{TFg^t_vEhPMy@y(D?iDaVSEE!m# zR3CGk#kUw-5NGcG%>SWWjfUhHMUOt;u~DSA_Cn`n^wQ-kUd?suOw21o>V^E-w~W zn%01%bri&_MBY1oKKcc@RhzvgAFc%lu4F;aqqMEKQMUP*qg}GVSa&E@h%&_;;1f#J zm658~=1vT79(c~i0O(?od;B+0jRbGes}YDfCiAQtb^$jI3JVN$e`TkT0-k&99vUMrLPfD>8p%KElMr2o4y{W)_^iql?%RUs`k=2B&}d53(!c&_v7y zSp2vLAelsjmQ%1p>Y1SG$)TQ81@#_AJp#j&XE7RL>?_4ux2q#iJd4tU8<2ipxm`8sgdX_}+pV$-Wx$-Bh@hGPJf z;cRdTpIihpL*Tocx_$X&nZcpv%K+TyNDMbua8Gkv2H2H>AsMbIiy&T>Yf6d%hCV>$ zIHX{=RJ}X$c^_W!lPMH&Wti^%!iix0mK6)#1uvjFuC3xoNYj5~ZqykFC53ecvU=d8 z2S}umUjWD|{uWvS0~qXuiZY6f!X<=2H}imeVyyYhYgU6>`L%j$u3bbDK51e=b=+PJ zTg?gXF~2N%QG=EqkjZyUrZ?(bEpZr!dg|7#W6Ysp?xUgc-JTUpwD}W?D0GBJFqcSq zcWbbjx)#c_Y>;bzt=QzhCJTPU6L8;YhJhEyRfgMgjRk2olIEiv`2^s(?VEig%Ft;A zbHSHizpA=?g6DpkehF2NOiZEq0*6ee(85&AERaCaw9(dT#=z;jQyJhy{=VzXhy)6g z77i|@V46{~;KJk~poOxEGn61xl~aDBRr&H0(F=(YA+O`C9}b-=6Z_g6=e<=UMZUo@ zqTF%&Jxg%nRYJ=?(yOOw@)T)9PO^zRBF;XdyT7!;&B#Jz@UzluE;LQnO&B9OEpPrv zQi(~sA)eQz{cZlp&?l^i?p;r0hLA9CGrvzJwYZ+yu#lOG&HI!2G=$3%ZM^e}5K0kk z1WRNDFwPj=YcNF&r6a zZDEM$zVUka+b&@+T*S%%(TR+a9lgXEoogo`Q?gauntQ|U?*=D^-OGX_!tUi-y+kEy z%g$sYN{pDOwapc`TDHWt?4dmHD&&)*ay9392mI&l@t@c9Or~;(E+T{WVov1fW!n9} zB9~Sd;r|E`YwLQ{LryRtGJu(pcwul#Bt8t;<59a-|03nMUskK%B)PQw#Fg5H>DlpS z-i+)gJX$?m9WFGUxcm~xERHOi0td8uPJ?9e{yp;vZjQcJa&`7a+OwkB#$0=(^NUEk zoslN)pz_d$k8Fzbv;J*pE8yxz5b^i#%AWCU}^QZ&)a`c!!ct zTlZr}T5aN@o}Ry1p;hS@ud*+q+ykj{9oI-X**RMsfc2C_Amuq1D+r~s{9_;sy`DZI zafWvtyNhs8SEsW4+{!YFS{UN={DIcv9`MinEiv>`$ZK3F%YprVfO$BcmsY00kZt7U z@a#IClwIZp96w4AfTJ0P#|e@m<$#MV`*v$^u1JVWggPzt89Q(UaD_n^9c|K@%N2n< zBk9Op02Apen$b0M?nr)@Mz6;k;`*ulK}ja)#@o@kg(c^{)5l( z6mR!)k9vQds|M=KvFgP2saARC(P0d7e<^`5Q}h()yTO;Wd~J%e^7EWx++^72o#yZoKFmUji&-Et5RRn@fltIoO*`OkOE)325J zV8dt%<%@lo5-gYxFiVMCY{fdo{0PTzPCaB$Fpt&=E!c$o9DHX;k0nSleL|XjCJo7^3Ji4Ku83AvdDS@{V|?-DKkFqyQgu9AJ})+ss1w{_x1d9;6`~6k zQh)FY=(t|YX{&NqrIB4}{40>$G^cFlRCJofyLi@Z3zRwoJ~MEQAfi@Zug2kLM?utO zEa;dQwU-BMo5ZPHkNOu@y1xiLi@ZzsA!Cacf-4QI>45QuG)^31mKL`Hr3Ux5Y-J?b}m#Zz}q&n!d zJrW!k+I@Cdpw02xct`YD$+qy!a@4p=1c))XZu^{2TenV%&F08{yugKX+Ku!>`M?u0 zqB6ELv%=ORC)An`Qn3CoSWscSTuIDdIVhs1x>_yZ%8KI8K>K1+!GKDm&FsBOeV8Xh zmHo!(Wwi~HjrEF`VbSp9x(JBE0bJyCmWnCcg(MqoadzFW>h9)W#^ArA(K}zI!fUwF zF((k5D#GIU<~LcZA~>xpT1rYKQVYQHT&A2YT>zV)`I8#6&YnJV+VWw#_F@y4r?)Q4 zm%cvBfJBhx-W+5tnmt`^0JTvh%m1uYO2Kq7YE9gf0G_` zC}wot;3hL^62ryFF(RA*$Yj9-2~wtyQJ)|i`wOV^+th)I!1s{0pk{D zp!giU`AZub|3&A7PHprRUCTB$Y34bhY!9)qt_zN$IqCBvW>5}$_e0WI?~N&0Hcd{W zNm5?m?)rdng;R`dnm@-G^srWk6Gu`neibwj$<7Rf3)U`Wu7}9S{qpsseBn=k`%%68 zC&co^{GyQ=f@+tK4xNW%Ol-d0JdCfAEQV}z+%~~NrJ%}In(f5(dUP&FjVAb`x%mlf ze$Gu_{hqu_Z|*Wsz8@BHS2j!z>= zPM>Q|5G!qu--J9O&94nUTjA9pp%B`Sa%>cfuu~a~m{aE{`Ne>az=A`696nUk{}@&FXu;Ae!C?6?km`&UTz6tB3ICwDv)F+$xrW>iiaZS0wQL-Pi89UDA zGy8JXQOrYPf*cebg*-+bWBA|vk1hS1|4%OwDBE6AOrqh8Enk(cFqSvGoL&)wKlmME z9~EmOGPQg?UmFp~*YZ6sZG_jwdkOC)ynA@}@Lt7x74HOh^DagYOwv-_wwcr}MmyjXQ;(8!4EBpq>#CVQ~r|DTSaQ(I$oB11aytesA9 zhD)kOOp4o(3+T|bp}7U0yxYNr9^W#FcVS7%aUnu8dmzLL&XVl z5|6Rpl4`lkCFgL4m~tna5$gQD&?#+>TX?2-Xat_UpS`&ta_q~{$Tr87{mBbR=A1q% za_ox`LOrC2ZI1HPS7Tu=E8nuz;WW|jAqC0zD8KiUuFu$24DzEFWM&{bh+iRJzqGL= zVW?K+>d}krN=U3n?-4}Q);86A@+3mRXGz59Z1o%*-14h7} zEO_*%INeDbqBs90ztUHj&xTCQsFKa@)@7eT`>v1zS72+f)RmY-n9JE5%-R!q?rpWh zjpoh1+@&teW4N;eC2mVrV-`IA>l{ghTrX82S#i4)DXrR9^;kXO2QWcW@(5F#&WtT7 z@*rxWSxPCphF=PRO1B78j7DuKgM;o<0}n28#<^PRX1Z?d>wF0)L z0jhO;O}gP>6Y}EOI}s7#UUQ@LyjKKDQ%v8+G#L@8BjdS@1nYRUSQg*)Y|j!R%wZ0B zTLz(=x~0Q|C=Dvj{$!}l0>OSoZL+0u<-ADEl1V~MD3=*|=?;tKeO4&i?fCBb(2h{p z@@J7S9hGIVISz3TiTXjv1^BLXqTwwur^q@p9KWBQl|ip`&F|38qqV5x0aDb}p0f9G z<_gdHunN$JPJJeGJ)A<)oDPSw+0j8_raJ5>{Wfb@iJQ$zI9^y&cJ@Ct0Aep*#qk20Z+9O?k3f zK>3)fl^Md7L%xom2+qr8nn`~--0`oBee7M>w10_4}$ z?Df>G%o-gUgBvna!vUy!+Vq=R+^yyr_8?5gDs9D* z@QMs&nE6zNwAaft;`H#;n;a))D|%I>D!u(_h#zW4=*3uo<(8?a4d%9^*ouf8LP@xf zXTVZ)K3Q;!0IR-<89W=4BoT#15VGl!DdTu$M*H#a1erg4-BH9?A&F$aNF8#Db#B_h z?BV5P!Oy5P`6?$+ktY$O_bq}0y59=EB8D({!?beLy`win!rZIyLQqvs$BQV$^t}d? zk`Hl0p3)ZDXDN6~EWtdAM~_9kQ!pJkXMH4rh0f4pSNEoZ3sEN|)oInaZ=g;N{|X@B z>L5L)s&DXl?I-G=06gc6)6iRFNP4E2g^pUM+~ z;9dEcx;teXA$C_GzaC!*nyrL!?MUZ!@Z1zy={ zfw{l*L*K+33q5Nv^m?`^42ofLr`5q1^I&K9L{eU7C3A6Q7)f zbN+K_#>Xrp!b@VU9=nPFu-)lSB`%aJK}uy8IW-a=%H<95Aza?T#X+Kln}l}$Rk0o# z&4b*lCNn;Mn>=Lm@Mj+M$YaXjLKu}`junk*-eD$0-ciLCOeE$|t6wQE=U4h9lADl(j{F3`8WJw!{#t zhtmIHR<|ESa)M)!us>En(CTLZdTzQqE2INhTR7`KT;i^ttNGs zN|GVBsv*B1_9zciSs?1i2-6w*i+P)HvX-3-LE;jRfU!E8lDg*4I#U`2e*$$3f-Dvk z&b1W0n8wwMK1JbfG%u6n<7}VDFQ)`p3_n{s{@sgGg|-QkCSgsH9U_Ukj2_DB#Tckx zumZhOGI`F>)~+{cg0<^SW!FpXa#J$Ga5G;$2h(#o1$!0@if*}B{4J{V=(Qd_O3Z2z zXB&>!C(^mnO*$~ptFK*L35AO;;I1}k^-+ALus_?2UxNAWT)Fl#wlH&LmeJueLM!~3 z3BU$q`;4WzJ-?w7NYl3=Xd}8bu98cNL_NEt$c6UZnQTcGT(gSpzPeNRzdKlZ0ppdx zI>OheoC%5Y{+)&-k%h{ot z^hjry`SX85_={orC0cNOH#fh-4Ydu~z8!XDTz@h15X#Av)z|R0j5pt;;>hNFwwP{J zXcL8ecn(ap2BSivs3S4MN*4U$PK$&69l2z59^j+|xdgRl&mjf>C*xHD^~!m9Dx=(K z0m1aVPP5f;(^}p4D9mY?A^Gb~Bv1aOl6o%L> zdh2s38Fxr=%*RiJ=&i>=6x&k;&I|hCk_P86ePC7usx#k9K~+TSxZTw~#zeQ-PK4^u z)~F>Y>_$ISTLr2Fk2?^o{cm8Uj8+&B=MtAWhQUz9d6K_Otf3}iz$=Di$OG_8HFiLT zYe<_aAFv!9zEV!5B!b!VyH=OWbaL!${Rf;ThoEk*F^gS1kp3j;a!#!zhUv@ZM>h(n zSPO>4Z)YvUZpcroEUs-8K=(sA@!+8PA2+M1C-G>S$4TwSOL4Rz46R~P2}Z!pSZ3{5 z+N&#S0%Ve~kUBjTT#{I)+^n%cB7wu!Yb3A-30-B+uPb^&@=N7E`orouk{QEOnK%o zRdl$SCBKoofGxt8OMorhu;g%<<}>nTGfILsml8t$joss9%x?Tea>8p-bQ~>i{4*Uv zz>=@dLPY*ty@)<>FX1`}@Llk`#Kgv)USz>u+hJxWXbBXGM<=7(S z#!zHQs{Y}YAj7J?JzYEffI%d$R)<;`7tt-rE?;I~Fm~EWaDuIA^i02}@iYCNo+;~n zFotd!pTbb^QDLZL!N@>R6vo&>(NtOrN3ljxIBK2`6cl%=cw$SwbMy0pV90lR*}hZq z9jbo#0hv?w?r&NSIe*6d{sBE^MG-Ly{*GkaJVUY=9ae@n$sqZMn3w%!Kp>N1p9k8* z8C=#7;-D=-y!l7#?$?RRZ?q)JCEBmC)tn|$Ly;B9Vfrs3N4SIVBqIU-_s7bz>98V` z{9Fkaxsnmqb%2$ybo)Y%x8MhO_tOJ@fVX@TBGnofyA^5wU3|J5Mni11OPUZbD|hZ( z^HM1=i7bhi#ZU0XfN?2e4sYh>%#y@tU*jp5vmh=>w%{B1Oo^`{UliZTcB;DTorb~{ zBg5Z5f2EeNZO*{Yw>?}|9{j8?BV2xNXoxrIwI{{|KpJrlE?bnDnQ{)kG}HW#LwzEZ zM{cF~nDmiAg7G6gpK3|ZJ1psW!Dt{L(yRI=mYG-=mEpK7M0c?qa8Qf)RzOjg8XueI zUC5r{D_YE%BGNfw=7dnTuYnkO34A=sZQjLagy=UaCz!HyR1Jb`7_F7jhTl5BYaokccgjgL$@{-YBcOBXoA4{*BN#bmJipy~IS$wm}%59scN#?dI?PEL)vc z_Ytvf<1Sowr1>)QQ;rxAL#H=oYgHxdXT`l>fw5Mkl2#n;$c8X=(VD4Ozw~n z(Zm;57FPU^@e0Ruu{7k8^k7j1cW2G0tZ&Avv9Xy;N=53-zx<5KvE6~muLOEttC-wX zVf(my|JWw~WE`oP&Dg`+QGwbz%-aEw`KJpqP{WmN4;wiQ?E2?f9 zcfY39f0ru##wJ`c3}39Sye=j=wEB6HY-{nM(|*qvyC%zrK!b~bw;4UwUGQ;-x0suE zUPoN8_g!!C>)d#}*@tkc=N0Ubn?O-xQ!xR)y;(l@+uZx!#yxxdH1Wed^o?8b=9f#~ zU-lBaA5$o;{y*rS-2DD4e!VX)lmFdZP(HiM@BS>f$Zu@+6Z1AU)5+Dv%jWvHr8oN` zGDIb0@jm$#W5?dZe%mJBWVuscY;q7@rtht1Sb zqZunR3yGSzb>ui4GMbtXpWfV=Rk};C;^ipF=5@bpjkHznk&mgB;vd@$!u7o6vvn7L zV)O}~G#_yoZ|lwPX2DLIkG|iRjJ!X=7rQId)40QLv{~jzRJ^w{S~p%68(=#gb8r>c z>Si&-=eF}74GD-U+^yQZT;;?2LsH6o|6pG-F8mS*rB-JOcLt5u>;7DZ8~MCe`=$IZ zORq(&8CiOp1PDCdh>~(i8c$wh#eFO*?gGvM{{w`Y4^LqFGfoDdO@axm9GW`W;_$#`W?&&Hr+6p+gE_Kjw%TIRwB~0kEBEfb|0_0OaB?gDsXM(o1f$@J}K#lAY6Z zOy|xvLoLH~FLSi_*T1JVyvREB_PmFYSFB*^#eGS_{j*PDkJZVKM}WN-fUXmU!f$-w zb9ef*xu$^BC)c}cYwo*y5;ACQ&9iq;<`=X2UQSI`cVl*fwkkx3*2|(dpO&*+EN>{y z=*{nE5tDSii$Z$rXZdsFN9C1stPPvLk$0P}&OSQ)8!79>|6aTI_w>S0WEy@@<959i zZp4R@MQul7u;`aiq&-k~jEgFaU08n2Htth+A~zmrPi;jYMEJ?sQ7e(X$i;o(egcZ@ zgJB5Hh$s-%){ZgiEs|BI2wDns-eg;%VW4R)qWmdMr9Q^PJbOhLcCQ%b8ov?jti(@{ zneiLPe9_-Xjo@(5v+g+jZCVbjEZjZo?Y?AU40pq07_jPHrf>mQnxv+1_O$*fl(Vp0 z%`1SZ8!DrfVP~X=z4w1J?6@5^!tX|kt-$0r4pqeds`k2{H_@lZ!CkJx4HecHu!y+n6car@5RkF@>so;&4A`>w)SXDnv-Y0(QJ&o zn^{EJs69n=qFu9%I+ax($$;E{cJx7&!c<8y> z7SL$AL3ZoRXuXv48e62Dy+&P3xllzeW7SqdZK?^bx%azY(dr?3 zNtUjlL<)Vs79XSma}2)KX?(Z^x@P+S*pCnA75C$V?6n>r`VimKnJ^7?!c+N+|H-oc z)jXDwggV2Rn@t4XQ-=njG%1PM!?q@4XE7mHCpaLMUZe$guVYEdx<@Z+ttmU76mlJ- zJM1ie<7J<*%ZsK8gASv~xKAO-z`}~A5?EIP#@A#~U7TK2-~75om0rrfkVb@;_!1^T zR!cmrn(coL@tR(4Kj4{rLwff29E8jEDJu5oK$?n`4WeSd1YJ);#q|F@6+6uOgo@3f zng35z?CJw3Ru$Ky=mJ{20UDM|`~cY&<2zWlihylWD;eqG^XZjr5ikNLpFq7x=_g$~ zLT?I`r$p`y*Vc|O{9#{IaY~`EH@P|Sn^Y^%svD^l;b}OI4K;)cN2z=NT{?7a9ahB( zC5@iwongVqZ>K0!WgT&J2gsB0`T#n<{TArB=J);R_=l;2jv-;M>n1@`6m9xMmXpXs z56gp)WQ#Wak0XkoNflmqA4U{)e&hYYBxymNMUU7)4|z2|Sq9Og?i4**=!^cNpB{;w z)baEP{$6A^1N3Ma^hgzmcapB?k&x7ly!g@o_8d_3sK)Q!k)lU|etN`aKPF9&c+Tmk zM?#GTks`>GZI@&?jvlE-g&yGor^wO5#DfFmNbVb#Hve04)M{8nXfQcSReL-+67Eo) z#M0!5LoVcKvqg?VUo-QH5dExUVCMh%1~dQsqy01gC)D9G^Zisvx2TXHmqmq6y{MlG z1%(Q+T!jiDfK*gSXpqFP8BBwM-&0E>EOj7Dz2hXbr{bZyL4-%%6xG=gdPCNOU)F;V zy4q%?5KtI&F{H9u$dFehR{o%x9_Gbv3@HQ1O#nMRY8!V=vvd(;?0bxt*At8X^ zf7pBlQvgD=Y4sxgNw#esa;_e`E>n?Z(Lx^AojOI3kL!_@WC16|=Ukm8$)_Dpl9P(- zNw`naK}C;KeN>!+c|P4t<42=GknQf9a!y24(cTrq4lvXbOo%d>3g z4eCV}ffPm**=@QuyH~gMp`q65M9}7a5uIQLJ`6q%p?;KF>ZZx#1}CuoL3_s5e|H0J zi<67?&->QUewp|G1H6B9@A2?n5)y>o2OH)q-s@GqeTr%J_H;HMwl!{TI+#W5^QhPTVmdm$~-| zC6_UCex-yj-;FD=ZNJ!Naq9C253A3!MKLyg!_P!I5nRwwZ)X+3cHxAt9V_#Q3P9QZN&B0U86iGD&YHFX%C7aM6Od;U=2?E z2(tf`#5Ycs*mknSXC!gEWygY**ix5{n`DD-T?!&H$lYi@)D;dD)$PWmBfDE*R2U!q zM|j2EZl7=om(wUm{Ds%Kr1z{q@THb#5Z5m(v&lhu3%xtq|q2b)Ax>Jhr5i@*^dEW~FFAZ=?Q z#z$DLY&Ca1D|m_HW|z0SX$Yd!Z#^PJSe8xNh1?3oCtl>a>meT*bAB8}nVHv{)(GR?1XTbr(wi3%E(Gp=R9ZSss9EMW#ufx<3Pf~Vz%3@MBSt(m3MYVPU{-OQ_!r4i8dBQ!`jcKr)~l6Jh- zdPGx-o@HRMTVg z%Xk#~xmd_yyvF-d^G5Z-*hNm9ajv%B z=!BcdFs~M4&wmK|SVk0n?tI6HLQuyq{$?9bZbLEZTCt(%Vwq=`CgUHG8-qRF#z-## zQI4jJCqjc>2aG4?<}d#?j&jEvPn@Xjzjz~uDzTxE!NLOHHlXQGl{6p!g#b^vDV{{r zuXBt+8UYQOv!@TfcBe8^WI%#0*4+wXM{)5YWpap=B^G8BJ~#;jX*fo7D;Z+S;*a)G z-w#sjfAA10@H-1t7%qQCDnu7syiw^Aeg4FNU?6i~Fb4*65WB&0{lj`^;4Xj@{P;I` zXdk+MIdmNp5Nlu+dh7`_60Ge_!C8821#HzN90QvySKBYw0)faSy_ocWt8K0`BV!nm zuXBfmzn|CZX^?~3T^??I-9_OLeUs~XOX+xODJi>2k7e1XHX5->M7}@ogaxa|{-$~$ z1QSkehlD7?wC{zh=~bl7BdhaY0_Khf`>P(H>HT|`ayC&$bS1_|Y|pK$k7l?~q5QlF z94avHpzw3Db>UPnOuTsOJLe~XrDtARb1)WuX6dH+J)NZ=|+ zSc>;?^t0mwv$AArGIXK&X|dt}LQ}!E(~3KSImE>ocams;*wo;Pp5tsNlq^4SE1*DV79&|yZHO5~Qe$5zPT{vJX0q+Flpz5L7W;UJ-Oc8Hz0VwG+B92kxvNAl)?+yWd zqCyI*F(6R!Qq=B#Pyn=XGaGxR&-f&9dqwO;3lnii#^g9a1jy^V=MuvtD^aL$ax{Hb z32Y&t=qbD^6cx*!#4ens^ycIcn&UFv>V5D~5*}nAB+K^AzO1rLn%s_gau#M+J&A0t zh)0r4(7R@IQng<7N7h_qlh$7sMqPq4fRgxdprwx9sk%sBQ`v?XckXi;diK z1I0&YbAP24oA**p$Up#*vWHMCsf91c**#k^gjS;9en7k$t7#5q zDIG(|90}H(7(=MY^eQ%jum~krint0Ac)B?hGiNpt_A#IpwV9)N9~@rjOPWZ#;VSZv z*c4ly57M!P^2|d&O_xagi0LP$&<|MR>6k*-N>|{)k+lAwV+sjxlsMH~)!&eL(@4rIp%-zZ+b7%VdEAm|_$CZm2PbG+o{7pvX zcCR|u5%^;&qIoe>L|5mcu4KWTm&xLRLrcg1kvp3z+bttddThe#Bnz@Zj?z5{N^atY z^5V)4+s^2GyRSUdzU+J6@R!3vGhk_U#b)P;R>#G87C)K=8wp^#QR>2v>he_avmWXT@|Fw7x z?)88)#qypWL4-^ePQYdoe2xf(k!m~n?9CD#IRNRp*a+rNt#EIu{jnYsqUiptwy1Fk zVS#533k_vdo3rzPYD2bg5wW_W3#A!%?=l=4mK3=_05qhuycp^b7 zEq>}aI25f;0cgswOQWUWg z1>k@x;E5mmEDKK>Gz&hJwl2X6Y_-&-oc896jV1Z;SrWN0I>%n=-mFD-Dga3gcXway zG;OU0s{w$H4h!XOuz-_~cLBhGsuK4(c3)Ai7=*c-m+eR|X3W9lQC2hcaEYNTDCSVJccJqH}8qyD{68Khug&w@gaUuyA+kM5G zlLZq&HL@1k-~mCuml1juw=oWS9xr9HN)}v6rKtsZp8}5f#a%tqg0_GWWCRg!@(40Y z=vd_%r|^+oJ6KdGyN9@O_J{WMi%ao4@IK;TUufQrco?44{5ikYHWZgLCi_azJ$}R~ z>?=PKF4_1b06SC>o7R_SG0RZ!`G)T^>N^2fQ$}9cJ8Yp%ok0gs{UMO4pV#ONu$H`R( zkQ4SIZf!;B%}A>cVsnkaS-DO>lEuuS_nlzY(Fzw8|AwDb0H{W*k@#}*R2rYMYmH>2 zUN);r0U^s=9&=x-g!pFxl@w`?_4bCq}Ba~9CZhRcl2!wi%mu`6-~Tz z*Tu1-3pfz4onHG2v4XUSD4MugYC5dg%jRkHwFcv|P$?9H9NZkmHZ#`yf-D)%-R5Vf zr!t(9Y~{PeC2z|jI#6v+Vneb({P5ViD~y}F;(wzA=X&-^-Zy)VSIn1tl1UXNYlTyZ zKI#4%gkK7j0=00kQhRY5T|Q4jn~laDDs3Y5BoAyq`Dl!r=BJYwMp~PjG1X0xqk#gKuWB zDmadUCK2-m)8qF@1^)Mn>7RAE8tA=v@Fv^Oi}&;4dQ^8Vu;K@}#9Vi(xtG>cnA|k1 zf4P($f4aQ3wLhdL&VK%U6_GWUh^!&;7Lix^*&UY{t=U}>ePn)Ojr<^^S6_RYo6zQm zhQfQ~n%`NzYpB(5WO05*d^S{%j&wH<2`y3WtfoS^F{CWgYOQ+f3rg%{M-9P3E+6;NM=T(r>yAinRm+&{P{98dg#wzy}~6{IiWKZ zK!(WhvAR#e$0_hZ8|=?-^Qb!?@Smy29EocA=}Mg8l@ zB)4aPna{CHdBi@`CxMnrteiuTB3{vA9udSO6)a$QhH@N5cB)T1_(?l0pOhG?xT9gDW4 zq8b@erSY1#VYIMyaCkdVy%6S1c5T_qiPgnjQR+G{do#o@Ux**fPlorU4AhSP`nJ#{ zaR!B-sxp5%MbWc*NQKyAS9vG5g)Y=K92t(6-sGKL6|VVU`hus>_{~Ey6NNdw2%0j{=^Aa5n~Ac9Y4-(*tcKHj+A`SKwYs`YL>2xF zEOBn<$?)4Z0PLp8E!YkpgUQM81U2~y2TR>8TKyF&k5h>5d>)B@d+YE>ul?3Va&vOM zQ1F-dV>i|6cJOP(N0hM%!&QZqqB+|oV_Y>vEKhvL1kwExXZOlTFE>8iOPSRxi>i)1 z_fkg4NB`r33Xn3{V|JL|87=tK6uez2{3OLADWcTMHJig=6sD;ZFe>fM@d?URU0Vwr zXZ0n)_B`{qWU+`wV%MxBBKy#2Y6=+pq^i#ft*V0m5C~a;_a*(!boy^VjjOa5ha1Oa zKtj01k2N#~Ua3kd^BROomKW*$OF$fgw+iV^n_k_jhVLsDTMMbPlKnxQxGrM#+dACG z?&^>gvq&U4Ke7S@-w5g{S?%>!pg06tX*K7BA+q#N;K5mYC2*14@`TNrARSlhyh!cI z$U$51BI&J*-UdgSzq%_81oIvTAlsanCJU@7aYj#zLWzf(yIY*B*-7nU><&t1goW^X z5)R21hFx1PETQ02@H%xrMX+j?(39ZoSz_!MoS~c(<&AQ|39~X_H-l&3!!*(yH_-S{ ztMQ5J1Yr_m)B!eestSym$W?cB(x5;wI>#CLFh4j?{<}c_J5T;QBl4jqc#64gRsU$# zVKSIFqqKYV9T4q|ut$4vb7ItsQbQvDMY@-f7%IVLr;G5@KQ5BohVovx{FI=76xq|XYqPb;ME_+8OY&*G3nXG zeHfm)O`&Y+lSa4%$`d5s=uIQX@Si(93@GxXlaV*iwp|m2{{VM zjQWfUJY-~ddp&g&YVsPK?zfrAUbf_j3Od~{i(ipn5%m-fXvAVSu1{U+hExbqkk6=G zj&R;*_(WygUZ35x_WlyT)Q^XnF;lq%D(I3@XW1dt~UQZsVwS0 zgtiZP&w@DutI(6{No<1ud`UtK&S#WeVJsPMoRH|2NC}6IJ`W&wxLH^vPcJ*X|!waoyH_y8rb`=vbTsY9F zxu^(?y);?yzDGV2-g^WY^y*Pmm!qoNs^pvfVY_#Rmy9UuQ-xb80#{T2Q))BkQLHUP z4h`{Dyf0goaFp(G>5lks)Z>A1P6yS zT>H56x?zzM$tO!_uwj9GK>bjj0o1aP;%H_LnBtSucc_#z$V)KukHi1}&d@-SN{%86 zI1O&p&h#fY`+q0V0V_4Lj7^8l^Aw6v!CSu<9m1ygIA)n)H~N2v9^!-{H~2=B^X+)y zIw8z4RoU2V<`-oGlUaRE--ub=t$N5s=bSK-C+mE;!dd207?DB%%Y{ggz%sH^fc@cx zf^d~YIjd9xEj-5+N*Mkl!svmcf_H)rf+d2i7FxW+gh)hB!S&K)!KqZkjC)onc&8x= ztEc#}6k-5^eN0Gv3xGUnmyt9&k+g^qtclsR*k<L?_Td^=!#TH!vle2O-~ zC+`R-zt5bhy=sRy`5Iwpv`gO7ZJ+7xo?AOB5FtU62pOXy%bY=M8*PFoqrX0at!ryW z+8T0gI9bL#nE`RNKCBmiiC%8`&nqxOryF9$1b^j`eRZLXNe-MUypa_TLN1B($b1-% zH{R}-&Sj9okYqH*K{|3gtI;d$K}%>dOk7#C4G?V~E4GQSTw*?$S&y zICd^Si@2=-0$vqant7YhG2$(14d>7bGQkgA7aPxiAm@riv%dRZLYlRwJ`31BO%~)K z$z@Zdn1bx8GZj2vt8l*`JiXG`UQxV*uvuPhP3{P9gMCB;7I@#WK*(;(#O#3kw^|)2 z&U>Z%kSbnTyz{>;e!o?mz&=*-vr>a__XH=Q)eYTj)X#XCVUA1BYR0T0Nj)n5;Xjl3{^rTp4xqNjE;s3%QXef{b2O$BMH zXW8irJHJTv=B5*~sAv}0X>JoFa5t|!gT+#te{qy)o)@*Inb_70R*T40cNNCF@pgVm zsLh`jT?8@=;KW=>45Az$ z%IhJWFh4e5!zl$N9_iAaY$mXI15e(@ zOnmj;OiW;HHCypgv^7Y8;H6C4-goLQx4S|UaPjsxI>pCtzwpFJF9h5_oC3Hb8yB{s z?M=kZ>Tf9G3mY95`l<8_0(*}#z&t8oA8EGyq@N@`)gxZwiSG$`4@tE7kJY7uY@Oz( z`_MtqOiAQ6C$SUsX7Im{&g7jI^bHpOQX~HcubuV-9`nSH6qD2o*@)Gkt)bGFY~ZDpzCf zAFFF%tb`cfYL-$}Vj&|8Wt)eV@;nc!+qt})`Jy)D>cgH_+|E>pg!#Uo^3YT@?t z0yHEljP+%J6S^&T2Ck--bhNcWh`N1yX%nUp!*%>-hcfwLt{Vz{Ec>#x_p|GpS91VB zvwf5N%praeV_@sHzAFx!OvX^2W?<(=CS$(cypCTvTb7v5f{&t?E4PL6_?=Nc$F71P zSuy%fsH=>1xu2S5!Z~1HY%ZrT1IV^c(ZoT(YFK@V@O1)ggc(vfLT{PRo`&8g)i-bn zrqG9?9al4_zMeet?=I1zV<0D-8vi_V2!`WD6d=l?QR&pKyLi2okkMP?l^p!~lP@H> zmMyv|_&wPRN2Pt}2^7Goi~AzD+QgZ-h+oIKe`TYZNn`89MwbOK~Wyq5mpq7P>PyUjjf}*AQk;n%NYqYyCfr!j{4n7Qb_&6J3jW)G|zfMk4 zduP<>+N!1+?b!GvWl1$_w9BWps^{s`p5s@mtGCMR`b2yGlN#;H4{NmPo3wifI!XQm z1&7GNXE>)U85xmN>yEnykdTdr~cTBs)?;1d$p@_o9X0k{+ju46SV`i@p5k-zjX{Z%W4bEOZT@x^I8 z-_Cs)1w#;|k!fTL|4j*4Io_w_M*d#qk5+#yRwqKHYSh0pe@B6WGF4!a!=ru|?xKry z<2stppE6T*C4buG{F2^9mp-DxEj;h$S^nPP?=WdoGOTV-D^$Pz-}a>Pkte_4*H21M z7ygs()4}}nSMp9>O#XrKR{2G(eD5T!mA~z&`We_&JNSExZ!)N6{`fvHC}wE2s(Mku zZR$7wuwcTQSd0leF{avAXVhq)Pdmm}u;#P)M{KV(4}?yq7kO8Z&1X372LR@V(V)9N z2tIiWt*;^Z`fK@m)&PI3yB%lpnFZu_ zaYu7OTinWU$%be_E5j^#-k)&WLXGj{L4sqOj<>E6_a zBm?(dy!v|A0zL`x!wHJBMz~HEYBNAi?5Nsz@xqQH2%z<1N+3LM2kyIgXp6HBk9n8t z@cz1lFCg0CStxW9_UymmxXz{U8&E9VXL*(q)+JQ$zj@zbxCfPr7_}kgGUluMe@Qrm zI8W@mSo?ZVDjew~DFBlj8id8QWi;X$LDUc10OX|gf!9QRY=qc;R|nE}lz?o$j^`=Tu8`VakoKRr zZ@6eXm$dNEt&*BB5wKum2|=3>NyiCxk`5Y{R@ zI>)T`octro0IN`lz~y^$G;#Hp9MRx0SQzVz2jAKp)DYZ5dH8#z#4``}OM8#Zp-Y3o z9HwVmSD&R38H_P~?OVmvaO+Q`OQWr2MPnl_agdyoCSwmXn~jmxQDZ>XKuWar46VQz zahnj|<9lrgNMw&VoUxK26He+g&k^4$u-jsZ(Ii$ZT*2kYbcE6 zf`Q!-Y=*EZ zolUoFj3nBr#zOQ4;iCO+wX`o3Uq{Zr69LC6wmV>EK4yg>0gIg zm$Y6wbZGL8n5rS&+Fph5PL@d&R%1W`t`jYiU50DXM`D5+UFw%$$nBOojpEE1ncAAI z7QA|*S0UuEN!V@cMiGm2(^J?6IFkXxvf&t_)qV=bMo)5r8^%vVPQt+Lk5$Xt6+>ut zKaPpJk$CkKO++mImWnU3I3{#d-Bx7nv0jDHvH3;gWxU=pf5_WY=7W1s8!#hy6qX$B z;=9N)iAe8FY?F5Q^jn@rdaEb7qd2HsYG;6pv_iF-RHZwtdHa3P2FWJx(fFnk*<*QhraIzc1|a6Xgh?vZ$L94Wx`gV@B=1{|F{y`8Q}CdO z*L1qnp7ABi|IpUZ=esjQgQ>#!7LNRTeQ|TCFL%rOqFL2q6zWL>N@-MUt5$y70xyqh z++TYJZd64&7cNm?ex_dT1Mc3nxVtF_+*{Y3Y+gSQC~Dzau@r$xPFV6GB-lCi7p-H7 zS~;xw$j-7>w@_L#-LhJAUnN(=3t`RWQS4K9FXB)&a7jnjMf2NVVI7Do>bD#ZG4!_9 zLd43g5eqx^j+o?B6%kJ-#AEfVYl%}ooFYtozeCh8EGgqVt@!l0D7#fnuUgAhEktQV zas=j*H$~kv^*b-jjlz&u)obD65qLRYODypRQL(1(jb42pCP+?ScQ3?-JXo{ib|}AD zU*}i!hsVM|q(09d51NhGLFYcE@FQ*Y5zKU2FX5%`?h$^UEEgP)0si*WDRf7;3;xkr z)*b*hR)+0CTze3H-PG$ywAmVlI3iKkMxqJiwOvIYC{dFJ*RY~L+--kz9fc73ov!o8 zE4k#uNGfffw14#nid4RK5;>){B=I6tyiN?$pF>Uxd$`7cHNohatC7Gv8UvoB^D6tU zWHi?VlqlBXUTb-)aa_q`H>gS$ri}>y3-1S7SJ_MBR_PTvBAk+5dAs^g&%au#|I_|v z()PVM^=i>AGHH7a@f7J45g*?w_oGy>2w(gvFzoJ;=y3dsp22xc$@Q3$E780EgfXgT z&+pI=!QhK5yo`{9)Sw|7|r(^>3 zg^jH;nudoG!{%2 zQ9O23vBXNU7Zbxp@1`BAi@@7kmOU8>ci;T#cJr zFeWI@NBVs-C3wUxBn#o=x!qK9II?(cuQG5GHUo6RADcJUT(HWF>92sOl3&r+)e12? z+Pc_ZP84zP1n|`KYk1pI?W1UZ@L)ReQ)Jlhcwf}-!qx#wLk__z-@Zkh**5`VN^a78 zS!eq~5ri*dx5tQ6FPzrW8?MLy z^!TjgRt)yN#L3$U6@l5hWK~S?NYlteRx7=^AjeefXKFk~y0PvJ7$D(p6+DvOpE#A= zpc_WIX3~uy-PMa=4l9)#kCAd%)W~XEFe{HD<<6wsUMk~z$gawx$hxn?KAJbdQ$Mms z<0vEAs6X;S%j1;BLVVO4^JJJy!CE)&=a>M+J>*_=qEzf|Hdv!UO2dE(aFj0Q#PQAw z8tP%4az`&xw{>d_Sb;Ni1>0w%cFQlrjJOXq{$TA26eoHW^1XW0J#|K7u~?#T2~-UZ zAvXr}5ebkV3W*%CN%-?y@e_QzeH8=*nb~*IvtBofxw|^=o z73+9MX_iH2UyH_G(oj3Ak*+E#KC0Q07@WkJQ!yZ}5*`|2SGP09;;n1bqE{Q;KB19W zBsR6caPmec>j;9@w$9Ozx1TdYZ)=M;#(T_JIUF{}7(N|ztJ7kz0n=pK^l}itO1J2+ z!+D?7wW7op#-L$D_iHepXmoF^{SqS)$`0&>jv|rKC+zlpFqf^JiPSX)+}#r$zpzg{ zj^uJzu}PeIB1yID;Jc_H3mS!hABeI4jvr>9s9~0?=0qTvg@&#S{iAH zvKodf#eGmuydDbk)-Nk)W|)q?Y;V7q8Xj$g!|N2yD-9;UwFkw1NW8F+UV@877@CpIxw1U3>Q0U*|Vt6)K6jG-5)y93=x7N zLkOV|+KBBQ-CG~H(@orKkASzLP*0nW zeTGO)Yb3-z0jCJFB-EaUGdv^jTC@pai^SMpy(%+`ZAH`&kJ)=wkv1M@pThDq`rb!q zX4>xU4DK6@m2gQL113XZrF-p1@aLFkw@2Zwi5pe|U`XZly`!x|R(-lUPFr|E;}^9@ zu3uMS{hHRZt2~yUC2+gGS4XI!Wo?*W+3~<47uEfCUh}UVk31lJzqjyS3v@SLSRmgs z+Ish@Jy;-zaf4VN-^1(Td*nK~X?ZoRi@ROlLjfPsT;C%CuJte3E#8kItB0)5>K;|R zd#&W^(o0yGcefY1QC+FRYXVnQnm#N<0!j{Ir+QS6y?#-b}@J_nzFyJ57oMUe&8`Ovj? z$6U@%d&iPbe?QT)s^>30$HMS-BpO?++WiTVqm+DO>~R1kc8;t7uJ6W|K21Jf$#V}Y zzE@r6Er}ZmPRnx`U7LK)(mAr~pAT}LNXc&Tk!Nc!yi(TM!4$U7>MQIDZT=>oZ}8~2 zv#t##rmi(9FTFsCLk7`hSvw1Kv$he45k9o=m{SB>95)$}F*N+gLmI_8>iS-zbvT+s zBRtmiKj8T#TotPQMVEHL*$(F+jjI=GQ?6dgLoyjN{1W zCAwnAk-AGhbmE2+nEVwT*O(qTPgm$?f4oMz=Rs@Fa7Bk=?ify;uC+BRU~Sr^$JHzI zCtaCaOE1x-DXKFfUv=WD_!QkV+U`Iofpxyn=Bx5^N7;38n?-N!FmRx)zXz>(&L5STd4ngMN8OGiDuc|;Wo!_falqD*gM*e&Q9ZOf%i4DUq}`H z+_hWSr{LQ(m@bOLH0|Q+u2#YsCDTyK}TG_I|yt4;@* z8_bv>^U3!YCYjzlsEXU&g~ngPsU753eDhoH(2=ATsV%znGj*Dw_`cRTG22G3&_Q=2 z=GtHvIF;VkJfy_;Z!XHkGYdBm^ANlZBI@UoYcVBPMUm|&?1nas2@Ujk zS_DoOa2Ti=>3NU}nnq?c`WB0LW;<@OwBN+G5T$14>^!h!5iURME4Lm+sf`e^1$PoZ&;PB|Byp|8m!e{)R}#%#U4`1m=2TGkHUYi7Nc53`9+Hyug6kU!6Wx7KNyp{1`vt@B`UKXVOt3++ z-i2>yYHS&iD~+f9ren#fZCrI!36{ay8mkT(E4#!0L1P6yTh)cY$kd1qNWr(N18RsRz z7q61i(kBX{w7VXMyvXdKfs-3MVI_c^+~A~PrzfP72b?_EX^nL9f|D0Jp${UDT2w{+ z+_6^G5?^Qfy?ThrtEfgh$b{%sL95+0?kwmXvlp-MC*?mOrnkCl3> z0bc^r;TF;xXavGeVYdyf7D0Hq*Yy!6x?7UF)xne#0v%QZNUp&)A;JG7Sb7Do?N@eR z@m#9whOH#z)Jg<|m&b$_nRS|?B}N6iJmJ3c0^K6wbU4?Sh*aTKW+W&wCyBjN-Q$vo zmb6)Ej;k|M=1dir;HSi-Gxe;*AZDdWc*dS0(nGay@s7PzW0F{Hj0neE(jISJi?d%r zy@I*X>R5cqie@1i1EjrrtoFLe3zqZflZUj#sh2!$g;NiC>Wx!3dFq2xjXd=&(G=CV zTlLgfyU`!)WzpDz%}af#cF7l}XfJrDn8q5g+H?zXEDW^YF@Xm4)U4}#?4E*9k04r? zYFlETM33dCi!P0}C({|4ooW+`z76wV=y!z?mNbsB_aLQu^iH8PPFs5D;ljp7jMi@Y z97j*8FAQ(B8-0v5==yG>75x{K>pC3p{IwE}=&toe7k1g}F|9*4G1xAHT_h$R0*%sw z6458JT?^U0yaPEB=d%|x1eCJcf5AcI)&4Yzupx~)i^ia~*tpvmG#-D^e^Xh+mY7gV zhw=SRJB=>obSvqK{ynNB5IMqY_px*Lkmkx@ z5uLhsTuD3Bod|5^Lsg3Kx*n-<_p|!oEo>The5@(Py((r1qB%6P7Ry%-6IR6HG9C(R zUHhI!xrQa5-Mh7Oe&3CIsm<*gHccGT*7~DxpzV6Zq?$&d|BP|p9aC}=dr6J$u&Fcw zmS>fL_B+fS`>|~xd66Jn425(_(gBKG+Avh}R)4YpA#AneQLAu&FVcSOd zVwe%&PHP{E1mo$p2Yt(tc5xiHFGCZe2VTDtW3S!zZogN<8o${`O%Q5D?7dh<8lkb_ z1wf`zX7}AoL*t^brHZ_TQe?X=nmWw@Iq*eKaj4L;-+=9FYY(rozW=v!zetu&xV>;dLE;7 zLkvH#{#c1ooacSSOFTaySm_xIburYyd4D(^^mKy@#d^^5&(j(O0l}>W+~`c)G|lrX z>Be>oQu9F%4YKCqUedb#b_zJgL#HaEV`|+>b%e*+uOQ)=SHaV`T$`kNDTji5|3Yo# zYzE704#74e!gpE89jWFoh|yp#LCmg^!4mZgWD+MwfHC+8D(uz~Y-7MM5!@_ni7Ari zeXw_%##V=jt)&pZOR>@0)+;H>B=Pyq-*6Y9_(sqG0p@B5I-TN6a>jRn9CImhXM6|X zt?^({qub(ti5TxwZQlbCI=TT^`k75d7qBtngt_QqJw51Rw#aY#A=76Ue(}q3o8xDN z&D5z%Q_tPlv{kf6>*|Q){NVQy-kh_i_En^~D7@u68pX|T_4^=x@9&Ali^CCgG{VJK z8K$4yeZk8IHe1=d5z95d+$yY+P*jzOYL3azinvZeoO=dlMmc*`7wiB#Vd{CvHo))W z!*HkH-e~v3kDQFbqkS|IM2g<3HmsdaRbvg{WC^|{({p!YRnKbpbL!I<{PqRnlS7~2 zMNo&r_C&*2-&8n4fpMdC04z*R*LVBbE+FklW+eh0nW{17x5j*9EKJTMweG|#hO~7T zu>qp)9Y=4Yo;drz#v0X4qFh!%YtV^pt5fOzZM45X7!!H43t{P_w;D@beK_>>Qa=~^ zarQ@5{Yn3w{xns;`@hu}=~?Z0;4ZSckoQ$e2U^AnZ0xJukatV&gs9@kq;JG3!T078 z&5%P&L-DvEjbbqzfV(W+Rk22qliKaD#c(>?k7nI!Too%!HD=dtaL8lL=M6h9v3ME5MS#B`HKnjq>@69V?F>Hx7+p{01 zG2iM!%Kb_+u3;3!f%fUbc|3Y>GX-zPcc4PCBY~bO#N!0so?IrpL?SUIt2f|0rbN6E z+FY`(m?R~w?Bia>tGQcKNJUAx%E2h3=)^@q<7s0yTDqe|kd_3zQV5rNT?KsGCq5~2 zL!&5}>R?f(v7vS|X7-Dg=oH&XbIA*&yfiE|KdZ*pujs)*4_i=4gf2wq`BX=2$6AQt z#Q0!)=OXNouC)5RgImF;_I1k;vV9>oaUt84l+u-Eoqew0xGXEp=)v~|H%Xj51R+q_ ziqxnV(LMxziVF0~Ltge^oHm;E%~h!6P!PhowhT7dE*UO~mFl&{&E1piHaPt)KC{R) ztf0{<_1WV6TsRwdJ5}`Nl-SxV*u1+&sVVWYzc-M)Hp`2+flEIln)!(`W3Vnl>()#n z-ruYW-I@r+6pZv$o9CK_9hgf&lcdB|HxHzV1*6{}vy*H<`dOjxdd=l;Uw9yl6?JVnrQ&gu*e? zYtz)14s1UN>nyp~&#b<5fc0=WuD>N?GP*Wf^c}M2u=*|y?TQ`*UrnyWwrFlT;?bhX zb#2nqh(uDeL0i|GY4!k>v+;murycMgQ_{Kw7+Zpog_sIzy%^NKYtXI6oq9@oN$dIK zaB(h``0B3NbX#B9bvWFvdxqk#1|PXx-MSCy+&i0k(XKaB7^9u>Pe$#(WVrnq$1%=gEMR<+v7GS} z#?y>HF}l4Y!)wRbgK;F|IL3P!vl*8$mNIT;e24K9#wy0sj8_;N7`@)*_!)y4Lm9_0 zPGd}Av@)(?EN9%wc$o1#BQKZaVVpiF@aI7uV%guDzACmNS0bk{T*Wj zs_u$bx;r?ZTwX5S(|?c{T5A&i%33z_-`pc}t4->Aw37PI|9Ja&RPXUtOLErKd`q5w zFm*(V;A8VLEZJ%K`a#1bpK8s>%-8EjKptcpp#C2jl)?J3$=1~L*yQ;c*>k1V-`(Ra zc?&Fg2xPE~S;Qm!-IJVUiMJ%@rKaokgJgVh$$8cci%^cA5@m{=92GlpN>u!miIYwD zL?uQ~m=ZO4tSKTYaaz>m_~?lf^n->fLT_q39HyA2#NSS5kce35nd0JZr=kz#bWgBY z7v|*2>C{2(097WTaziYLCOucfH;!(z#y4sPxO^;VlKi6B%Th`0qLOrqPgA%)~oyn zy?WLQ-2M0PDBNvuR}bP9|D|5FiyjTwt8q`Dh-m$nS0R$XzX$ zYRHK<{DYo;uhdHbiVn;rUf1wSS+Am*Uitg7pEL%_ell)@#QM<^3x`V#eXprrwW+CI zHS1M33-90QiF%Sa<{pbRA~`cNB{_9I5{^TmhQlEx zBYTz%#>Ej{Wq3K+`8k;u*8u0DH_t(X$V^_8hkJ-6&ywdVRu*Pt4;wlw*_M`(BjV8A zCCfFGv4F9HF9#8f36{-p20I8$qwqRbazXX9^XlXOv?qY+DVc7`=0IWyw{ zOGF;dQj;^|EveRwob0FqB`7~AH7H-EBO=d|Y_&vZTP=BWl2a{;d3@B2#F&W@rkKQ7 zQ-nEsLKNE6_?TF!P3MqDOr1;}VItk{ATW=QnA%hxV~UTDicg$8apDvyCpB@zQ@w}~ z^&&Dea~RzMq%!zb%dBb1nF}nlgxV|;RWj88-joUIcII7_6A z2KODQi^)k&v!tN{BCEPSLHT`j6UPtmR{pKNGk|wPc|PScW`-4CL(=j2RB{TgG-$V{WF zERX--AL)(A$;!f=9jZUQ&XnVg-LoR_9Uhako@=v?!2>ERGA+0>_}Sg2{Ih3T@Avu$YS4taWVzAibFx|2mv zvSjP>(Z!@%EogmeRs`O@{Z6*b%|N>HDAhEU`X7s~dH&F->89rzpB#z(PsMoq{KbmW zisZuNdoCJ+Ynkd6S*-naSvK5LP|W z=P9P#Jm4Z_oGlx_OvPlIi*)5G6H~3qgq#IRq$O1e(hp7>>G*@bSU)t4X!Dby?zS%XVex9NvLzh^X?TTgjtL~vYZ(mNLob&RM7wNJs3w4>6?74D4P{_~Es()G} zr1EQic(4O48a9`?^1g~vcsu`CAQ8tu`F9QLfAYNYKa`7WUDb|~<)>o|W(;LCGbS*m zGZru|XDnshz*xq(jj@vPFk^#B#;YA8QO{^*OlLg8`llId7%wy08Gm4`XZ)41fl&#U z@%S=o8QU=iGIn9qG4^5%X6(i|pm5iqu?Tm_n>j$HeF^RE|v5c{b(axwH&DWPPfw6$G znDH^j4UFZCM;K2t+8G-d1C28LP{t(2V#aNZHH-?^yI{sR#zMwzjMa=ajB342OiaOp zWm>S>RudCb(=4f(N@8Meaz4fZPWOCk8jTCsAu-XKoF(Qyl9IUb%fGv;Pn0DF?l=#JLwEdU&X$@vxt6?S%svncf!Ur1B1~~J_NiEzs30VhNs0ixeM}s_=Vs%5WgV&4Dd7x0rbZC zV*K!4aV2TN94p;S`oJ$8Nefb%aVo4nN|JR^t|e!Vs-`5#2_Fm%G0CNyYs2VY$dahg zvgbPW7SMG?sleSvQw%Igl9xpm7Y&S>X(Xjt<|JdLlOCW|ub9Ef$+zW+v?kFtPtN=I z{yBVC?|2pyWll+O#_tH-RqT+<YYDH6i z`+%wbtMyu55Q|Ecq@1km4Dno;pFy=EDc4a*vZc6QR7ziS`OMi(G?MTDnLHOy`u|X< z&Gl|us1%>GT>n;b&eCt9rzs5`Tkou{zLdp+PhUu0J=1LMeWaYJ$mR6ypyNqBqlFRR;L#f z-4ygAx=2fYYFm$V|rLgj1?Zw(8_9!BuXe@h6ufdogFAX=5pQk&9=c zp~I9(F7?Q|i7E4F<}HRE5ggqRB~=_SdTF6xz^PT-TPQ8uv>s7WQJ!?%tyjbXUa&4S zE}^BDGF-2p9@i2bz0M1N6|bc9BpixTkXwMmt0i1pDqdb*5J8*%E5X^e%uMvtbOd8A z%qE@y*g-lg%4MG^w1$8bq=wvga6Uh47{C9~I=1@x{xA8GDEYS?TXt=lwfAb@)UL#p zwDbdXaiaAf-2nfsM$sg^RQp?)@V^U6OqIx{rp?9B&FG17jwyx#wmD754dF_w= zbF-@QzslNLmnR0;yN{*y*_e7baMRC051u%h`|~&RyBWTJA(0j{kgDukdymfZ{bc^AT(oGMyMc-8)cG$G$?P;&S zbWe+6?~I>allWDk_ZzqBjc3-BR~L4g-!m0C4UHMR{^0bYFUE|!oZRE}GvV4%ALsq@ z?Gx638I`R!cl{yr%w5{gt{^n+BX`bBo|v6+*MoswF2;HE8~XLYpZdh5K6C$}&FyQ) ztqC6B)d7=lowCJhtZ{H}<5rKjygP4vabj)X50B2AvwlF>#&b*lu)G<6e&KT)b=%@^ z>^iU|XzlpeOCHM7ki9p5t?n^7F4*sbU#|I2_is1mt0l8XeECer84X=RpH#kV_wJ)R z7QXsG@~sc(repw_1wMFblujDEE(47Ij@Y|k3X?2(ADkh{oD)oxdGX&0$%8M zZSej6lgytS+xTtNoaxB#V`9@DSv+Rzi=Z*X^*S^;n(`&o)7QO@=nO{*cGvNc1VNStL;x+ zW|aLAT>tCQ(XS5ZSa$!HAFYVYSfa_kp?zUmr$x3+k1NWBcXo`j6)$_L{ph3fM{Zwv ze#MJkW20izeK()`>xrj&edGT_y1q~3_Gf-FEq#CQVYh^c_5Bl2gVJtZ zDZ2NX-`44C+rC$*%z3Y`?)b_5rttjd;Z}+b&Yzyp>{g2;MW_r=n>yV+v z&N6$|@$aTq{&C@*NX^gN9@6#756U~S^hU+yw>$${8dJg^m^gdWu_3056SK1Xx?dYI z?b~VoyMvxSRTsRxZvAHq!=mqeE2P^!y3nobPaOMW)w!eRCp=o2KK13<8Sktdbw{uJ zqn2o~?XXLqaUHU!zm@XC8!LPK{PoR^?_d7;LWgS~&lyr!ynVQ#f5omR&cE5wEA7|M zC(LWSyUo05bKC7WXFMEsS3&x7gXf>w|MqVmWu(!N?eedQg*V?mG4;oC-MHr(ra!&K z`_@~U2RHX0m$7cwoqfCPJNU-QFHSEwIP8S))U!{OzHso_@=wPH1um#>-Kx0!cdNGB z$Uobfzumefb%yr4PoK7Doa$}JyS)9;H%e#r7%}8|TgwN=KT&&6$if@h&wgk0X}_=G zNndS!;KX+k=)DN!IC3^pLDPx3v$lYb0B@^rt%b)!T5uD|<)uHEp06W@$EvHN@vL%{TJ-d>gX?QaW94uAK`%*g}Y z!zX?>hUUx^?bp9fZWT5A!kF!E4H)|FD><(m9i9C^@b=#q?jB!Vb#iV^L-|{C!wzkI zcUo1Z&kHZCQ+AtYhOhc$+^@sVOpdjmyMKT8E;m{&jT$%a^c$xZ$6WAw{?+GfO7W=6 zv^xjQ@5#@#S3N$-OAGYfJ#59zEd}lRzCG#B5l`0z1?c<5__T3OR zNss2a=i{z!{pw)K}=V-Mt4&&~S2Mfr1+8m_tT+jwDe*~p&SQ?KVg61nrt9mm}R zSL8l7@~g@A$?tqS;bw=H12@Nha^ch#?}^^c?*z@AzAD^DQFL`gu8>i2%IRE{SisbbAwI4rw#DCrw zyH|&Z3ZwP64}0Lii+$Vw>ODI8m*bmXUHD4RPc|)^x$(%rJnh{NN51e$h5w%N34>z} zZ5{IBM>}u5xnOYjm;r;2`Wv=CaVvM&nOPq`@^rf~e|0)Cw&Cm0D<@a9%;<2!t<$Zm z?-}0OvSV+~PhMTbH;8aC-q>SYdS+om|L{{6?%y!*yDwU$PRjebWBJ=-@4f5GV_&)5 zb?fV~$5!|9@3Zw}&ks6&=&`Tgkrp2uuKU+LUyX0|!eO_s_OF?AWl-U?y=%%71`Zu_ zr$y^+A{R?6CQ?>)yX>x1@Rg6&u4&uFPL@HR+pEhY$4oCZomh z6&1aHZ1L2by6otVIpc05etmxUqUdE2uYb2_+?Bt2Je&Jv&CkaVd!E{~`r}L`?b5qnO?WlitNi5` zqaL?bO_{0}aF75x~@0+X#Z|r^gyDsxWFDJhFe912@ z|M;Q!i#K=QYdCh~?MHG)cYEQptPs0z;F1nGYcuY<(0@&udBn@>+Sd3DO&HZ-``YW~ zJ|9o-y?DUc5?fhmVVM8>=R!)&$Fm;txZLX*G0+n6(jPaBhukp^@@S=KJX$Ln&pQ-N zi$KM##hr><%OJ(gD@1Ykx?6E?Wl-F`&5DQjWW~d0w&LNNp?Lb*6wlVn6wfxLil^U8 zN(;ZYlot5xRExI9m6mO5l$Li~Q(F3aYP|ddHC_QB8n5;dnpW*+Xj*m1)wBvM)_4b& zX}mk`(0F(HOyk}8Cyh_%R&G9Bg4}$%Qa?g70Q6EbEk>AScs+-4jGt1tegp2Kzj-Q$ zpADnLX9>#`*V)}!uqw$@tguU&g%O}UpCzh8OeXAOlHs4MW#&RN7o5&9-yy7QGLa$+G)F=!A8u)o+h9kN z30HYgif%7a<^PlMJ5#LYvO7}spHcvGcnx(i_yJwcaY_ff{;!=&Q#J}ygv)6WVW?q7 zPZ+CYWY1BeCqz$~0R?*8gE@pU!8Ae1#u`mLPH3c`4?-9{A+m2qc3NN5>q?9e9S6qq zm2~e@kM+&?YH&PLIF2>VI7V?z887XX)eMz3;~o!7`98+QELX97rIh=wl34#d^A{wl z;a+Bc*uE$1ssCi$v{3RI#$L}!u7>aRbklIu@Km|Ve`uDjLgoq0+^G(=$B&-z=tt#7 zNY^|NKYHG4jUU}os4VH(lFFE_IU%_bQvX9TsynKT!X>|iv_3-NVn8e0+5+iOjDFvIUjCiEMQ#BSjf1Xv6%5O z#jjD?KFjHQel7|R&T8MiT3Fjg_vFxE4w;VTI;omxg6qn^>s zn829JxSa7Z#tn?y7*8|SGseROWwwAchyP zS)f}c*)70|h0b7q04pg=R*HrC6FOkakV6di46e>(+u-1)SVf7$kB->bfoW+27m09* zr{U2xE#aR{H% zJr*IS0AsjM}7H3+{NiqOh7p-07k++6)3~EiBKsV z8BZR3Q#>;7aOQZ)j#?fv{tOWpxybm{(WT^>F8*bDY$yj=A!NE}M4ZL;-7;L6&K!rE zOmA~7HT_N9O-Lmjag1~xi+=-ArfK-Q$R$1GLjU^XZbv%wPv>wUO{r~G%os&y;onH4 zosFjvF4;haC{t0s|NlWHp!?guV9brne}9+#-NlR%&VTy193zY7|C;JA#h66>hsT5e z>Yr-}%M|B7+Kckv{8Rnw{y$#{3YRV`TK>qRD~eaHDtT=6n#W6@c=D;W>()QL;hATj zd;WzNUn+Zf<13q9-TYel>u+p%bL(4gZ~NCf?{0tZ{SPX3?ELVfk3ZQ}xqHvveftj_ ztU7e~(<4WZ9j`ud^0Uvs`0~{0GiT3z_4WC0YA#&7botvW-`Q)wulwQ0pMI{tdhM5A zf4lyB!;L>~{`nU^RjzS!_wa1d(yNuXk8kTXe%iKo__qsa-yyJLr_Nov-q}spy+_Yp zLB0C~_r0rM|BwL#o%Ylt!$yT0MjMk;V4yH(ZhFSN`I%YSIl1@4+S0aQVZovYX3m;@ zUt-d~mp{08$wLqSKb`;or{n)WE`Mp(evdhN9Cpb}m>4$+J3#T-lY|-f%JTpB^8Z(q ze_5g&_ti71`=Inm(mj{4l2Mn;?u_bwscp>b8TBcw$5_dzOO@`0jMa?lJ}hDTyt@6%fX$Gc0h&To=@W?npw(%i@ImZb;p(L>TVyNI#UpW@MwO@@^3@>C;znX zi)O1R1+-(Uc?$l?Kh2P(A`O`c=K+zH3^4*!b0oubcH95^;n7$nA7N8!DaWocs85^! zyw8nBbChBlKhhW}9o&L+r-7*z)-<(#3MA9VGvrs$vjFG9@Ze-6^6hTtr< zfATMP^3nXL>X%Zl#{SRcQ6Ne|)*%W(tpjQZw~wFVbJjts$28)k-Gl1hLYfI(c$+lc zK0NZ9iZJNez={!J2tuQBk>l1mqOH)lTGp+mS4vH(ykHj0@={qe-RI~!pG>CnUvAb%Ano`?91MZXbTr5=cWjsYT=<>=gVh_&*F!USnCJR3>spnx3=% zo=){TsCz%};CgJzvy8<)D^qSpzG*^yw3q`T+1Si{m^tUrBZ@d^ZiwA!Q?08Wb^&0s zBc6llaM6P)ztf#$ZSbRwx;E_D%C^Q~e+ev)c&iT0y;1lye@7S-5u1#?N@V+not`f4 zv`Nk~0h=7ANB8`P6#$OtFV6d90a$D}(Y&W5@~ zherHWl$v~*9x5j~?9AOIRqB~jIndF-oaQs>@Ulw(G#^NZmO0H~(hBQ0Bv!8=2D@Ivr-_)b{9zV?IJsC4sqt zc@p!{%+r}0nddS$F)v^q!Mu>UX!vj|W=?I8j#B3LNUCgL9?iUr`8ej~%*Qj|#$0Xx z70f5Hypp-vUsN%l#`0?BG`~;BY36vlsW@twt1~rr<`1*Hp1FoQfClDn%)J)K>+jAy zkhuqQwO{jO9?bF<%tM*CWNv2e#XNyIeQt^lb-vSwc{n0aUB zq0GB5H#6_bJc0S0%+r~7V_v{q$Gn(%cjg{%!#s?6J#zzdWs%J9Xy#hxM&>%^CgytP5zLLuBbg^Kk7AzAd@S<< z=Jzl!W**Ia1M_jr%bAa7Uco$uc@^^s%uh3)$lT66j(G#~$;`bTkolj+JdpWx=E2PG zWgg1h!raV!KJx_TnatCfFJ)f9T*Cv9V&)#q)o~aMdBw4T<*k^PGxug*!Q7X56>}}~ z)6D&u+nKjx-oU&)bFT+wd30hP$h<3aJ@fv|jm*QCConfKPiG#%yny+9=B3OvJfJFL z?!kN;^OnpjnYUtI&D@)L4Rc@S^~^glR~F0sg)t9gKA(9oa}5vBLYaFoH#4VKCDW0> zycP3w=HAQ;nENs>X5NYU2IgVR%b9Cf zVa)59Yjkuqa6BaQ*GeatqGj&QT*o|&xt_VEm((|^{+Y+A{+TDK{+Z{h{)46eLe)R> zQq@26GF897)ZeD+Gp|(jnOCd&dZ}Nd>NBrb^_eRV%lvDGN_{Q!R?KzGy_xG({ZOfI zROQU$RQV_=Pg3Q~b5*%f$_rIF^HP;ZN_m;e&600Zd5q+hDvy)Anz_ef$!nN*VqVW& zt~x51H=tETJoDnYl@9fJ)yyklshAz1gXaGv%8@3`F4CdCJA~%B=%9H%iFpFkp+SdS zm4*Ek>(TrR9rV0R2R#?lkm`3mqBA5gqg{DmwBV`H|_$6sxLaMqbX9`=@y$I_9(fd=4)kvsrZ5I9w~oPxDB0%;k7!c7_g`xuRn}N{)^!M|ntj z7Q4^o{82g2haSy)QAyAok}N6mP5Gg@wRo&I%Y*7ih%!b|pMR*HgeXytbyBJyG{2!f zk5D}c!D_Vn{6q85#G^PoIV*jSWB!BcO9)n})#s@>BL1-sIn|#KNV&R;dK7{++^HNM z)hCk2J2=g%MPr>_9#o$~F#DqV&&Cxeu0DTJ{R+WsjVh;lM){r0{;9r&fT{Xa?})4M zQ~jg#sQOe7sr=OZQ+*6UX{zDpqqIq`mIu|(5TuryH)5yorZ~c*`bz0j!=rjj`Qz>Z zsiF9$x?C?RuQ9?7jIu!Wn&MN-m*%wT`l<0#Jr6NL+vC4zSMk?UI=QDTJH)(xj6X)!oQPK`*PK% zb{2wKs^&}1a*uUfH)?m34>jFcBA>C2eA3LYYrT;3y0MOOq;?vD)T;HI+ADFjT~NCv zuEtO8m#$0ma-jC?T7ObRzMbi!cJ7)^YVWR`+I>?l+lQJj>IYoqS>pOT!>9hiRi580 zJnAp#x~So$ihSOK8YK_vM_jL?>`$B_%6^4v+zjDC_AeBotYP;fKU6Yf_{wC9_UpW^ zWS33XK`m$5A4MyZIDXlmL^#@uwA*l|kM2VhTLSCL{$z~fy3l=x`YW})$^OA9&ldT2 z?H@=#&ZVAE`NhMfY5kV%+S!k}wu1=K*Jg?o$$I8&|FWKqag?`fy>a!YmRl6kPKPX) zC`bL1_ieMIJ;`!1Inpi5$>iv_WH~weaam3gj`o)?>WR#!YdU25o%vML6~`qk(-q@r zH!@xEj&#a&#kiz{>YFoOnXY>r=~mOFUN70-j&Vt67RC_HmMi0rbkrXif2^Y)P~&&@ zhZMiFzRCC_9Q~b)-HH6!e7_^EgyGYN{4!7WjbOV>6GOk?Wl(`o;ZhG#uLj| zm-=n0XSa`sdH}VC%6O(Y>W_@a*`8!Pk&gVkhTHVIW{IAlY5FIkCCNjEJI;kuIZSip zhw@J@YR!=8n&FU3{}GOUL@j6azDoY-N;LJa_9Ci%))>ciRm)H9Z)JSr9Ql;-O>xvC zsc&*zSE(Q4k{*ivUa<~CIhOirYg60Xco#Y8$2s~DH9zWoUWRXS$v>6OR7bmz@-fZ) z$2F5ra?}qQzS?W5>5F&Vx24?dQV%G;2`=}2;&G1rJIDF*PEYm1^-$-H)qdZ|^Tz#I z$+$*7H_6eqa~(#*&&JfS?0OS|HZtJc^UIk=8rQkV}6|ZHs)V2uVj9bc{TIb znb$Di!MvXNKIY2fGCyB2*D_bv33SYluw2i47jq-?D(1mlUVWLzvHUsa>CERdSJ!R2 zGcRO$sw(GoI}P(vmaF5yGUiKIzK!_@%++;3b)B$^2L~#mtW~-@yD$=H<)}F|T0$B=aifJDHzmzMr|B`F7?F%)eyr z^@J?1YUY8=)$wvL^GcS7GXIRZp7Ym=xtZlBm?tp*nt3|&Q_KsPA7)<6{5|FynD1s@ z&ir%c70fp=uVTKH`Dy0gFt;;*ig^R`8s=V4%JTS-c_8y+%!8R?f+9CI`C^UM>N zpJATP`~Y)xowO(O0+w5t>$!ca>!QUh&t$ol<-M72V0k+8P`*Ci%*$CG!#s}t4`5!w z@_gn-_V2^IisfsWpJx6lb3605m^Uz2@9$nu$?|=l<$=sUVqVSZQP*9ASzgR?bzO8g z^H7$n>yTznuP@8ZELWd15;**UEKgv$m3cY)4`iOs^0%26Fjt=&;#j{m%Zph)k-3iJ z3ueB7<@1=U>+Ic_m$N*Xc>;&uhIs|cc^b}Pr{Kr(DweNfewz6t=II=smbsnf>T|c5 z(;LF_29~RL?R1t8V!78^c|8l5Z)5pL=7B6<#ypqfZ_8X=XCBQwnDtjO4`m*&`seTm zGdHtb&XU2-gylgjPhk06<_gPuF;8c?oJGUyTbQq5c_{1mV_wYiEan@S7csA5{cz^x zEPs@F1xr4daI?2~F4`e=t zc`);*nHREuf99bqU(LMKCc_)X+|2Tam>0AEcFYr4K27z{@&M-PET7K2fcXaI#mpaL zzJd8m%*&aVt8xy%J@X2dConH#c?afIESIwiu%ofb{EcGyX_i02+|K+p<_*jjF!x$7 z{Xfrq8^_m?c_7PYGOuTO59YxvPh%d+{1fJ8=6jhZFyF>Jop}ZG0_LAGFJ^v_c?HMc ziTMVW&tk65>tAAC&hkCXE17@H{512;s(3}GJ3 z@&}mLu-wQzl;x|Km$5vQxtZmK%*$Cmf_VbVmol&7^mbvM&hq=17ckFeUd{SlnHRG> zQRN)p-OM+ze6h;e?n=kJoaHIZPjmQpGOu8H67xb1e<<@RmcPinhWUfc16jWh^9Gh@ zF!$OZuTPGu&)l7PF!PJdLz$N{H#2{kc>?oyn5Q#;gLwh-vCNB^=Q5Y`vt+3)5Ba>C zjOS`so`P9kSDwbJ9_sV9g;%Fkp24d-Dxb%zDk`7Pt0yYY#B9H7_|6?{GCefs;VRF; zEQBk+znPq7OI_tO+v&>H*={xdMK1ZJSx-4DnCdbgC|%^=Ovu#`Xk97oDqS z9j#l@`~%r@%R|mfr62^d)pdp=*K25YKpt|vCe=~Sl$H>bvRY1Z{U!~w-SUw0WfYz( zckUaMa{B8kCoBD?T)hvf^TAoX>Lk~L<~n8{by{@BmGkD z%#Y+5j`T@B&vBh4&u$ieuA^Q`xw;Cc#z)rAuIY2ubE*G;qaMn68LDlnKF!0BwYDoy z#Wicno6DQ}CmU^7eYy9OTx5LC>n-_Q$Musu-BE8O&w}l^Yk1j?>nG*v>Z2MyeKW#U zU+!Hc7gb-ax02KBxU0U+QLm+ZflGKa-)?c?wEjU_@{sFM8IJOl>xb&9vs`DTuMDZ> zBj;7sRbGe56nBUvcGG2tgilJ;a$lxnARJmva5mxvy5PL%GT+ zd}_Dykn`K>sgPf`YR8)KCD_FNP7gj zK5BW$^&-_uf#ejwt3HMA><{F6o-;jiy;-$tp!af6eNS1m4vjDkh{QS{^WX}Gd*%W$Ju@)cjiZO`m6eqU)EZ9mki$)-U{s;_EliPO3-?fX&FLunul*#pY;T4#C79c8YZ*8f#26|(1`+&k-! zvPek!@pxgkSY?G)Kgtp+l_(GiS$RXt$oiyzOVaB@F?dp=>( zx*b-+vIT1&6c{^nDPf$~bIS=U@4vNzu(0&L62kg74PObY{^ON$!QYtoI$`ze`ZoxX z4uQ(EJGT%oUH#CTgxW7mTM5hV_jrpiH|>DHZEvl3n|Nh#FLC|rdoLGQdh(J$egBAm zk=!_Rqrjv(KEQ4 z>PJ%dR|FahZ%JG`<9*VvpS@k+wy)cKKwN2;Auvg^SD=1#mkQzTzCfTp@VG$Zl|DPj zy)>&(VA)%z1)_X*l6zTVvB2EwZv<}pbJ&OEUVVABK>flEe`18d zq>nQN7W%FisD$qlsGVpRSU;xiZqhFc94WA{Iz?dVyb^)h@7@)tAN!T$#U6V|zvj*U z0;{)A5m^7yVu6L(n*?st9T8ai{x1S?9ruzy-I_5Ht1|=^UN04xq^S^CQ-5Bde!u5F zk&lP^3#{%EFL~J_iLJ^6DhUS!>L2?-pmFmZqJPqF4iy;p*nI*^6P61!`n(}9chhmn zd;KP`dR3gYL10{sPqm1DL1%$_kA4ERD@O`6dX5uVQ!qnd{iSq) zxnm0i7M86LsJ*scpgwG~!1@Q@6BxIBpTMNcpGmpzC4rUwt_jpeXiiZ4#>rZVGr9@X zCkzm%Obi!T9UddFtk+C|x$g4>#+@w?XncKzz@*&u0?P)zDiHmHz{>S|1a32&5Lkcy z8-ckCe-fDF^_Rf1Cwxv)dgA;$3Cvv`EKsWq7pR{fC9wKfyg;R2isTRH3N#*lNMMrx zYJs}R&j~DB`MSWG9Un-%xL@ElkIx0hb-O6Aa&Wys-RN5ab0dAl_-oskP6BI21Pj#O zHB4ZW@fp%Bj>mQH^!Uj8o$$|F4;Yf+_pk8jCm*u6UUw`!BP%5=@|VxTryGX7`u)AX zhWqTgFe*J_UwGSB4M*SFUv)%@~( zc-=3Z?(}(}mEoXUW$wyXD#Iro#LCP7t)Wf!(Vvu=0fx8VbI z;`c4aRgL#IZ2jov7tcntH{A98@7rIS)79|vpTCFJymLLgxIXv5l-ggy-#t9CRjWJ? z!)vqqeD`>6SHpKZ?qB%gH{A?LExcdb)yK;q)6w2A^S*i7>%&_Z#-}X4>Ob!e!^o7v zWA7MR8P2Z_s6C-?XBbrWxK=yAtHE?_r_b{%+8g?PwD~@bS7UhQ+yh6$-|uVazh&P? z{oVQ*A}wDpsqSzi{Ket!)02z=hNR99tSvd=Yk27O#eFPWI~ZP1d})7daa+T(>J~}+ z+IBEBcnumgpw&Rb$P4{0?Qhq?@a)9=zh3*Wx1nfx&3ljk`d9cmpV))X4eV*~_g!SR z=idm=PB`H8(UCh18^%q(Q5(?RP%&uG3}c6ZhPy-4f=Z5eHWdC?(zQIXo8j`AzBgjW z^)jsW?QvSS;SNKqOCC>|eeN>cvTWWmEw-B>b<(horM}$_!}`p)Rp8eUez;3jQ1N7~ zVV*T>N&LE+a9KWqhEY#N{ZPKGh2iyczjbc7S8E9PVruU{E_N{Ny8PCzA7->RyfQld z?7)()hT+3U{`GZ5FN4P&ZzVsp%-`^mUv2b*kM%cno$+)){r7zh!yX)zz4?ufhN&&` zT20FvV3_je1Ah+b(%Ue3WAgESbNU+eefG7jiU>3e`}AI)57TspO`1N-eTRA*zFhS9 zdsEkRGCcdjeb09fsSTg^*Tb!^q>eVUOPzG4&2s|`zIUf|EbZCXu=Kuy(BhoIhPO9w ziJ3Pu%+O`Qny=>ds1N_K)5t-m#*H#`Se?Jvo)%yToU`ic504Kq%vrGTw-=ubH&m3Z zt`9ShF!WA%eKX}I+0`N;>`gd5&Yc(mro zrh$eRy(>>{TpngPF>}wgZLjJK&s?9`Bk9Xvk1&|`O>4;6 zJ^N@kv z4ZgpPIJ0%%aKoR4_opoH5Nt?}zHl&Q()Z!A{tYq|FL=3M?5N>}rQx+Tul`@OodD+7a)ilz+NNv9u-8a5gRIM z?AT-diuDw}SiXu!@9_UTTkiJm_6q)9|H#|Doqgt+XZq~y?CkF&Ezb<>>3^?fq-DYR zKYFk4Z?@c4`s!b(7eEX$*pj`--baicB5pa0xF{)&N??a5j9fAH}Hi}lOb3fi;rmR~Ni4SnmDQI?-a zy*TveWg{&g)UMn-b8&{nU)ANm6K4*vY`wjq{zKdTiBo1ZetFq~BNJUeUzEA;v@=Uq|4@=X32Ok zUHC0yoLY~@S*F>NuXwbBWEog`+qO5d&$gtQb36N=9dD`V^UW1``I2Rv_sk1ZD$cU} zl=8E4Y2?neJ11tJJ^C7d-ek*#Q}(U9cgIM}HrupSm!C7# zGNvK_*St<6EPQ6Sr1$i_)2^V8<+iDxzU6o~ z$MVVPTXMU;#mCH zvGZF_vhjNud1daqo!sA;Evc{lo?iVyfu+ab zt+!^5o@mKiS=Mdf`-3c(dcT`)xo4PV)Vf^fg9naGJT-Zzu&!{R<@@>-%g=dgh9&j4 zieqNy49h7yw!EJ7ORi;g-<>N*Rh?{EzUKRDc6K<+vd{D8$=L^UEYap?vU~&Z-*L&` z;RwIH^|+LNh(F%ri$az#d*^na1`J?b#TGHla5Qt1D5;W z>~LHXZklx9{XdRLt$*?7I(&Rg`X7H~S>w~kq=hf}pE&iFW70P3oojo0k4fJD@(<3M zdrYe9{X@eoXC0IJ%YQifIQ|`zuDj~M)^GlMRQj-A^AG&Pqtf>)US4c_`lxi><^2cm zT76W?9=);j#U)3j4W9bOC+8oP`hAs)e@CU>=O1}w^su9nx3%`>7djr5-g~29e(kO! zQrf-ZwR65bBK`IezoX{$BhoFuT-g4(HAkep>kgkc^!g)G-}f)Zza!FBg`GaXZQc>d zf8cF?vjq3%F=xIo^oZnZT_JYpd_=nb#;bO$+Iv{qpLE-`qkcRrU1GV$arB+TQrjzU zc0c&!VQKL3j8Xk>KP+8;+IJJHFFh=^-|_m^Umb^~nSFkLe#MN#(x*w^PMdnhVQEWa zb-rEjsB2c#cg->~e?vkyqkYgY1UBXM2+ zJ0K-x*cy4#0V(Hlezg6b{ZhY~=gm6l*ZtC-_Lt_Cwe6Rd^D(QRdwsw3_Ful_K~L?M zu0Qj|MUUURU)sE8>58K3_Dh}KyQgb%!+z=6_1W8gSh!!RspoHfIA^~!xz`2W&nNAd zZakLzu0MOfwCShGmk%GZUrJeiU(M2P`z7~^wD~?^ztqdNtEX+(KI#24@)zy@WuIi< zykh91pYM};RlnJ@`ptdP#NLmj7CyUAYVwqPe)dEAq}H};&YE%yu2-$-S+!!H)O#Tx z_u$3*q=SQIUG$fIpY+JI30KZ6-X~QKOgndT-ahH4*6TLTIc=YmQ_hzh8oW=s?demi zp6k9(O6ob!wxaz$>Bz|=vMcxPm455dw(nrTMkWm zZ__J#rPlNKqV`Yjl^*|i@J{>vd!@hUtuTLa(_YEeejcCGxK}!Bk@f7qFWxKtTD<&~ zJ1h1|%6#Zv>DbCwx1Tg+uk^nzd#0wGxmP-;{bO^EkK8NuIPmMA9s2E+-hHR*FN3<^ z+{*E9uXJzGkL5S--Xopa{*-mT|L&2BM*r}7{dgF(Cq+f1+^he8Ud!+N< zC~p4dnLW}pzWAxjAKoJ^Y}>H)%-i=!jZ^uc_Sfu@F6sXClwX$Zkrvp_&Hl7zk95yx zd*A=awnzGK@%z7SDcK_(-cs&3FnNzOea92#VQ1};t{(99NefQiBh5PZ!l5r@?UB06 zfAH_<{Q1fbknh$a@x!~N4$rL3yLrcMNm##a)97D!OYhY`e#@4xcT2xLamkw7KiDnJ z9x-@}?X}%f_X*E-oAB&zY0&0nEmd5eF}m;FyQQ0w7koMBMqD4iqTBctyQLkI zKKT6n`rXph4nOU_(YaeXF!I5&PcPUly}5MM;l9PYrODlVPc}^6Ep6B|_xx>V?UoMp zyXf!vr|g#AFpZkFb?|QK#sB>=Z%M{(sp)ew&+N2YvI*~u+{|}NRi`}k+7x^S1YVaY9*-CwdxT0Nu?|8_~|+j`ykl4Y0l^QL>2&dJ^-jZAMI#7FLu zmL49s;-vw*r0uh(Ha7IwC2eV4*l|kQE-5{Wbnt+wI>e6>Yry>^*<&l)M|I z3IF`MQ+jRMn1y@4-6{23@$gegpY4>+_4Pe6=-r*th`%T2OnY^wwDpx;3zt8;Q@Z1l z-u9Q*?UWwq>dWL0?3C_%_=d)k+jdIdk7`=ga@|fTJ!3`Jj1@bjE57NIdEJtoQbS$d zwMn%*B|i3u^~TDbQm0|{W&O|JDShzu(MMjMwNsk#Nowb^sXL`1Q?hUDEbWxKZay9V zc1lmS{(bHBqjyTDnJ@Tl-r$|m(5dS(&AoA5{@W=vn)vI7JK)@Z%;Z1jmmWUpxkG>N z^-E3nZC|waFTd2)wsLmTHotVo{^}y+K3cm156TaIt^WqQu(#5&gU$w5) zFKxd6q`$VmUk#;EO*{WOn(KkbO;*sg`An&Fa*TVdV zawK2cU*oAWFQnh}9D<&Qo&Bo7&R$Pvn5xZCgmqZQX$)oXB4 z{=y$-Zn#i|HjT<*<(-e%nAa zdo_Oyx{dOM!qflRAg8(L)9o5vn0k5A*{r)3&lY`rzeQNT!*8q3zg{=}mgP5U79sAs z-*laHXn%TpNw5A-A8rf$*q~>8m@d6M`Ch8K-h|)bVUs-R(sb*?C;j?AO-6QW6PXX! zH-^dR<;nK}-F4H;#^E#Q(sb*?C;j?AO@`ct!;cO6#)D{ol&VX0*YBe6i7>sIf6}RG z)7wGe=>PO~SmEb)TyG7NiI9IKsKccDO>(3|`_qR@di8($aNQJt=oufTOD`|SUw8d3 z{Ek)z$RFv{Le$$q;pqSLb~ID`alJK6MlUb>Mn9_kW#G3_9*{0gw?2H*ul*I56qM{7 zoqY1}yPAp~xoqtlue`nDQ}R=B*8K8%XLV&=wSBQ;ex280ufbRBtMSEqC-$!`TjH!8 zm#s+5uk%!>0(Q4^zH+4)U#`bTn1k-)vd6e9%au?+!QnmC0PRR0nXSo35;7liRmXiR?*AAI0xDa$~I`^J8EUmrj-_TNvDC3iZr&w{b*b#Yy5PLa*fh5y54q9~kWXUW$3mWDN|PZ2ImirbFR=EN zNz6Ziu>r`GZOBg~O~`rX!1gSoyt^;m-PLA-{qH=8>qCXW+A6J048$8hPy|?ME6Tg( zh+`Alm+?5-)fI1d;YfZnQw6f$s_D&4=PU*BrSptoLO=uQB%Tb@>sR$IoTTXM-d&L8 z^m<88Cgf6!lgy+y$3SnpH$&~sf*!K>2~}@_fnL(Xv9WDhWeQ6h+rc-`-Hy4D*6qE? zEN3#l*$2i$ubj@v2Rg@xQCR`GL3jAM-cQ%Tsci7JG}hmn$}*v!@;s?n@RR*j2$$0C zuDcoQbq*BaFS85E3(5^$Z^HEp&QRQWrzF;?IK|(=*S<}(3g&@SX1tw|@2E66jFo|J zReSX3U9mxK_v{qX=_Z{U)i7I-j`Y`xBjraMj^yG~-7_S*${4M5qrSeY&MqHx96`TA ze~p<+9F%gKhBBGTx_D4-^!3i9Quv^Oll+`g48$CJ$G_ zpm4NuvkiW7$U*yqy_CO6Xl*Dh`>XNMmmAWP*)NIptL(@!veQ_qCrM5Vvw{ACw?p-t zp$}Qj3YAuh&Cu&FR?0i+G15QJK>wOB{WewqAyxln2Kq^lTs~1AZff6D0G0jrB!d_8pty@9FE_ zmfnhX2b(GPELK_&xh`OH-5>&LiCcg}mHupXs-S&!`AtOxJOI{P}h(`>3=q^-%$ zSRS}bPAAwF;fDJyoeanI?Ch^t8vmTN^9#0tSxGGGozGahw~Zy2evJh^j4dbLMO-QV z+dh@g$CNO31Gypp=z8!RJvsa{_z&?*=@jP1Gt*dRX%oxLzL{n6YgoG92b`KxmDev=ZK$?Uy5ItK8}rjyOnlk-LgBeZk{gw zPCl*Q>xgfNGIs48lxa1LkaH>rD5vJ-c;^IYQ0GOlSQ|yR!b-Cf0vL zZ-0ibXKM=e>x0l%J*2cjcdlY=&=vTu9{LeFa)Ha`jbr2UPN*-PJW1Bf%p`qVFSj*K z;}9qO-G8NgP{s@CXjgkAvmRcQL)6V4C_g>McJ)zMBz^X^=(j*Q!lJ*LY-66Acb|pIDpF*D``eQ_2(&_IO~)bOw*U| zVr(<`2x0yYN1mR9I%7guU0C{tG%N3!%6gV|WIfS#_Z%B6*@{gmkb_-g6`NAfFJ*ku z-K@pu`umZFU@LUfk#`VThm}{mO)Z!Mdlt6BQ*SzEV%&l9 zm06jL_MrpX2b9gRJ$#CL7r1wZdndSeWSOgb_%a72vq8oE{e6AC+cH{vx_j7^w4^b> zoiCtmdJ$!|C+J8X&8IrUdMy<7|@Xo z@OEVb&`%FQy%-Rz7gog{)Hx4Y|CEC2k=^M@}q5QOKnYdcd$`AOJ%^82HWIvl1%qQZf?SO1^I14 z`)nH9QN3^Mggih#AkE}FklWGDXh%EII4&?k;P{9~M}*(elkCnM&>sC*4eP|KSdv=_ z%Z$n|*Hx;go=|IeM}hpAIX>iJLSCCZ9o=1%lzV0^-M=Z|>@V<^mhMz`wxP^;QD)F~ zs%?^=xQCL{(JHq`tI-EjyW|nn`Vv}y zZ!hO;EeP0c5Vz^LUJ*y;Z#!rG;>diB>sxg)N;&S1a@-B&IGy#~pex6HMbwpyKtI}o zbcoXT_?32}Gs-N=E%LYfYP1`2AKe3OQFrvw-OwJTvwmMFeYBzneHZRmGLhGEpWs0o zMExV$%pTCw1A2O_?&_1%ZftwAS~df1!tM2(JpuBy{0_^%7ngAM37D^5=Y4TsW3%7I*GJIHfA%D7Fb zzgl@3)QGYV>NR`1(|i%qgwjK)d$R3RI(Mz$Z2U^jZc=4m!1-jHKc?B5%vo}ja{dYA zI<4aDO)Z?Tc;9ygXCDI14kd58gys#^4T}GU(0*FUvrfn}jIWR{{ZI${6|3W_lhFTV zpUJv$GYi&UrJZr3k|=4Sq>q*+wRdv%G5B7!HS;7I4|YX6W@0@(!FG(&n&tvBM|D6R z>|wpLceCEy&(hR3(~Wl>mEeDHFLH{OHYIP*Yg3+MuUJ}xu5Eae#*qUe39nPJh{{C0Vq3tP`Q8E*D9!Sn1}dpV8q%rlbEZgxPsiR0MrHrRA}h)t(O+62>BSL3l+ ztCokazR%fxn|1Z5JLWRc_G@#R%6(8O>MibtU{jCcu5y1saqIgY`c@FAgLL2S#l1&m zJJfLz@`7CdjPkPUN1RoIKsoP|iuCQs`k+tjGq#t%hp$^(*VZn9_F(K6oRxzNb$(0f z_j<8j8$$cN%;BjhUl~-#LwaYezGt4C!upgp34O9}68iA#1Z{4CuIF?|89f#Kd+8m* zfb83Z0sJN*Sc=vD9WuO6GV6nJTp#r7ebBG>8Qa~;2dA;Y7*h|!AB?sj}X+u-xc|4vfveYU(^T744aq{;2ov3@eba|Vi z%u#p6M}c(rmTetdQv>(Iryb<%A`mDKRL-+|vdr){FB9XL^3ZlL73~A>n!>u4htxvl z-jmWA4}?;^G(Q15^y6uaTXBy@_dbF8^QS1V-C!m1k&g271_AnMo@I4AbLLRoe^jBL z;Qd*Ok4C2L&F$1Sqm9yx@;x1MW@#czt7^|Wyv10GoZlEDBmL<)f>vg>V@&rqps?sD z&v76w^hfnEC2YQd+%84?(L%rv9Xa(qd63ucWiaO|+dPQ0D1XBnmE-1b5s|Pza`C}E zc}wUu6WnP{0sYZ@FC8<$JD1U~&_8YMgI;dP<)Je28J;cCpI+{F13As!rC~OaU(#?F z+|3}|X1Y#)v<86w^x@2p5soiPI1JC?jM7F{j{niKcKXwY)BOh7Ddc}ze5OKQ_#e60 z;BFKbdX`Rqdi|Rom7NU#r^(&Hm1po<;hoqB{RMyGdG$f08|GzmfOe#LS`#uh3z$I; zumT%!19EEwntKy|H-i?C1zJHH@Bx+yT?Bq-;b;aqzzS@@4H`icXa+5y6|{jI_#s_p z@(ZkhWPQK{+$2LsxHkdv*982a1+dxt^*V(^*RybJf@~{}Z8(yxJ{(D36Y!H8FkxdrA7pGG2j^}av*6YS z88gnUbObiISxE-J-JlWYjW}<@u@&c}gY>uHoNOS!q^}j{ZJG>S1E#(R1LOcJps;K> zx(WDc!jW{h&^6!#eqibce+1-PC=C40!O@CiBe{Vlz~E+so15H07S5YM8!&-p&w~)uZWM2_*+jZ=G~?Ka zqnoZ%9^rQr$bx$_jyX8C68LQcZ6uGhp!gFMo>i46e^#8k5sn$h7RYA7jdV2Pob)%5 z4LE0Tr?5zGGiU=g`i;1_am)cGHLMo+A^RKQMz*x!ya`7$&doUbaBRe}1xM0h#cw({ z;WxQ80kXA)WZ~BY`$>Nr&RcP0B!l06TqFHCKu!xBvw#`2!M_=DjjBB9Cmp2IkE034 zEF7C4*NP*_liy}Qy2*Y&xd95_2N@g4!MPj9EV#+}2w5}StaJn{OG)1x{B{FM`&JyY z;NA#-6t^~Df*XaK19yg_6-SC|6Wm*HPO>JXKk06Sn+?~=HaCu>+X}J>T&KAEA>T^Z zakPOP&f564yBLsp9igPaxTWGmU+f}<63S@=!Y$enc9 zNC$qK0QqeMbiGOEM%PKc39^)ieq49sn1y_3fgAN3tvFJDLjEZJq@xv(KhjJ3NjK>- z11+8{_?-ngAD}YWhI2PC;k+5x;MRz6C=F~lZzFrj9bq-%Xa+vGQGIKLzZ{(VaZdeS z3w~!o&Pvx%UQDD5M<0$hT({yVmn9tCIH&qd{>fgl)dZRVgPR#gO5+?H{W!9dba8HPJa8aV-Z&O4AnnZX|y=nsBs{JASv}w;MF7 z@>C{T=p2{<$x|2)fGp4o{BX;{b<#~Xx^Zj)K41k|fNX8XkM@64!whxIq(W20maKg=@eH zY@ivm0Uuza;Rf8G2{eNil~x?vfazq|39O(Av;xx@=ma*EMjYEf)+ulYR^SFrparx6 zKcKajv`pIy+@J}xfHu&$80VmwSPFT#SxPoD;4QEP_(3N@WMjd3U^!R=J_Gwe zZ&73uKp9vG)_@Pd5pZ%lk<9>Za65PbYz3+9MRq!v4H`iU_!1lfgOfyd4!8*10^SCD zKo6{em&gJ_dV17Um6$!4mK=_!t}k zgE2og9V`QD!DjFm=+RMRXMr-X0;~sL1KtVg0vzBz@D<>lMV1Z9!D{d}*b7E?5m^Pe z1+;>lV6X{k3T^;pqHkgi}A zcmn(fq-Kij3{VMf0h_^5FuXVN23!GN0)K$+eMFW6YQW>54eSS5cwSfpmVmoJEBFoc z?uR&n8qfq@1V4ce{Y5q&RD#vuZLkyc9U!u4;8O4i*bM#w-LpiN2bO{dKr8qi^csk? z0hfSv;9JmPkjPF0HgFqw59|X&2a9YDxCU$lzk{AbP-ejra4&cb{07obLYW0l@DTVJ zqz;8%FdtkGUIg1g$6+XApbXpuo&leM17O5(lqYaAcoqB(I*&lT19s2^UIgER4kJZ2 z3Y-Tn2akY{z&)r=UK9bHP>M3Gf|AF~c2P2v&fn!53g3=zS{61Goq@ffv9wkUmyq7H|=` z3%n1GfT5>}Y!+Aw?gO8Lf56~ulvi*Ccp3Z#vQ9@E1a1JEz*f*@9O4CB;9>9~H~G5fcynlgIB>{ zV8A4#Gq?mi0lovNlMyah3Z4Tyz{n{mv)}>nG1w0#PDL97UIT~0nS~;20MCQ(LDDpl zoeJ#W2JkZY1(?o(U$6+=2R;M`!Js0%QwlBx4};BM9~d+pHiBhf4fq%w0z+q@jRLE{ zli({L%tSo}mEcbB0XPnZSyA@D<=}DfIXDc4&qCb-jo=CJGw4!`_f~)lJP1Ajd%?gG zv`?TBYyvw#&r*?125xW{*a*G_$3g$uNE1*3E(W)P4d8DuY7YE>jo?qvXD;FhR)VL& zA7I2hk$J#l;Adbu7wsVMf(_trF!Vf>OV9+~28X~I=ZkDHcnRzS+4GSH;BN3G=x_nr z0MH0t2D`wh3sE+}wO|w214fmhJpwm_4?!mz;t!UB$H5Pvi(O>-zzZG(AAlp^)N+w6 z1P_3Z0jofsgJobN*a3!Bip&P?0pEj62ijL~ANUL$1*a^4?O-){1MCI^7ouMQOTfe6 zGr+5mUZ4bA37!W(0MUu~gEDY6cmezdGA}~?1XqDg;J={DB9t944_ptP1K)$>YPbU@ zxDIRtJHXHyk)02&1J8r+L5EtjePB6w3hV%*Tquv=VekXMDpEEYl!D8_TF?f>i&4+O zEO04k0l$K79;7>H0PDe*K&*ofa2~iBYz8|(UoYA)a0%E5c7UvU_y?DOr@;@P`(lwz z1s8)g-~(_ROjv?)37!Ny!O*416L2SJ144ty#sNEM0xyBBAZZ!W2wVuR1P_6C!FFJ} z1o;T&fMwub@EX_(QkJ8C0dqhjco=*D_JbamA}(MFSO>lWyFmJ77(akAa3xp^J_Y;1 z(959gFu=LsGVmz)82ke=SD^la8gL8P1h#{;mGA@Rf)!vr_!8^|eO4il zfel;^9t4}g-=O0a=y$*za3y#gdJfd||LUIssa<6!tzX#2n_unuene}OJn zBd(wvG=W#Z4lwc>lyz_gcoJ*{8P`GwSPmWoKZB0fp|1dS;6d;OXm>s02Fk%b;2R*^ zfU!L|AFKdRf$hL_BgQwt1?~e|K>M4JpI{ES46Fekf*m09X4Flv2;2ak2irkf6Uqme z1r~#Q!5iRLkai2&3os9?1do9a!EVrJHNpora4C2Kd;{9uit-Oi!6jfF_!R6118;*3 zzzOaEo4|MAC>VA-(haNv&x0-CFc@(M#)6;*+zUPhdqKZDMV1Sk;AZeL*bX}1B{B)P z!5Z)l;CG|0gIS;++z(y{zXCrEJ?eHFySm4E_ea??v4LR^SDpuTJxgL8@J@CLOJ!-SBkRPlDx8@Z){wGp*lVT->&bet43^1yvp%dZ>xXH%0W6CR z#8l#7HiVtThO%LJS9t^*$wpz?<7765ox;rQR7@kB#*vm7XR!%*w|645 zFp1@`T$ac3SpmL?JsDqvpNcP2Ps7*hi||Ep+Aqh-X5rh^CD=7*Hoj>+m(646;%nUJ zv-#`-b|EWcHfCq#tb);(rWUY;`2K_wQ%Q^PwGaCCiwkQbFUB`A>hS%HdVHZ{3BG64 zfUjs=!j@xF;xcwQYh)|fO16q!!LDRiv8&lN>{@mmyPn;^Ze%yHn^_aPg{@|{vfJ40 z><)G(yNlh;?qT<```G>L0rntkW)HE4*&}QXTgx70>)2y#J$sxz!JcGKv8UNHtc5+x zHn8W|^XvuoB72E#WG}N<*sE+4dyT!$-e9flP4*Uho4v!{#e4eiv(4-S_96R-eat?= zd-b2OHugFDf_=%pVqddw*nil!YzzC2eb0ViKeC_L&&wv+8*yV)MLm+fQw*#UNt9b$*s5q6XvW5<#G9LsM+ES+i3ldupm znWyknEMV@4H@Q0VF5JYs@^s#fcjtJ6jrZaiJd^k4eRyBqkN3yw@hm=&58{LQ5PlLL z%7^jcd;}lKNAc18WIl$U!p;0tK9--xv-#_ym47pU5p-;yFB*=ka`A zz$fv^d--Jg%HQN~@wfRq{9XPYf1hvWAMg+PNBm>{3ICLT#@qPk{0sgi|B8Rj zzv2Jk-|{W|JN`ZYf&a*V;y-gA|Aqg`xAJZLzkECYjsMR7;D7S}@xS=rd!JRh(bG|y^thy5R!!yAyr5dItrbH&O#T# zBy<(hg>FK3p@+~@=p|$bnL=-&kI+}>C-fHv2wB2FVURFb7$Te`3>Ah6!-WyTNMV#P zS~yu4Bb*|bg;RyG!f8UbaJn!~I71jOoGF|oOc2f%CJGio5^{uGAy3E`3WQ0*WMPUh zRVWmu3Fin!!gOJVFjKG!vxH)yL?{(z3v-0I!aU(z;XL7dVZLyIaG_8p*aW*!E>sAW zf7PYcfoEyAt#!heKsg)PE&!uP@t!jHmF!q0+F_(k|t*eYxj{wr)3eiMEd{t*5Y z{wMq;{4MMd{t^7bPGOg@Ti7G)74`}Hg#*Gt;gE1xI3gSsjtR%n^m9=VMX{aOUQ7}@ zh{z)evN%PYDi(^<#B;TiZ6*9#h1lb#8<^l;%nmT;u~VC_@?-l__p|t_^$Y#_`bMV z{6PFr{7C#*{6zdz{7h^UKNr6czZAa`zZSm{|08}YZV|r|zZZWHe-wWbe-?e>FXFGF zvS$pogu&J?mDSa3io-j#g!YlKI^B-o+3Y%phso}_*21z-p-ATlX<4MOw$5v>uD05} zRY6sSif^~O$XVwl#kp>G9&Kq;SQ%SL)mGrt*=3|`hP}oSLzAk;QHgNtV#v*?uZbsG zSC$9+3ySg!urWwpLt$;DW2u2gh!xf$w1B#3GWiYV4o`)Bfsy&Ll7i}rxJrs#OXfIT z)sEVVsA@{;%j+sU&T@xQipv27C17_nbAv*YJ+7L`_8Mn(LrIn0bTpxvOKSB+K2Wrj>x=EqYI}LLV+u-Fk-c_d zy?vp>sQAdz`Sl(Twqwe3cvR7I?u7w^4 zwx$VG)6l3)vU{*uRGrsa@2RN5W>B+hol8rdhIK}^am4QN8;BdRV zQN@bv^|cjM`L3E8du?Trv(~tPBDRX)ta^v1!D{!|YYfBHGPNk*@bYxKXHmYZ){A_| zcX>SZZsW8oc3fPK#K=W6<#D+d%v_))%Q=n)-RmfFRU{x4&r0NAp*24rO}}B8RYN2xC6-EG5G85!Vi6eA-|ZPV@EimDp>B7;66T&GU7#6jsAH?yYK z*EuWVT0la@jvALYqWKB8V1~oH#N|oER?s7j1AnTNN{{)%LjQ zsz{a~3(zmcQ(IDjj<>e1%AlzZPlDM~qBU88dKsfp36V*tu+R1ybTi>b&2d&bTu~ya zHl#6(LLSM@T%k43Sz8&sAVO}g-GjD!;dJz-NY<$?*P@6}0}W1DZiUym7;PvWUA6Fi zBC&0{#&5 zsp#LUBa2}~Ad7>Em5^PvcI=U@SVu$dIM(Q-6Ea9Auf#S^GO3-bqcYg*#P^|gPioOC zb}V+*p|r$~Nl^b}mxp=}x4qKvW+Ny_17CF%8p!(CffOLO(SjwKx*_6;ByWg^gwj_Z zQ$#!%x!4hzQr+OLg1o*IC*~v3axQm?r44QeLP;d*(_Pde#4oWWUb=TWA=3k160*Ip z))}i+Dy~O$H@JHarhFMSo|6jlUDd9LyKuc!m1~Jw!majNM|F(k*9Nq)M7%C{uC8)U zch#V?t&g}X&>2wbvDadYT&*#V+jy0tty}0oX1cx33Tu6JU984S6)twTX~gbM#m40@^~`~DcB7>7B%jshpOlP3^DaGX){K|$O(Pt?31u^Z%DoQQT`3u97keT@S-j;5lz+J#M+7t*v>H2u_Q!m)fIND@tOQ8`V|u5-TlqV(|=9ij4!EpTXph&N$^~{QOnq6honP3RD1)1qKU?i;zjpNk+UKZCaWX+xT#>p-KVS8a567Kr%rY% z>bk*AaWpS4+gZ;2HHxt%R?ERGx; zM8u(PipN#&PE4<Ud~&?TWn8;MSc;5UQ6rt+^T(IRbH-wj#S{p+k|+#ciZLVtl1b zm+6j5XMIhA!g!EDZFJpSr?+Z$EvES}L+o^U&~>S$KWgWNf}tzSF)E2F6|eltmC`fA zS+S@#cGFW*gSo>5qesJsV$9%robr8l)W}S=*C`sK#X}kLkUC74#;~stQ?AOGA-A%| z8Fe5R6jO#4Gu@8bIKvCYi`->l1V=W=1t+S(vLV*`^6JQ0FRic?)&&M0(bZ6#VOq@g zc$&*utHrz6<%$@5YXKHH@L`HX1Svf3Tg-Hf4!an$0L zQFV(~rQs!kDos*y2TE;5v_ybVthAz7C^P4{r>Stby+(CPFH~6>Jd>aLL{#PA^|E|B zo^MvG`Jl8bDCzR#NVvT^Uj>eeYCLu#ml#r$YGq5}OBg>G(1kbERfj=g?En{{o?r}A9|^}mC!bIZ4B8 zzyx}3y|=0a4-gFId~~8pdq%RQ7#>AFJJ5^L{05%>VuU`&UX6#>F=C8o?Ra|UnBiEW zw2^d|-(bguG zKxnZlf+}MKjw-Cv7&kQ`PHF2#cNeF8(v}r7Lkq3;I2i?bWpGv)@sL%Q zoscS#A8%rOQ;)J9yPS|TE!HY5h>--!oOEGrg$v`;_;#XJV2~6;9|^>&GxGJ^T32mD zjjKLZZtEH|d1Ol|9@7I2opi-6S1B!8iHu>g=CQz8SE2MsF@?Q(F12qlya&`}7h1mp z+8|EHoA=sRW;qdRBLSvCauC`U6lr_vO;C7_PRPeho^;2UJP?0r**=_J+8|7idcOH zQl}VAbG&p`vNwKwF-)&OL3Vkpu4uzMx#K{v0nvnXQ_06|YY}b*YU}7>lfhy*z)pt*%%WR0u zj+krIn?#8|ySONpj_K$c90e}SR$zJnW1IwJV;dzaobFm2Gk;~F*tW|ebE_PYGm-ij z(sV@uO;@_yG(qHwTnwp~Mw5j{KA7ZAs4`$i7wH!(LFfV9Vn+hi1#JwK!G)MHt*xBy zs*IU95YES%7H2%60&8Ij)&EO^GEl$LUyNJQFvy zwA5ObFSnaYu3;>WRk;R>mUWW!Q|%2RLxsKq#{%>*afHwal8E;7Q(ItcjqGIQkX3Ot zSoBKt^r#ZUWZ*KUBzZ(-l1{LW4*o$wGagoXo_?O`U>rV3BPCRw<<^QKQs#q}qxBHTnqz zQ*2*CmQkrnL>f!kogPOW8aC$wCzhpA1BYkjxOY#$Hw`v(7vk2cuy#Sjq@cXwDX=z0 zSy_c-3|AXPtjK6_qavH=y2c3UxJw!pAIM$BT4f1P?A456UZ|WHE@TXmi@Sy~D6Lz= zWYokN3dXZQr&Eby#BE_ftvYuTbG@QoSY4{9&J{;na~SlYyz~;Ma3Di4jKUpFoE4A( zwIS;UVt zNO0MgIzbsZs}zt8EHuh7GacthcZznCxa?cdFrqvE_S#DSh(lGBXA6r6t$80qlFDoRK|vQVw09uRN<{X zIoudAk>9mJe@W)W6r&f$;tNfx#-eK6@CK3^!)+(l;9}~jupqX6QvPqH5gcw(BPaNS zxqvE40boQLxdJOfst${33ZmzLs!Lv9A}fp?F?z3-QkS0BBp|M2tYT~eu?mXEwnC9J zo_`KW07a07Y?xWAiI@b@$zsW|H;~aWLZ>^x(AgNTLQ0)!T zIAY3ULN!_jFMH`sm7i@VE{~q3GF}rU_eQw4r`(!VkD_Vtz%OWlx};sp+VGg^Ud6NO z?sjo|M-$tV@oKQ6vebpDg89~n*^StqRZo#q)p~EV7R5u8+wy#_+`FU)uR(wv(;-cIc|0c+=$Bqexse~lAK`{yxwZ>X5(2a5&Q8nr1 zm1Jaxwj!n4p{}ULGdzPiI9&;#0K>K5F~65q1pX_}il>rlybg_(*UB~@Cm4$vSnDFs z2L3ApFeIio{O#5M$~W3Y+<3{&(7*Dp-mAw=cIDB7;SxZ7?i4tvN0h~kW^44KXc7}G zIbovl8eN+GmraQB9zd8CvM4Q*pM;s6h^ba1;RK=#cc`2}7qXmBVVVx=ifv60evmTGc| z6-P~)GJ#eJCSpNiB~jCyU`f=(S0)a&K@}cq=QM#$z8I5Q|C9N~26jr!u%f@9d zjhtoD#}ZbYh{cJSs>~YcD{ekk3VCuPS0d_7iS#w8+U~|aVp#o-DV&%!BGPYZoXV;A zh+o|mIZC5)tYI>qoQ;e{B9%_jJO`@}3~o>;JE=WZ>acO;rR!vv(Ve*LAH%E!45x8v zEGw*bERr*9J<3RX^ea)@N7-M@Hj#z|#x@1_zrs@+^{t-#`Z_P(-F7a-gJ&d0nR??M zZdoE;Y1pDDis3snxMD}eV%S0}Lds@hLjl<;b*S{Ol#mU@j*FDHr{Wn?;-VG*b?WHS2$n-MK|y|2U)Ob*P76_AFn@kR?r$xo|2 za-Z8sAL`T369|LWD5B2V8-ni|VM&8B)e}D>Cs)_kRcY^-1}dPzO8Q7s(ehD{4&`kk z8YCOO{9))(w~{GtW!I_$!V6Y7qcu&&Aq5&bqn2$VH?(@gc}W{tf2zyOl#zG;>5!@dV9jqcVQ(JqR7YF zhQj(B9gy$1z3sk?AXwCFZ zdcqcmHze^rQg^Gy6EBJc>e>l;j2N6o<~xn|qc46mmJb+{SjDFx0$xra%|rafZ!qNj zFw3ym1RL{uwIUKdxfF4EJq|q$qQ+>pyVQka#Hccu3aTd;_Hf0+ZFr;YF=-lucJngY z%{$!dq&ly&20@TtS}B2jua{th(b6TDi&Hek$TKDMa22#rQ{JVMTAFzARhs$*&V_~x za>K0+m5$vrD8|7DUQ)vtQI4Ps?OXyDO?GE_7in^ib33Az2OYA zu_!g430R>d3>CM0w9S!As120MgAmm{UhAr41GTMsU$m`6G3^y#KB|uxEg=M&|B~j0YZ{#QHIpLHkaFo|C#0wpGVR5AuHz(+H1=#xlZVf-$72%wPnR`>8UyApKtyB-)(HGpV}Z)#1+n#Q;r3d=u20 zTMP-*HNmt_kNTwk6TO=)?^|H>CRDJ@k!Y|8 zQ1cvP{6NBrrNQ(tdRP*wUhxsf;=(%WQ=&e*&}j^loEdl8qzn^!T9Hj8fMVJ>P4f_= zaA=$Ikl_i6DO()Ip5>7r@{!I2Vn(CvsHUPBLl+&~s$Ts#OK8?s&DBKE`^r1@%M-h5l8`sTFd1mS{=l|VXb+Vq zec?F_^@^bU*vP2<5F3Nhb2stfeqe`F?M*7GPO>xIS(T`;(iE5896O0`_Vy3_%rp+}AX4cY! zCp9OvwVV-EAo!My{JAH!6T*w#s7CNlZ!{WAfa!fHlg#wR37W(Y-C{YsSm^vwvx)&O zlq-Q@o(^AGGI{|!h9{%DSz~dd9cB$BBPV$b#pGd3G+De_Ltm;WL#orZUbKf{U6rE} z3z!_rvl`=c33yX_;7D&#rUw-RHQ&k-rI0{l2N&4IIm8d%i=cs#@jp@mv-0v_qhT#m|r8?5eIU-1p5I4Nly+dh(;v=n!kaT0*!Q1}-{zHl0v zuOvsi8`5KwDAiai0whX3dVB*(Dz~noHsZrFkzof!wYDU_b~#t#i_N7qt_g}IXsoOf zw==laQ8ph-P`&DfzB+Y6KByx+ArG>F`Bn9`5nF|XraYR-e0v2N4}7b}aAqUA_*BE! zdn4qjuq39LB&o%vcjpaupfJ>&uwc|#o(Q!ejYO0_JGv)&3uF#Dt3*A{s&_>WbtBZ5 zpbDY;^DaOy7N@==N0FC_22Y~czlOF@!OJ#=pT3JQCZr`jq4?4sy{`I7b##eV3A0eq zdrqyniS!luS;bHz)hsmp)RCd2zGaVH&uHQ)EDrIkftM4=KS+lpFx84gddWa16H~+e9iUu<nrgpXXIB-R25VguwR$_oGrFgjK)Q$OO-P0<#b}$^<8_Bab`2H^Qv@JI z3IfUuwkXnxsb2;*np@UM8P00!uB+W_lp&=mh-s<7s6~D=KB84t1?9|=p9#YuPANrf zWpPNNYRlV2*ztK{tZR2HG0Fj*e0~+aq>c&RIAVz#FdaL}R~j^=`e@-dP0xj!8Z#*r zKRP?&NvrWU*kv-z6wJvP><%9$M469ig2)eNsB(#ADpd|Dmvpyel;U_4=D-)WwakoB z#w~ufV3JMjLOca`HWrc0-S&!LkNbl46QSU-YSQqf!9t zNFv5aS*IsfeNz?1NK{nefp>;MWEnkuPmHQY`iax4VwZ+jPzm=C;+YqHM zhPlOddRHy4p%iUeWr02Nlk93hcLP=$cg$Y_8DyMp> z#8U?B$sXC-2MiDMhOZc4g0f-}-OELfM5rhA4K!q>$61bDQxo+^FQX+YYxHz5B<@;< z<9*>^VWKRLY)15PR~BATnNfE9j%iO&Slw0Q-}o`y<0&L6!C>Oc>LROVgVBPP5cN7Q za{3rPFo@ z#xcCGR{8XZ&hlu_JT#4Uct!7Gd?iSGX;yB?qeoX2Rw|1=7^TI-Gy53X8sa&zfC6U5 zGO11;-^Wpiftb$T=;|Oxn&RY%DitR`==n~Ru~(o@DSdZt^siY3k^tR2-H+(&dWFH) zv`HRwezaQOS!eh$J6Qs6tmZkY&@ax&Ej3CFdF4};tJa~sPqV=2-BwMJmE07hYbyb*reJDPlFpP zA26R5J6L*qEYDuK&|&!fUCp?u@&X<@ix#3hMebGhoHYERn5K$`@-&3RT7#HDMp-hX zsi7mSD8naB4L)0=NySV{`Ql7`WC>rGEx~@CSOsFRwpCU|GraU+IptkDZ6&c`TBz+_ zf;pf{%WVf06{9IFs^Vy+Ja{u3TeRXuE%{x)h^Gy*p-|*bK&;f^!KVrmi;#TJg>p2z z4h{Y!+H2P^-V}Vi4-_fyZsg5GbA?hc!{Ml`!_7{ekxH+#8cQr`Ghw<9lHbOrWup;6 z%W}$$jN$#Ne4Xl{wJ_g~4+&#WFQ~zzTztdZSXodw%Isl4rQ033q%H84P=3jz;zFy@ zi;O`LWyv^JtJLD1KfIa})gX0XlxO#1=BdHzT4GQUHM20{#6~RgXG^hBIVFm`A#_yT zk@7{3+J%wddJULeXw9djG#=DyRzQfBLKxf|1SJ-aLo+77@QNLqCzyJ=|+CUMQ@cQ*4~%g>`d^>025Y;3@0MARW_Q49m2Z z#HD`R0_$V(z$bETpkj%!hiG5(3ivs3v*y}86j$}_R(j-a9OV*hOix`({CcXi|HTFN zx+=r(smXId^k{6RbuywUFJ})AGekyynUki+BA5FF!5r_Fhg`0&)grFDpF=6igR(^^o8++|7;Rjc?yPkd z)YrJt?u33y8P`fZ4f&K!Mmi%to+Y;<&`)oVlr1)!Pt^rOO$;v8k^A^CcXj+*jiC{) zo)LDH28Ihg_8Mht0z3)8AOoX`YV6FJkR*zO)fF0NGR9afqKvPYsDXs3*S^la*nDcZ zja2WId3K|CD_5yEiIxIE3Hm}3={PMc@SsF+bMKg9!B?o17qSdfKBzS)PN9#B8amx! z%c{aNH>ir{wUI{IQFSPdF@iZWst73vPXhgS9Q- zq{CwrGof|+PL{~aL*zsm8)lPEib4pFuYS)f0|VsCMwSILOMQjas5V3hp&}Go7l&s{ zgc$bAx)=>~>Hll*Jiwy3+P;65=BkLOs2JO7P>FTGhAl1v3f8p~MF9bU1YreKP@^jn zgC&vJg3-iX>H4hfkO5FS=JB+(+Qp-lP&XGqnD1JqnSzU&i$ zTQpUlV^DKeO!}KNRm@i1_g`rv&-TU*ef7J!)mv%&7Vm4O(U=VrY4|;pV80>JctUnH zpR3f~R}XmIcvU(=bgefk+|ZvYXO(J`Td7!{xAGJ9Y8wvplYVB1&a(4xL+ni|cmwA( zw#6d?3mnfnt8|%NTSGh|-e9tg<-U<$8Z*O}{@GMk9Pp-7X>eCMqrHAAk@X*R6uIAc z;w$pL$;2b~Kb`p3E;4@oj*|et%BpoZ{^opHv9`+3Vb#x46NMQGnkzU8)dsnI)T*Be#aAI!dpt2ToIj$prmN_xd#heYqk7}zu1h#xWr<%0sZ_3-d_hg7|>^=i(_&(eE!uitab z`VflIxe54+LdCRI%Yxyq7>&QJ2|53!YuH<@vEtWt#fmGgcfE(;?o#>f%u4B)7V%!Y zBG~wZ>6-RdypXQC_tjf-Rrgm3zLj>-_*J|Vue1ZNX{)`CtF#kOTaEND|G99yJr)p- zCxOyyTY4yG#hg^^!3#?Kpc-YI>1+J8vGTo@Ketg)%bVn;;ixTNMM>U<76p1M4Wl4V zOvPTtY%@)n@$YGlDU-=eSXo9zTK`;Vq!naUqP{*eh&C$KX%O*^$$jQOFke z59A!q>B%{RGme0` z=W||PA>WSNF4pUBB=5J9a}nnT&Q#9boF_R;IG=M`Zxze!$~m0Vi!+LI4X2iKA7>%w z@0=#v#PV!7+jI8k9L_m~Gn6xiGoCY*a~tPA&H~Ot&KsO%oYJp({haokPMpIzCvnc; zjO6^3Q_H!V^9Rl=oVPija$53vd5g0nXD?27PESswzq<2zFm8`DLBztx4*ZPwKUk2& zaC?q_>q$!c>0X^%csRbl!0wpPA{2kl(FLbW!;h|uPu!*N$tr{KAYZ&~z|NRW2#LTe zz%W0?&XGnj;uGC|qAkQ(aqBE?QH4xyNwNg&q`DAAzp^(d5Kp7sPR6PM6D|S=vf5X7Hm2SAFB_hmy#30 zCI|Zm;QN2!OlEGB6T-^(vk`9NM!Svd>Db*rB!n@+1h+B`!+{&3%$K%klCVKt38ihr z3DhvVhTb{5PQsfU#^c*d#s>w2gt$fF^cjhsj(2w(X)HX@&^N;yl&rBCZhE4p;#OVr z$-a0u1rJBXUHOoJF*E2P&u&rdBPM=I(2x!NYEylDk!EW)E;7W$S4nSj@G~20OKEt9 z`3G~9#%G?vRd7$0b)-F$g2N+mJ7i+$JbdDUF%wffkcgg=7KleS;5rKv!mgQ348;YB zKYkzp-?R$Ao6upQ;=4VJeNFwSb@%(!M#I|ybCmP}uF1>-<#UG)3+E5ZG1ilADqfG^ zgp2Q5)6XIq))#KWe`oL-@pqc}(H{QnSfy0)W-=0)%1~!?1g>!DjVDmfpUq1gt@qnl5kys6^{>j8=fs} zD37UVG%7`q-?1wFXbIzqF38W0R%)?vS)VkUV0?qyn8eyjXz?32TU5+6wUO2wy#2fh z8mlyf6L!X+#Sj!qbvU~y82dmReSf}~*;CqM7!CSG5T&@6?k~}21mQhBOh>wm^2HSg zrD405gsyT73xM@AXv6n_s06ntw7|oVM(;&Xk~6c!++r=@H$JG#cng%hOLdHK8%3>& zGW1UTskGL`M$q~VG{oqdc8#Ud`5@6)Ft(1ye|%(lkEcltlS9oL zf?v3x&*`yV=7v{Cl*ZIzJ3{d=oB(R$wo2QEHwoElzUnCZWOm6g&W87G*#(0h7a)%M z9IBZz@u`NBu%2Q~Scp{qWOm3D@3tBq6~ov8gD&62eCc?6N@vVWL&J=xtpDYIUhyr(4_9oGz~=T~$lHxlL;>$GVpGvs&6&YF%?VMYYtcuce*J z#x<8CucbZ8TFOz^lCG+4JZq^px0d{}TKc7~rJbyna`I~#H$^S^i)u-i*V2APE%T+U zrJbr;{Gq6pbXH5cx|a3`wdBvOWjyt@lw(|5p6?7Xth1nMRPpV2PBmv9r=GKv(|COK zCF^;~#Zdm71jb(C5BpV=$1(A0w~bvGJ26f6CzeHBc zj(;)P1mzmX=U*(U%HdRQ?XUc+WUY0>c)TTYNMYsUG)0_8W4n!{f+}m;{=2_~qvCwk zmghgczk0ib<2AQen=UeU9N(^9z-zXJBVzeQd|X^!lfU|InjiWzrmI-Jdi9JO z|1Gqx^-ru$R_ot?OLPA1h5F9$^ADIA7!>^e2O+aU!<4hbBO>R_ofj27Uu=r=5Eoas zp~KughL0FIYV;Vz*m2`0Oq?{?^Svqm_NKl;g+{-Vn3&OWFaJw6Wy~igI;I^@Nxdox zO^V1RTZ*2Ok}pyjs73w*Nf(l2;_g@eU-Cjsl5>9ZhWVX&{vF7FDzP~6Ep|PzByno( zms*w_oxFUXkph4(KwzMB6`;gzB%j5#baw(ONG|1hD}WnnEARO_;k{lkj> zXJr2$zD!0Q?$cn*eKZY;{cn(_~}D?dW~xTd_Kh*5#za`{BV z8#&cK0_`0sm)i->@+vjFru=_g@@vZTz_&@zh#}>zcuoF)-anyA_to5g$DcH1; zsAZrV*9veB*D5fD>s;_0*F~UtWARvPus7FoFo0_%xSne@c!X;`_y^al3GPM1s67Ia zYvLTPRbUF&x!^gji$HT5-UhHY*K#m`YbCgzYc+U;Yd!b}*Q_bp05h}!oXxcgOyN2g zJjZnrXx@z13-;z(4hC?o1lMz|29I#92mj!jHAlTLL%l?E4J#dH}s9TrdNpmi&r4i4g43C43x%;Gv1Ji&D-*r2s|tN<#wR<~hn zH;md)1U7CfwoL~1g;BbI3+dr7(n>I%Yho7H*6kS^2una7IXIVV75EL;`QSaSOTjj8 zi}_{XD6SP?1lPIX8CVpx4Qy|uufg6>=^z#y)bU>w)+pqA@guz+hlSi*HFD1C>Q5887rfbLu?z#y)b zU>w)+pq6VrSi*HFDDBM41ns#NKzFVcU=Y_zFpg^<+*6|aO4~6$tSe()Fg3IuH18&s zDFa=(R)EW4ls_Ka$J6sc3tYI-SgF7hTWxmJKdTr0siuH!*1*STN;*LtvoYo!Bx5T>9$I^sQbm?v~8 z_;ye95g?~cJ!FjO!0~jmt2#m&4 z4>olYwG1qVQ5#A@r+(-Uv>fz<#i0$vU9bdb;xkw-H0#gU3|JJj68s)U<>hPuGE15r29iJb;9_6VBj29xGv?!e@C(J$y+@b+NDAgWi+m^Z8t{UUC2 z#+X7Azk@O4DFTl4o7=nO6o5-V)F!mkV4_<`PycdCA zy~kJ?j#Y!YDd;cq6J=8o8lZ_wyf9YK@n9ZI0Zkk*4ef^}zMPJ6gC?%^#+*VEC;2e8 z8JZXZ(?JuHVTYlK-*c@8>-dV}Zw(HC(RqZp8s>_25>LWBp@|kVL{03=HSrwR#0y*# zufjrbY!NurPh1NMaIC*ruMhYXMr(uxFy;#*f502jtldm8PAb4HK^R;30C8zB)(FZZ z-iEb;wtgS&fzh#YFz*BSHu9H(b|GTF1TYvzd3kQ3Ms3RnO&9XMgR@|iPAr32!#|0C z!R*LaKg9jGMR+G1`Y>p^SZsqG_#W3@;3lp&gEwGw?NkDGQi=Hof{S1@rz$WBM%N5^ z;3F9I>p9qaiP#<|FobL360T!G9oNJITptGQV#Jt6oB*SK5reoUZiP`@+rdwlq7CR* zJorv5(xJN&$&iP*fa`_eF|Ln;zrv{eVzA3Hj5{3*PJkKer8F3oLoDW+IO1b5eI&RU zM)`?N;>2_ta4^?C;8vIx_+22kEWAEwD$>YVgcT#F_#Vb{1^3iW&UJ z1{@Eg_C$i~U^M>3?OcBg&iNE;8ON>zudK!#Ko^6b#lwf8bHQUUYV%pp`ZIA(i6go8 z0XwZhoJW2+_|XRV8?+i!Bw+nQ6Bog#92IyUmW=c=u+v69KHvrz8!#%f3~Zh(mT3zHz$iTkT*A|1!GS5FKM+^Kl*qFR%;q{5 zJkNC@C`lE|u?9!LX#GwAck}db!Dn2zN@MIYO!os~I~b_tZ2)bv5TlSz49mgXK`X%_ zx#Bf|4=CG;b%K5o+kDN~CFGHT^}fM6LAoWlWe4&jT@7B#gP%hegB^Cm7oqLJP#E=H z38rzK4;I5@#}NNP=^h*lZ4EYpb%mCJ?O+1563m4;LFa?*_o7~CJJ1(qcO1S1TJIBm zPYzy$wLvq14$2EeP5hDTvtaP|;=G4|H-8Xy3FvoR)c)Y*lbA=8 zUj)8)TGU?PMHrorE`fiZ!8{^Q8JKZaY^M$!@sn8ZNU+m+F~2>S$2HNlP^^pC;%AH@ z%4r4O=lT(N>jLs1{Vte#QC!Pfu;(QlkMv$(;1$Fj=pgXWRjfgb^Kmeu2yq`e5?lkL zYnXN5A+8UD^?niC;0bQwS`CiBhWR@P9{|mZvDQ$I0#w6jy%6VLM|?;6Be2dbln*Th z2iz6)KyW5$&R9 zMC}9KH-|?4N8m;a3G0P&HiIs8MC}S%)|D969nrGBgtbB*;zpPr{dx`_Zy+)F4AI<5 zEME#bbL|Q~ZX{vr(8n?`rip~_c1*Tmp~VmbEjO4unF z&E50C61Lh|Opgb<4w0}j)Jx2TQ8~oNF4%|kGH|i0m_H6IfKeXeb670eVC^PhUk(+= zO$XY#OIR|}iM}wZ*B{iw=-B6A-f)zU*hp+KLd??&Tr*O_igD~Zkc|@a6Q97$PT^Q^ z&}g(7dF0>}m=a}rfpfWz0&}^}10TY=;@C&vS_SHYUI&Jb6WgEycf;(ce9(0~`~lh% zTmYl;RbV{V#9v_4ue;#<38Ib%lVOyfxSi_)u#juw=!v55DZtGzic7>pFcs=O47Qjg zmeUGc0HeCpVC%^erbZqaxDH0I@rhDTQ4=S^Xq-L44ey~%$e#cnf>GNnr(oP*l*bEP z2%~i3X0C}p!?xqtOJM)0VjBj6AHe83IRxCq^=7cxOJX>$-367?CG1-qON@n4JRx58 z#=1g!5vcbSHL=GG%!MB79h?J`LPvo+Va3Rw2mS$5AiWGc;wNEekzN3nz^I+X%l;B} ziOL6u1&Dog2baTW3|E1&nPNI|Ka9pdAAAC%ad-~42^6&q91EjkCx9PuT?&r+0DVAv z6kzKR^cPwNKAVLxhkg#ahGFeP6Bj7q!>2LM;J(@LC#2_tl1K@Qgf;^^!>A2jV3#>! zJG+7bFk2iO3GRW(puYvv=1Q0kv=&U7Cox>JC4*OCa-9$!i3KmioG8yn5@xkbY_l~u9;QS&Uf|<6F@~3c>y}HHC-M{1 zV3bD-mcpoB%N01+z(_}eAFjf@AioNX{{-s+nmBMZ`U_1ojTh^+1YKdm8Tbr1b&c3x zFYsp=t@le{$XYQ!arAol74j=U^Uozr1}z19z^H8kcn_A3x=O*%zrdP;P5>Kh!hGYJ z!4C9*QT~zOn9ZW!dV;^f^hhrSEx#1=*n&D3r4ygQXdOKVoxehSp!S2|unExXz-}4| z^MdvSzl2fzQG-`u)UP6NKnm6o@(cujgz2DzQYGw4npl@qD`D@#XuSu4*6Cthy z@)Jv7R6a2+L!1jO*km7K%UKg<1KRF~PeKz}zSw@EH;l%?2Ymkkd;@txz(N?+OYDA7 z%rAgWhp=9dhj{BS>Vm!tE;%B$ITkz%(;}U?@+i)K(79mPcjCEV9eAh!bAj~3;G*xv zwX6dFfKhwO!2LgOoLG_OkCW9s}IfuSL$AX>DBUVHA0vi{iE@&Gt^#bMy*Rfh~>m`imPbdeR za~b{(9R=38BF083*dIpcf`MR{Yq(xPIbFd|U<#zigK048mloW`bpdGmtC&Xy_Jt*& zeBvFL7Mge$b{M)898oM`BcX|PZc3OxG|>(g2TlAFMt)KTZYaS#AzcmjxrK3tb^^!1 zs67gBIoGQ|tJ~tS_MkhA+D}}^HL;lM+&fsy_YosdrWZK!0mcxTcpXM-hdq?A_h4Pm zBM-Rz55x=TRp9b6_y%b(=?UU2bTU}?1>!%nB{%>^?HmZUe2FnZdMmIo!?^{1YXg2F zL5#$F#e>&i)Mh6W+z&OwITrbgz*1NVv|w(+*27An6Tq<+@H^z604}J5F+;iu`tT-1AfT03cLuLfc&LkBP;BOR)TwB{*(^dH5AJs{sE)$F9Yjai)C7Z zX|Oot*MdzOVf>(N!R0WzH@gZv*BIAvR3`WU7FU2dZG!S)QoN5NfD>Rr&^}-+tN=P5 z%!Ebd<5=)58{GSWmVv`z_6J1HfL7vO88I4m7n-=9YvM7O0`(Gq<>|!8rYP@wksG1u zSYo4Qh-XMAc7jnlaX8n+0IrE2a!vf4YvMt!iG^GfA978!Y%Z2h?8r6Ig=?Z0*Tgwo z6F=pesO6e?fNSDKu8EJhCfeGH^%8qfa zOcmj;6x*ey728F2W~>auZ`^Oj#xob@!CYA*s4;9J!?$^v8}r8hW7udm0o!BPNNiIc z+?VguuW!$OPKr{~Uf7z1W4!Ud8*+_D4myHzj%Gul>2I&uJ=|rGAcr}dfj!1PHaG0W z{S2_WHz?3vTrILl7S4iMC<{bs!7K!C0eN6+CJVzcT@85z){6@R+zFw-7dRGIai}vA<%NS(;%NLo zi{6ehv@pQ1CBxAns3E|RSH_%~0>@FQGuRwM+x?-!P>wf?FqB4RbVHc|IL@!qvEA4_ zD zU|$&X#~AuyU-cFU%mH=Md{zAKfcl5Smn+)sih3e&Jb8e!QVp-4KkDN^d~1b#F$`@8 zM=8cOlTT3FLeK|sWngU>#)3w|I1W}gc8sCsK*Ow3zm4Nb-X!`_g-3WJO^KY{(dQ8S z!+mi>U#gBng$KS_A86f545J=iUVFt#!aZ!mDx83J(p*KL-pVsUu_f26%;cY~cjG!Y z^6%FBBm{uht#?O5o>$lVe}%98SNcmIRv&#a&T(~*#rFg7As-n$w&H*CSmQkZ-^Us% zM?qlrzaP+ZPRmesJ|Ok zmSmr#Nb*ebNm6O!wDH;mty-(q=4$h_`Pu@lUR$Uw(w1mTwJbxLVVz-{frABulQU_m zb*gQuQ>r}GJ=G^QC{>vnm8wdOON~p5PgAF9({j`FX<{j231wQAE={*iw@sI&+oucZ zPU-S=_jE%P`WZbDqWQxmmZ&A;~F8p5)GFBPdCk6qTe( zic5-5N=Q;CX_Io3@{;nC3X=3mg-Jz8B}t`8co#L;E=ev;E=y)9(iH0y+Z0)feTtCc zlp;@YPf?_Jrud`;rLY`nj&+W0jx5JMN62x?k>|MQC~`b=d~$+vlsQp3s+_o-_?(0s zjDruZ-!UG>a!fwpdTN8TQSi_NcxN6wvk+cc zrj^2*?BPl7@S-4iP#nBR3(qNl*Ob6xr0`gKc#1o`BnTc72k+3rGYa4pCGZF-yulux z;0`Ycf(OK5_O+P#0?c{|W?ZVX)!FNubnZG&U63wH7pF_mX?1zJ0$ri5L|3MhX4z)h zXE|lLXL)7?WkqGhWhG>3v+}YEvI?_GvdXfg*|ypC*-qK+*`C=!*-_bX*$LU&?7ZxP z?85Al?6PbrM%W&s>yD8P!l=e!M7242IR!a|IVCw|Ie3%>i^UL2)wb|7CwLk8SQPv# zL9I1-S0OyBOfA*eYV0*m8h4GSCP)*diPI!#w3<9kfu>MXqAAlz6Kxai6P*&>6Fn1y z5~C915)%@&@W%r9VhQ|Eib!A&|8qwq2!h|m!RNH_w*vSY9y`KtyNLU{0AE+AecQCJ;nPL%X9i!E!H?zeVV@LbiYg^OMV*qHlAoeaDM~3#VTf0Sr|MISQcF`=nsu5iO-PfcDbjq>lxZqM9Lr71M;t3kD^0^q0If_bXysak)<>(< zst_&Jh?Mz=l0_6D5!+;lYjVUiAH*{iVwoCoEFUqf2=R*{e##KHthpQMfd{i|sN=sN)n_x*&JmNpi3 zJ1=uz_PBwhj)^*^r8$x$CK8E5eW9*}c^4~Flef&7Fw&x47Ym7*B&M&4#7zCJFi>b- zDW#!M*A(}T%@R#i_@sZOJZT z*`hWzk(iiRTiz_X-n?+=i`CbCp0_L&`UwrnTO~0^{Q-vNm`*gaXks#PyhBr=i3K+5 zHL-LJ4h(h)2#=I`g!(zOK&y~kzlrs{dGosaW0xOxb@vOKGNcEb*pT9a@x8i2>crS<8&VM#caO^Yp_?628wTbuMf7b57INz&VWaryn+xqhh zTmK&$PxXG{Y1{kTVJ9?wo_w(QY_8M02U;Fl)$`}V9=0#+8-AU(^ru6iuKB*bH#B?k z-sY8aXPv*FIHEz9m-WLheC4D6G`{!IsEHrTG^ei5s#oB#&>^8`Nc&qK>m8>g?rsv( z=(m2&zueeATyx`T^3Ug*JIu-c=)?BegC#?k{b?Uvu=PUmCPkC!RwppUurhok$T(C8)E`*lO*{GzJe7dwRJ;@%B-20joY2XoJVVp#W-{uyD}}U-Iot%6*Lsu((bp-Y=vg3nln`OLrjB&C0c!Z z_1bTxzlKN5_g}aFarn$a$KoM#W@LYS=fSz3uT1)S%kh&o_PZK`)xn4>{F-N;=T*hr_P#xzwuC?rRO`D?Hh6O^L>W` zI|ntos=8^}bLRdg4QxUmE^g~*-R9{hEj!E#S)H=eSF-iTq>dWv*?P)!?9*Lz7uylWiP3B8?(b_xD79Jh_-t7@z%sXrGd_ZN-rKP)MWu>({ z#o4%arA;g+<99Idh*Fu0uQI^FMre$=Fssv~K7LbehA$q87ZMWS&=92=7PEZ z3U?G79D53mJ$v`--EWF8y;5TlXn1w?n%Inw#^c%|1N>wzVd2U!@mJqaNOx+DX_HRn zyQxAMo(Ct^I3AB8Mi7cX)v|7~VUAeBf4{W1>b~}vFyix8jq4S5i2kho=&syuHh@jcs({Ejx~y-FBe}SN zgO0AbG`ws<&*JEM&r0XSId6V>#B}V&jPoH|)&_qZ@XpeY4*J*}F8F?c&4v6WrEMET zURl#V^M}XJA{TbL@pyR4#c7AW>XyCYcIu6cPR%~_eVj16(YMzWni1EhJ-u#~)^$rp zfb2}+hLVDXDWlz8U$(gBT4zoD^FgtyY41n*yPfbEx@f|ICJT>5-~ax=Y;kEXc`7V< zTxsokfAL4C*YV?#U!1-C;l05Z?~YV;zbrUfNO4XwH?Jd+m~|8=!1HZEvaDH9WTdix zj~-!u5z6k72G^m}fWgPa_#hQ55FbqNR{LN~Ve7VLPC_4{m)cP+EbGn>_6rZGbZigN zqbfU;i*tA6F`SE9cQCUQ>KR*LDyl*sUtUI0y zg_0-1pS&GWzj4^G^z43($~te|l>649Pw%|9fn}Yv9JNjFkSg`+JpcK%`K>39)CSnz zik8plup#uj@A|&Kw4L-*=i8Tmo;hym5#z|vaEM2rih%rmTieJfB zyw#~kP6aRa_YO5Hd_SS(TZ(Ee{%>DHI7?#<3l2`@XK7l)j{S^lc*Z|k!@b~0hBe&f zb!(V@^)4Ws)^V@buH!_%`u$w<_Pm|u>aoY_rKZ%k-uSh-2^X`vHXME=^yRXF<0m$3 zdeUrgm%q|CZ1{PfZ*={s-;Z22_j-}bi1zD$KiI|Pz?Qa%WN&2w!V^piGn-bJgajw>gv?78=6zXFdh`yV-SyJ!6$?+OW|3mghBD)}FXX1JE}#50`dGUliwip(x6j)DsB1^Rp2Z&ivl`qu6lnF>Jh^ws z!sX45-r9C`+pWv8qq7^HSRZ+5hu4+8ecQT3-H7_;guR(8DdYZ#!*TC?W~Dma==o)@ zxuIjGTe%H&4sDQo+v$s~{V&v=ZT-Ae^mf$(UGSs7Ht(=(*5&@%*PB3xqIx^Py0&RPUwAO)Ue87sy;4E!mEaffBu@nLFgey4*T-g6yx2U9c7hX zSkPlaWfV6mU4A&>vY}T+=%SYweH|PrVTv%SYMbaa7-EyOiOFPVhen30TU;SYhv1RB z0dwaBI9M507G4LLG@37?L-Q=AOLT~J! z78kZEY`Li>?a5)QdrhpupLfo`JG$MvX$$j{UHrWt-0PfevB;)wtfcPKkP%NV>TRUc zPv306rI+1&)VJCJ~w&v;I{F(POim2mySxxcj*=#)Z@{(;t?Cxjyd0^-msoa zUMd_<>)!t5^Ra7MtbROb=-wY*bdxum81&VpWlj^eADyndAJ(J6^&`%|UforvfqAr- zYQ?d^IrhFg_B817S^6iB7sT$Jd~;aKBRtMaxqr;sBDGeugH5hOJ~I%3cdZC={?PY*{6SNQ&4BeFUBZ!1S55z%9@*U zc-Ml?^^#I<&o<5XdD89AfH_v4mDa3}$M=B^>MD=-7-ZM3&!fRJ26t+ErRpgk@rz!g zR=X}R73PWYMJa>|z12O{4$FE}Ty6BM@|v|rM3ApzH(&8pYB#@#Zh;Pl%Z*nK>-9#5 zjiicYW^RIu(AT1_&oXz(vcE!1B|>KeEq>u-B5Br&S`&dQr|Peud$=gZ3sMV-OSD`-F&*=`XM$WT94?^ z!7Y8_uK_z^hJN+yeDiaGVSAPJEbWA52lVN~m2KBKZ*oa|FYm3dT(X~UdhB&^(&@nG zA6h#lTYP4fd7|NnX{j&wbU8S0MOW(y>Zh|VA1wU6fo0PlRyX*gNAJszzZmk-PnpLr z%}5*ZZiln;OvjG9eju%r<+wEO8Q!^N(fwaE-ZHf35X*1$&k_+61zf(_@|b8=*S4ki zxji%IjCc7aY`sESJtqG9?z1@bbP(_|OpJSY6^5u-zWE;s&98f9R(9s=R*S6~F zbEdw7M~hSXXKov}_P^-*$KvbSFE2WtpM87om*(Afu70}4B|qiK^0R$goZpdAx_Mbj zVF#U6w0-|Yw@wG1Ir{9KPT!qb@bml4e{(DHMwb^M#}-BQ>(e0qUhy}hlhbd%UoSKEp)PRrH-{guIxXw) z_8qDJyW=(-c+jx%qa&X-Jdjw@b?oDaZ})z*b;i;N=a@RhPAlJ!km(!xM-2RGOKQ*b zzgk@Fu=cbuR-Qnt{JzE)Qq^z9)ENJwjS&EGW0vs28^#S6<57aivc7K;H_BrM-P+^9 zV#yLQ_Rbfgg#LIFrO&cn#-hSQss&MF{CTyUkXMhb7PtTI*fC-hb{B>UP8Rie6lNx7 z|0oROb*3?XS2`!vc*^=GH@J*3^WD1*rr3S`u>I+#1zi_*cxSG*^|V&q`oDGfp5S|a zQGK7k4h?E}IA`Tw_c~vdP3)Wd%b%y$%xO4wduYn1Q?5<7TQb@@`S-!IKlQU}G9v4| zw`=snJ~kFB9&a8x+UsTf)|PdwK5P1Hcg{Cs4}M~oZFls$PMtOH*uT|(w!O)~?w=*) zcW88Nv&}Hg{GgzmO=(^-ub*dcwOqYNTXv*z(3Vs3V{+G&CZ#w0EjvhuIBx5+!p`JH zkMKpU&U?6RD0#2-t*4RxA8flb)8)d~xvS3IYH;~aiR|P?2fsnmw`{*&vT4lGwbmBT z>nwS?^1_oCkH<4YHRGQ)UT6DJ_cfp04hnH~+%{8l(>D1F|FQQRVoYD+I`Wx`1n0SB z|A}B^V*Sd+YsJlYwOnXXes{dCse@&uJMm`1+ZDUa9B|vy!RdUnY~NDnrFDh`cNlqL zUhKHS7L}tuZZ4a}NZ9)9J~?vFcilcTT|Du0H|^B4?@r4X&Ua{=d#Ux4&3B%*x%u$N zXV*&32YvVDlL=Qgwdv6EYA=~2;dtNGb0WvjqO?w_aD{o(!|v#S@<*R1(@YRpf{)4PIC9sQ)ow4ma(SN_;K>z2ai z^PM|{1>@RuPEEMH|J=csyU!e~w_`)|>CIv~nRZ>^dfIKr#~a6exABY1J^UjN-TbTF z?v3w$-CDQLCA#?Lx^B;eUwu9fI5GFr0l(Rd?y0yvrC!iGj|a3}v*pEd|6}n#cJ1hN zsCeVj5gT+~-g_EP?)c|%%LI>wJ1h&|X+6J1gl&VPpR~}#nAFGn5p^m?&%z-_QifRf zh}Z9(Z%4-|ueigU z_U$@8RUYg=tczWEVDrE$KjqmCslgrAPP?do?_sYEn`(fn$%CuJ7mcsL{A^f8Ct+cLSW~3_3dD z?(|znf4TdPm1fQqNslScDNoOOeL3p5d9RkyEw!`#v)3+ra%{ZggOan~jqB6=w8|mu z$;J3xJqqh9uj{%_S`sm?&aC8c4=c^Q^<6)|GNthPakDt@Q)PE_ar+mV{QjG@g;UdC zq8E-jw|GIDja$#Q*Ew80zt-kPvqLQxPxQ0dJ$r-pvHkH5NiApH{pH`1( diff --git a/Libraries/Farseer Physics Engine 3.5/Dynamics/Contacts/Contact.cs b/Libraries/Farseer Physics Engine 3.5/Dynamics/Contacts/Contact.cs index f72a4e7d6..c79e46edd 100644 --- a/Libraries/Farseer Physics Engine 3.5/Dynamics/Contacts/Contact.cs +++ b/Libraries/Farseer Physics Engine 3.5/Dynamics/Contacts/Contact.cs @@ -249,12 +249,12 @@ namespace FarseerPhysics.Dynamics.Contacts /// The contact manager. internal void Update(ContactManager contactManager) { - Body bodyA = FixtureA.Body; - Body bodyB = FixtureB.Body; - if (FixtureA == null || FixtureB == null) return; + Body bodyA = FixtureA.Body; + Body bodyB = FixtureB.Body; + Manifold oldManifold = Manifold; // Re-enable this contact. diff --git a/Libraries/Lidgren.Network/NetPeer.Internal.cs b/Libraries/Lidgren.Network/NetPeer.Internal.cs index bddd50da5..dd220d533 100644 --- a/Libraries/Lidgren.Network/NetPeer.Internal.cs +++ b/Libraries/Lidgren.Network/NetPeer.Internal.cs @@ -69,15 +69,8 @@ namespace Lidgren.Network return; // remove all callbacks regardless of sync context - RestartRemoveCallbacks: - for (int i = 0; i < m_receiveCallbacks.Count; i++) - { - if (m_receiveCallbacks[i].Item2.Equals(callback)) - { - m_receiveCallbacks.RemoveAt(i); - goto RestartRemoveCallbacks; - } - } + m_receiveCallbacks.RemoveAll(tuple => tuple.Item2.Equals(callback)); + if (m_receiveCallbacks.Count < 1) m_receiveCallbacks = null; } @@ -123,49 +116,55 @@ namespace Lidgren.Network } m_lastSocketBind = now; - if (m_socket == null) + using (var mutex = new Mutex(false, "Global\\lidgrenSocketBind")) { try { - m_socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp); + mutex.WaitOne(); + + if (m_socket == null) + m_socket = new Socket(m_configuration.LocalAddress.AddressFamily, SocketType.Dgram, ProtocolType.Udp); + + if (reBind) + m_socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, (int)1); + + m_socket.ReceiveBufferSize = m_configuration.ReceiveBufferSize; + m_socket.SendBufferSize = m_configuration.SendBufferSize; + m_socket.Blocking = false; + + if (m_configuration.DualStack) + { + if (m_configuration.LocalAddress.AddressFamily != AddressFamily.InterNetworkV6) + { + LogWarning("Configuration specifies Dual Stack but does not use IPv6 local address; Dual stack will not work."); + } + else + { + m_socket.DualMode = true; + } + } + + var ep = (EndPoint)new NetEndPoint(m_configuration.LocalAddress, reBind ? m_listenPort : m_configuration.Port); + m_socket.Bind(ep); + + try + { + const uint IOC_IN = 0x80000000; + const uint IOC_VENDOR = 0x18000000; + uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12; + m_socket.IOControl((int)SIO_UDP_CONNRESET, new byte[] { Convert.ToByte(false) }, null); + } + catch + { + // ignore; SIO_UDP_CONNRESET not supported on this platform + } } - catch (SocketException socketException) + finally { - if (socketException.SocketErrorCode == SocketError.AddressFamilyNotSupported) - { - // Fall back to IPv4 if IPv6 is unsupported - m_socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - } - else - { - throw; - } + mutex.ReleaseMutex(); } } - if (reBind) - m_socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, (int)1); - - m_socket.ReceiveBufferSize = m_configuration.ReceiveBufferSize; - m_socket.SendBufferSize = m_configuration.SendBufferSize; - m_socket.Blocking = false; - if (m_socket.AddressFamily == AddressFamily.InterNetworkV6) { m_socket.DualMode = m_configuration.UseDualModeSockets; } - - var ep = (EndPoint)new NetEndPoint(m_configuration.LocalAddress.MapToFamily(m_socket.AddressFamily), reBind ? m_listenPort : m_configuration.Port); - m_socket.Bind(ep); - - try - { - const uint IOC_IN = 0x80000000; - const uint IOC_VENDOR = 0x18000000; - uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12; - m_socket.IOControl((int)SIO_UDP_CONNRESET, new byte[] { Convert.ToByte(false) }, null); - } - catch - { - // ignore; SIO_UDP_CONNRESET not supported on this platform - } - var boundEp = m_socket.LocalEndPoint as NetEndPoint; LogDebug("Socket bound to " + boundEp + ": " + m_socket.IsBound); m_listenPort = boundEp.Port; @@ -269,7 +268,7 @@ namespace Lidgren.Network Heartbeat(); NetUtility.Sleep(10); - + lock (m_initializeLock) { try @@ -425,176 +424,175 @@ namespace Lidgren.Network // update now now = NetTime.Now; - do + try + { + do + { + ReceiveSocketData(now); + } while (m_socket.Available > 0); + } + catch (SocketException sx) + { + switch (sx.SocketErrorCode) + { + case SocketError.ConnectionReset: + // connection reset by peer, aka connection forcibly closed aka "ICMP port unreachable" + // we should shut down the connection; but m_senderRemote seemingly cannot be trusted, so which connection should we shut down?! + // So, what to do? + LogWarning("ConnectionReset"); + return; + + case SocketError.NotConnected: + // socket is unbound; try to rebind it (happens on mobile when process goes to sleep) + BindSocket(true); + return; + + default: + LogWarning("Socket exception: " + sx.ToString()); + return; + } + } + } + + private void ReceiveSocketData(double now) + { + int bytesReceived = m_socket.ReceiveFrom(m_receiveBuffer, 0, m_receiveBuffer.Length, SocketFlags.None, ref m_senderRemote); + + if (bytesReceived < NetConstants.HeaderByteSize) + return; + + //LogVerbose("Received " + bytesReceived + " bytes"); + + var ipsender = (NetEndPoint)m_senderRemote; + + if (m_upnp != null && now < m_upnp.m_discoveryResponseDeadline && bytesReceived > 32) { - int bytesReceived = 0; - try + // is this an UPnP response? + string resp = System.Text.Encoding.UTF8.GetString(m_receiveBuffer, 0, bytesReceived); + if (resp.Contains("upnp:rootdevice") || resp.Contains("UPnP/1.0")) { - if (m_senderRemote is IPEndPoint ipEndpoint && ipEndpoint.AddressFamily != m_socket.AddressFamily) - { - m_senderRemote = ipEndpoint.MapToFamily(m_socket.AddressFamily); - } - bytesReceived = m_socket.ReceiveFrom(m_receiveBuffer, 0, m_receiveBuffer.Length, SocketFlags.None, ref m_senderRemote); - } - catch (SocketException sx) - { - switch (sx.SocketErrorCode) - { - case SocketError.ConnectionReset: - // connection reset by peer, aka connection forcibly closed aka "ICMP port unreachable" - // we should shut down the connection; but m_senderRemote seemingly cannot be trusted, so which connection should we shut down?! - // So, what to do? - LogWarning("ConnectionReset"); - return; - - case SocketError.NotConnected: - // socket is unbound; try to rebind it (happens on mobile when process goes to sleep) - BindSocket(true); - return; - - default: - LogWarning("Socket exception: " + sx.ToString()); - return; - } - } - - if (bytesReceived < NetConstants.HeaderByteSize) - return; - - //LogVerbose("Received " + bytesReceived + " bytes"); - - var ipsender = (NetEndPoint)m_senderRemote; - - if (m_upnp != null && now < m_upnp.m_discoveryResponseDeadline && bytesReceived > 32) - { - // is this an UPnP response? - string resp = System.Text.Encoding.UTF8.GetString(m_receiveBuffer, 0, bytesReceived); - if (resp.Contains("upnp:rootdevice") || resp.Contains("UPnP/1.0")) - { - try - { - resp = resp.Substring(resp.ToLower().IndexOf("location:") + 9); - resp = resp.Substring(0, resp.IndexOf("\r")).Trim(); - m_upnp.ExtractServiceUrl(resp); - return; - } - catch (Exception ex) - { - LogDebug("Failed to parse UPnP response: " + ex.ToString()); - - // don't try to parse this packet further - return; - } - } - } - - NetConnection sender = null; - m_connectionLookup.TryGetValue(ipsender, out sender); - - // - // parse packet into messages - // - int numMessages = 0; - int numFragments = 0; - int ptr = 0; - while ((bytesReceived - ptr) >= NetConstants.HeaderByteSize) - { - // decode header - // 8 bits - NetMessageType - // 1 bit - Fragment? - // 15 bits - Sequence number - // 16 bits - Payload length in bits - - numMessages++; - - NetMessageType tp = (NetMessageType)m_receiveBuffer[ptr++]; - - byte low = m_receiveBuffer[ptr++]; - byte high = m_receiveBuffer[ptr++]; - - bool isFragment = ((low & 1) == 1); - ushort sequenceNumber = (ushort)((low >> 1) | (((int)high) << 7)); - - if (isFragment) - numFragments++; - - ushort payloadBitLength = (ushort)(m_receiveBuffer[ptr++] | (m_receiveBuffer[ptr++] << 8)); - int payloadByteLength = NetUtility.BytesToHoldBits(payloadBitLength); - - if (bytesReceived - ptr < payloadByteLength) - { - LogWarning("Malformed packet; stated payload length " + payloadByteLength + ", remaining bytes " + (bytesReceived - ptr)); - return; - } - - if (tp >= NetMessageType.Unused1 && tp <= NetMessageType.Unused29) - { - ThrowOrLog("Unexpected NetMessageType: " + tp); - return; - } - try { - if (tp >= NetMessageType.LibraryError) - { - if (sender != null) - sender.ReceivedLibraryMessage(tp, ptr, payloadByteLength); - else - ReceivedUnconnectedLibraryMessage(now, ipsender, tp, ptr, payloadByteLength); - } - else - { - if (sender == null && !m_configuration.IsMessageTypeEnabled(NetIncomingMessageType.UnconnectedData)) - return; // dropping unconnected message since it's not enabled - - NetIncomingMessage msg = CreateIncomingMessage(NetIncomingMessageType.Data, payloadByteLength); - msg.m_isFragment = isFragment; - msg.m_receiveTime = now; - msg.m_sequenceNumber = sequenceNumber; - msg.m_receivedMessageType = tp; - msg.m_senderConnection = sender; - msg.m_senderEndPoint = ipsender; - msg.m_bitLength = payloadBitLength; - - Buffer.BlockCopy(m_receiveBuffer, ptr, msg.m_data, 0, payloadByteLength); - if (sender != null) - { - if (tp == NetMessageType.Unconnected) - { - // We're connected; but we can still send unconnected messages to this peer - msg.m_incomingMessageType = NetIncomingMessageType.UnconnectedData; - ReleaseMessage(msg); - } - else - { - // connected application (non-library) message - sender.ReceivedMessage(msg); - } - } - else - { - // at this point we know the message type is enabled - // unconnected application (non-library) message - msg.m_incomingMessageType = NetIncomingMessageType.UnconnectedData; - ReleaseMessage(msg); - } - } + resp = resp.Substring(resp.ToLower().IndexOf("location:") + 9); + resp = resp.Substring(0, resp.IndexOf("\r")).Trim(); + m_upnp.ExtractServiceUrl(resp); + return; } catch (Exception ex) { - LogError("Packet parsing error: " + ex.Message + " from " + ipsender); + LogDebug("Failed to parse UPnP response: " + ex.ToString()); + + // don't try to parse this packet further + return; } - ptr += payloadByteLength; + } + } + + NetConnection sender = null; + m_connectionLookup.TryGetValue(ipsender, out sender); + + // + // parse packet into messages + // + int numMessages = 0; + int numFragments = 0; + int ptr = 0; + while ((bytesReceived - ptr) >= NetConstants.HeaderByteSize) + { + // decode header + // 8 bits - NetMessageType + // 1 bit - Fragment? + // 15 bits - Sequence number + // 16 bits - Payload length in bits + + numMessages++; + + NetMessageType tp = (NetMessageType)m_receiveBuffer[ptr++]; + + byte low = m_receiveBuffer[ptr++]; + byte high = m_receiveBuffer[ptr++]; + + bool isFragment = ((low & 1) == 1); + ushort sequenceNumber = (ushort)((low >> 1) | (((int)high) << 7)); + + if (isFragment) + numFragments++; + + ushort payloadBitLength = (ushort)(m_receiveBuffer[ptr++] | (m_receiveBuffer[ptr++] << 8)); + int payloadByteLength = NetUtility.BytesToHoldBits(payloadBitLength); + + if (bytesReceived - ptr < payloadByteLength) + { + LogWarning("Malformed packet; stated payload length " + payloadByteLength + ", remaining bytes " + (bytesReceived - ptr)); + return; } - m_statistics.PacketReceived(bytesReceived, numMessages, numFragments); - if (sender != null) - sender.m_statistics.PacketReceived(bytesReceived, numMessages, numFragments); + if (tp >= NetMessageType.Unused1 && tp <= NetMessageType.Unused29) + { + ThrowOrLog("Unexpected NetMessageType: " + tp); + return; + } - } while (m_socket.Available > 0); - } + try + { + if (tp >= NetMessageType.LibraryError) + { + if (sender != null) + sender.ReceivedLibraryMessage(tp, ptr, payloadByteLength); + else + ReceivedUnconnectedLibraryMessage(now, ipsender, tp, ptr, payloadByteLength); + } + else + { + if (sender == null && !m_configuration.IsMessageTypeEnabled(NetIncomingMessageType.UnconnectedData)) + return; // dropping unconnected message since it's not enabled - /// + NetIncomingMessage msg = CreateIncomingMessage(NetIncomingMessageType.Data, payloadByteLength); + msg.m_isFragment = isFragment; + msg.m_receiveTime = now; + msg.m_sequenceNumber = sequenceNumber; + msg.m_receivedMessageType = tp; + msg.m_senderConnection = sender; + msg.m_senderEndPoint = ipsender; + msg.m_bitLength = payloadBitLength; + + Buffer.BlockCopy(m_receiveBuffer, ptr, msg.m_data, 0, payloadByteLength); + if (sender != null) + { + if (tp == NetMessageType.Unconnected) + { + // We're connected; but we can still send unconnected messages to this peer + msg.m_incomingMessageType = NetIncomingMessageType.UnconnectedData; + ReleaseMessage(msg); + } + else + { + // connected application (non-library) message + sender.ReceivedMessage(msg); + } + } + else + { + // at this point we know the message type is enabled + // unconnected application (non-library) message + msg.m_incomingMessageType = NetIncomingMessageType.UnconnectedData; + ReleaseMessage(msg); + } + } + } + catch (Exception ex) + { + LogError("Packet parsing error: " + ex.Message + " from " + ipsender); + } + ptr += payloadByteLength; + } + + m_statistics.PacketReceived(bytesReceived, numMessages, numFragments); + if (sender != null) + sender.m_statistics.PacketReceived(bytesReceived, numMessages, numFragments); + } + + /// /// If NetPeerConfiguration.AutoFlushSendQueue() is false; you need to call this to send all messages queued using SendMessage() /// public void FlushSendQueue() diff --git a/Libraries/Lidgren.Network/NetPeer.LatencySimulation.cs b/Libraries/Lidgren.Network/NetPeer.LatencySimulation.cs index 8a255010e..6ef891f99 100644 --- a/Libraries/Lidgren.Network/NetPeer.LatencySimulation.cs +++ b/Libraries/Lidgren.Network/NetPeer.LatencySimulation.cs @@ -132,28 +132,36 @@ namespace Lidgren.Network catch { } } + //Avoids allocation on mapping to IPv6 + private IPEndPoint targetCopy = new IPEndPoint(IPAddress.Any, 0); + internal bool ActuallySendPacket(byte[] data, int numBytes, NetEndPoint target, out bool connectionReset) { connectionReset = false; - - target = target.MapToFamily(m_socket.AddressFamily); - - IPAddress ba = default(IPAddress); + IPAddress ba = default(IPAddress); try { ba = NetUtility.GetCachedBroadcastAddress(); - // TODO: refactor this check outta here - if (target.Address == ba) - { - // Some networks do not allow - // a global broadcast so we use the BroadcastAddress from the configuration - // this can be resolved to a local broadcast addresss e.g 192.168.x.255 - target.Address = m_configuration.BroadcastAddress; - m_socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, true); - } + // TODO: refactor this check outta here + if (target.Address.Equals(ba)) + { + // Some networks do not allow + // a global broadcast so we use the BroadcastAddress from the configuration + // this can be resolved to a local broadcast addresss e.g 192.168.x.255 + targetCopy.Address = m_configuration.BroadcastAddress; + targetCopy.Port = target.Port; + m_socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, true); + } + else if(m_configuration.DualStack && m_configuration.LocalAddress.AddressFamily == AddressFamily.InterNetworkV6) + NetUtility.CopyEndpoint(target, targetCopy); //Maps to IPv6 for Dual Mode + else + { + targetCopy.Port = target.Port; + targetCopy.Address = target.Address; + } - int bytesSent = m_socket.SendTo(data, 0, numBytes, SocketFlags.None, target); + int bytesSent = m_socket.SendTo(data, 0, numBytes, SocketFlags.None, targetCopy); if (numBytes != bytesSent) LogWarning("Failed to send the full " + numBytes + "; only " + bytesSent + " bytes sent in packet!"); @@ -181,7 +189,7 @@ namespace Lidgren.Network } finally { - if (target.Address == ba) + if (target.Address.Equals(ba)) m_socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, false); } return true; @@ -311,4 +319,4 @@ namespace Lidgren.Network } #endif } -} \ No newline at end of file +} diff --git a/Libraries/Lidgren.Network/NetPeer.cs b/Libraries/Lidgren.Network/NetPeer.cs index 538a87996..78220b565 100644 --- a/Libraries/Lidgren.Network/NetPeer.cs +++ b/Libraries/Lidgren.Network/NetPeer.cs @@ -2,7 +2,7 @@ using System.Threading; using System.Collections.Generic; using System.Net; - +using System.Net.Sockets; #if !__NOIPENDPOINT__ using NetEndPoint = System.Net.IPEndPoint; #endif @@ -121,9 +121,16 @@ namespace Lidgren.Network m_connections = new List(); m_connectionLookup = new Dictionary(); m_handshakes = new Dictionary(); - m_senderRemote = (EndPoint)new NetEndPoint(IPAddress.IPv6Any, 0); - m_status = NetPeerStatus.NotRunning; - m_receivedFragmentGroups = new Dictionary>(); + if (m_configuration.LocalAddress.AddressFamily == AddressFamily.InterNetworkV6) + { + m_senderRemote = (EndPoint)new IPEndPoint(IPAddress.IPv6Any, 0); + } + else + { + m_senderRemote = (EndPoint)new IPEndPoint(IPAddress.Any, 0); + } + m_status = NetPeerStatus.NotRunning; + m_receivedFragmentGroups = new Dictionary>(); } /// @@ -292,10 +299,10 @@ namespace Lidgren.Network { if (remoteEndPoint == null) throw new ArgumentNullException("remoteEndPoint"); + if(m_configuration.DualStack) + remoteEndPoint = NetUtility.MapToIPv6(remoteEndPoint); - remoteEndPoint = remoteEndPoint.MapToFamily(m_socket.AddressFamily); - - lock (m_connections) + lock (m_connections) { if (m_status == NetPeerStatus.NotRunning) throw new NetException("Must call Start() first"); diff --git a/Libraries/Lidgren.Network/NetPeerConfiguration.cs b/Libraries/Lidgren.Network/NetPeerConfiguration.cs index b714edae2..3a124900a 100644 --- a/Libraries/Lidgren.Network/NetPeerConfiguration.cs +++ b/Libraries/Lidgren.Network/NetPeerConfiguration.cs @@ -35,12 +35,12 @@ namespace Lidgren.Network // -4 bytes to be on the safe side and align to 8-byte boundary // Total 1408 bytes // Note that lidgren headers (5 bytes) are not included here; since it's part of the "mtu payload" - + /// /// Default MTU value in bytes /// public const int kDefaultMTU = 1408; - + private const string c_isLockedMessage = "You may not modify the NetPeerConfiguration after it has been used to initialize a NetPeer"; private bool m_isLocked; @@ -48,6 +48,8 @@ namespace Lidgren.Network private string m_networkThreadName; private IPAddress m_localAddress; private IPAddress m_broadcastAddress; + private bool m_dualStack; + internal bool m_acceptIncomingConnections; internal int m_maximumConnections; internal int m_defaultOutgoingMessageCapacity; @@ -93,8 +95,8 @@ namespace Lidgren.Network // m_disabledTypes = NetIncomingMessageType.ConnectionApproval | NetIncomingMessageType.UnconnectedData | NetIncomingMessageType.VerboseDebugMessage | NetIncomingMessageType.ConnectionLatencyUpdated | NetIncomingMessageType.NatIntroductionSuccess; m_networkThreadName = "Lidgren network thread"; - m_localAddress = IPAddress.IPv6Any; - m_broadcastAddress = IPAddress.Broadcast; + m_localAddress = IPAddress.Any; + m_broadcastAddress = IPAddress.Broadcast; var ip = NetUtility.GetBroadcastAddress(); if (ip != null) { @@ -142,12 +144,6 @@ namespace Lidgren.Network get { return m_appIdentifier; } } - public bool UseDualModeSockets - { - get; - set; - } = true; - /// /// Enables receiving of the specified type of message /// @@ -332,10 +328,11 @@ namespace Lidgren.Network m_suppressUnreliableUnorderedAcks = value; } } - /// - /// Gets or sets the local ip address to bind to. Defaults to . Cannot be changed once NetPeer is initialized. - /// - public IPAddress LocalAddress + + /// + /// Gets or sets the local ip address to bind to. Defaults to IPAddress.Any. Cannot be changed once NetPeer is initialized. + /// + public IPAddress LocalAddress { get { return m_localAddress; } set @@ -346,10 +343,30 @@ namespace Lidgren.Network } } - /// - /// Gets or sets the local broadcast address to use when broadcasting - /// - public IPAddress BroadcastAddress + /// + /// Gets or sets a value indicating whether the library should use IPv6 dual stack mode. + /// If you enable this you should make sure that the is an IPv6 address. + /// Cannot be changed once NetPeer is initialized. + /// + public bool DualStack + { + get { return m_dualStack; } + set + { + if (m_isLocked) + throw new NetException(c_isLockedMessage); + m_dualStack = value; + if (m_dualStack && m_localAddress.Equals(IPAddress.Any)) + m_localAddress = IPAddress.IPv6Any; + if (!m_dualStack && m_localAddress.Equals(IPAddress.IPv6Any)) + m_localAddress = IPAddress.Any; + } + } + + /// + /// Gets or sets the local broadcast address to use when broadcasting + /// + public IPAddress BroadcastAddress { get { return m_broadcastAddress; } set diff --git a/Libraries/Lidgren.Network/NetUtility.cs b/Libraries/Lidgren.Network/NetUtility.cs index cf5996e98..600447a43 100644 --- a/Libraries/Lidgren.Network/NetUtility.cs +++ b/Libraries/Lidgren.Network/NetUtility.cs @@ -98,8 +98,8 @@ namespace Lidgren.Network NetAddress ipAddress = null; if (NetAddress.TryParse(ipOrHost, out ipAddress)) { - if (ipAddress.AddressFamily == AddressFamily.InterNetwork || ipAddress.AddressFamily == AddressFamily.InterNetworkV6) - { + if (ipAddress.AddressFamily == AddressFamily.InterNetwork || ipAddress.AddressFamily == AddressFamily.InterNetworkV6) + { callback(ipAddress); return; } @@ -163,7 +163,7 @@ namespace Lidgren.Network } } - /// + /// /// Get IPv4 address from notation (xxx.xxx.xxx.xxx) or hostname /// public static NetAddress Resolve(string ipOrHost) @@ -176,22 +176,22 @@ namespace Lidgren.Network NetAddress ipAddress = null; if (NetAddress.TryParse(ipOrHost, out ipAddress)) { - if (ipAddress.AddressFamily == AddressFamily.InterNetwork || ipAddress.AddressFamily == AddressFamily.InterNetworkV6) - return ipAddress; - throw new ArgumentException("This method will not currently resolve other than IPv4 or IPv6 addresses"); - } + if (ipAddress.AddressFamily == AddressFamily.InterNetwork || ipAddress.AddressFamily == AddressFamily.InterNetworkV6) + return ipAddress; + throw new ArgumentException("This method will not currently resolve other than IPv4 or IPv6 addresses"); + } - // ok must be a host name - try + // ok must be a host name + try { var addresses = Dns.GetHostAddresses(ipOrHost); if (addresses == null) return null; foreach (var address in addresses) { - if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6) - return address; - } + if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6) + return address; + } return null; } catch (SocketException ex) @@ -240,7 +240,7 @@ namespace Lidgren.Network } return new string(c); } - + /// /// Returns true if the endpoint supplied is on the same subnet as this host /// @@ -282,6 +282,18 @@ namespace Lidgren.Network return bits; } + /// + /// Returns how many bits are necessary to hold a certain number + /// + [CLSCompliant(false)] + public static int BitsToHoldUInt64(ulong value) + { + int bits = 1; + while ((value >>= 1) != 0) + bits++; + return bits; + } + /// /// Returns how many bytes are required to hold a certain number of bits /// @@ -401,7 +413,7 @@ namespace Lidgren.Network { if (j >= h) { - if (string.Compare(list[j - h].Name, tmp.Name, StringComparison.OrdinalIgnoreCase) > 0) + if (string.Compare(list[j - h].Name, tmp.Name, StringComparison.InvariantCulture) > 0) { list[j] = list[j - h]; j -= h; @@ -454,24 +466,28 @@ namespace Lidgren.Network return ComputeSHAHash(bytes, 0, bytes.Length); } - internal static IPAddress MapToFamily(this IPAddress address, AddressFamily family) - { - switch (family) - { - case AddressFamily.InterNetworkV6: - return address.MapToIPv6(); - case AddressFamily.InterNetwork: - return address.MapToIPv4(); - default: - throw new Exception($"Unsupported address family: {family}"); - } - } + /// + /// Copies from to . Maps to an IPv6 address + /// + /// Source. + /// Destination. + internal static void CopyEndpoint(IPEndPoint src, IPEndPoint dst) + { + dst.Port = src.Port; + if (src.AddressFamily == AddressFamily.InterNetwork) + dst.Address = src.Address.MapToIPv6(); + else + dst.Address = src.Address; + } - internal static IPEndPoint MapToFamily(this IPEndPoint endpoint, AddressFamily family) - { - return endpoint.Address.AddressFamily == family - ? endpoint - : new IPEndPoint(endpoint.Address.MapToFamily(family), endpoint.Port); - } + /// + /// Maps the IPEndPoint object to an IPv6 address. Has allocation + /// + internal static IPEndPoint MapToIPv6(IPEndPoint endPoint) + { + if (endPoint.AddressFamily == AddressFamily.InterNetwork) + return new IPEndPoint(endPoint.Address.MapToIPv6(), endPoint.Port); + return endPoint; + } } -} \ No newline at end of file +}
  • r&12KMjky{3mXH0+PTgAV>iqZj8mDK1sV{{Vh`)0Oi0I+D9!@A&K{ZuBXelcHLYsu&MrM-*ZxJl=SP!p%MX8UPYKz5Zppvn ze_F;ob=zM0({pU!_~CkE>o#p$9dpU!pt!u zIAP6Ok8WKi9$NMN^Uf3IQHN6v9v;<+4{3WU^3wejQ>(CZ_sZTy{B2_u(DOukkHEI) zGB(W0>KeE)zL~=xk;zpn3eCDC+Ll$O{4se=i>3F6e8`KRmGvxXiC$Kf)v0U!_AQNB zJ)`R@m(*9i-aGC*VmJ5N!z-_i#or^F<=qgp(4lKD(|2`0j%Po7V4S{t=NFIuhA(Da z`V=Q-ic1GK{z={Tq?407Em=N)&$q>~l^bs?eBfdD?)#|yC)Xb8z4~iN_DJr84`$7I zj~5RsJ1{0H{&nF@kF;gZt;;8EvMaH_ruA{b+XJEWi{fE>GFZ*Jqoh^E!#KG z;KznZH`5O!Y`?yO&*j+sF}?b-kL&osU%GJm&DeO#qo?QB1IvPEj{NhV#pS{Md?I7b zt`B3c({`_(eoL^Y+~thr>jB~2A5Oor`QOP;@9jIaG@WJmGNQh1zZsKzy*8d*;TAFS zZFEMeSp}@N`GkLjeM7dv$8T%@;6^vYRbP+Te~LUgcZ+WwRY9AVfUtO8F{s2fcxms z>iY}CVQmUe%=nR$^Jh=J{GLlc4>!8#8NH~?XJw1TK5s9NT>mj>l8HgUoY>0UDS7D! zx`1jxV|XY0px@f?(-PjnFO6dOLGSe7#}t0_;l~7i&_AOdGY8MCk5ACkG{j%P{|FBK zAQ*%vfzd*5@qdc=#^fiBCd6Mxkkeuegpt^XL}G8SvM8!V16OByz7%za)Zl3Nk*G6k zt^cFy?C6V423I;HCtp2vJ=^kNaqZ~U9`}EYUwV7(5wo0)r)Fnjgsb@Pv8`4>&|9Z( zTb2&^er4U#U4?sV9di49;}M2IuuEuY6OxKl{`KanYfWo!iSg?%I(0s;KX|JMVsL(`lNfPtDPHyH6ZzTYGX} zz`YNf^pYG4yM<}b^BMZ_>#p-geTLbu*;2Dkdr1{%#FpsnCqvl%jCTLKY_?DTMV0wc zW{00l>ypLZTvqt^{vOq<*~d~U9xQ9~ZG5+*Mf1d{ zC$1fI`@*8l-M@5sUo>pP{Myg$uW0>Vx&QB1t`SX)ZrCp!*>joai*?*}8VQ$__I zacXuhw~N-w)mmE?MAhxEd;X+N)D?C7|4+Yn4(ic)TKmQB%>qiU$K9|Ukbia7fGHF%E}yLDfyq17ujar+a4(m_)rj@F&LwR(C=>#%@Ygr4!qrDY|8p0TCTn^7mXo!W4CXOpAi zAMWKl=j1tj4e5M1PrTJdY`67(_dtDN%xI(cF?xsMt0r!<&Ds0&m0hddNo`GI!nb?x zHWC`~AEj~EdFVgsE`DdW<5cVBXGe@Vmffm_nKvTXyIv-$e%4|l-z%&IoP$=;zxeR z{kA)AnJ_J)ef`~nQ==xikKVlMzR~%OtL}fPIrjP0TdTts+4h@fa)!-0x5(F|-Lsm< z7qoVb5znxBP_@hbw&?(iKO8)(Duafd&}z!R89LL!u=Tj>E5e?I9@I5|+q$ya{ct}w z$III)gcEA@nWae>NVU!M?CH>KFIy!uMFqu z9Z!s%&|!Y%&DA{?e$Wj#u$5o==FP^!)}5PJkKJ+i&$foHlWJIkGLd!F?KZ`!%bt5h z1sVPu)h=)Nfd0cONABO((b~Lb$d$t6i?Jz-Z~uHg;JbNZmo+;(KiwA{!=IEo{Cvym zLxaEGL#C_E4AvAqLu@KD*3{Q3p2C`IM4i^S1L~CDf zmQ`zGM<X*xEZhOoU=6-o8_JfHhmW*zIf9hFfqGi(?b0?o3#=#_YkjL$mC9y?@*~f@O+6x6su5siVu%hF`1>=!K0= z81})As{P8hmp8bGz^@D~^HWGP;cbI9JEEcE3GBRG6 zYR^IPXAkQ(2O)@&J-lL>Ab3+$gANGwOqQ7*#ema+l_Rw)LAaqSH8p(tic{9yIayZF zyT{xemPURDmh_r@AZE`d&cFH#7CP;{`e(l*p?$~riA!FM=#XB!;cj4Rdz-O#>`NyW z9jaTIpU%x)W&XIWS?fHzigVwaW=97!wVmu-r!lVG&dg1_s`iK6uV}aGw8hdPO`?(< zj(Y9daeSKl+pIM4aeJF#bt~3t3r2rjJGfl$=>FJ89Xbs3pY&wur*3Autvi7i5majxH=`H&6i1uAl(=5|95QN=V&EfAaf! z!w$dSwi*=rQupO|DI5uYBJ{dtYS1 z(2>UP5?pOs1-6Q@+_AX+pTeRGq5FT~AUvt*8%6;=kQxlDaO}2G3w-;aYFuomUzOi)d!a?DYN47Y4@A&-h zNw3!-cewAk1P6_G>Us5;6~HwjX&M z2sXEdDN3Bo6IsVgFG;hG%dzJ0bF9-*`fqD+41*(CWX*w#ko&m}xtjrjpI{vi*J@*Fl0?>mMC%L@x0g&s=>ot-l>wxrel%;QLt+Cl z96~{5f|CvQ8AbMTLlaY9zqu_BvdiB6dhW_QAF@`y&%B()3OUkY#5 zO>0uFmUOmhR=;LbfoSUJlkG+rIxO7&*>CXM(S3?qee&#@WS#FkrrtRB!o8QJ4qw8@ zj7T^f@0?ZduHP%_}>qFKfVX1w=0)?asd+UxRt zeJyV|bZF7}t?kUYM)&+%!T)7e^Px?l1O`$Q&buJJ60nLPXQa>x9hI@8wn*ga+K zaHn}^i{9s-&$}aBoxGo2zH9nPwz>(q5-5@($v&( z0jXe-&n(TEoe6745HCun=rr`ma<)t0X7UnPeV}{VlZvthJea~y6B$MMo#jdv3W~Bj zQK+n*l(A741srck_nZ>RL4@nXS} zL2glxWhXfODbSM=TT-*5PP3+_Xt~1b4Z|4w7t3lv0We0@A$ybhvN`!uc7=s$al7D| z8r9Fd*X;issc-L59Cx+N=9Pnwz3Uq@KXr#jOnmL_k=a zj@2z$$G@}6Db3eZuQtTfTwv>G7rA)uJb&@DDE+JD7d~jX@8>kRH28XvN%)<2SqHmh zMz5RKzo)^N2~8Fs+uJNK;d?NNLdSkN6khNDT08xG{{x*( zii~|-mOV4N>zLcJl9k(X1FT(HQ)m1awY8Q3bq1lVpo??0x`4(c*T#n0j^rqVb+;y~ zz4VK&qbaC(XE0~`IQDgP0*%jdkbc#1?4)C@*FKNGsr|{6OT{Y&zDezDK0U>JtwwEG zQSIsFPIWKh&ay^GMzEolV=&07Zch_!t+qNXYc7u~9aML0&_83#bEotgQToNqY6i=W zvewtJ{?j2LLwr&E6~Bae=LA-)kYq5jq||A@ra zp^-j80lwCq>>M1>l*Dy#@Qd)X4h;$jkFbU^931?EJ6U(4RXPADa)8VZu$UL;pz|7b zfMA~>PUr;H;HAru18>p(te26YM+!H`D?T9^;)1v$4+r!FFF8UX2mN8_fwp(iACBT7 zZURu}A-k2h?c`p>+U6}fje_j?=u zFz~X2X@$<8^Yn|i3`{Vp93Ok(!`=P&l1`uBTKVs@@8)IE#?P6xOUK&(#qGO)bWWZBc;_Pv$0xiW?T7igU37c4z$ET@`KtUHGcALpJ-p0g z=7$Eh0Yxqutj>-wSiyCmsbw(Sw`N|7M!$u{xyF~k#?*!-@=Lo zO>kskE(@^Lx?y9%8YN}|`#mlFf520P%uG-^j(x$>?Bm=UnVHTkS7(@{;L$CX<>P1t zS_-oyTS@6cr@O(I7Cb!Aku(#He%%|kVdl%dJM8kZoHlZ~xubV&<~*O?=GNjC727l? zKMb(iW%RkV{_Lo%P3~1czMES`7Kn77Y$;#0O=#ZjR>nvN_k?`IRi6F&T3GfI*?hZH zIC@)YGsApA*}y&eZ|ru)Un%Mt=$^ZDZ}qUBm;I~E7xZnrZ`ho97XyP&PjqFKlzfTL zTDrz4^P^9OeYEk}RW&_xt{mAq-|YAhoAULJK@$RVZFFuODX7oCcem^<|K7Bk>Qccs zH{-r7OKUrNY`$SRTyK1nFU8keZTK2ATo@QoVB4mn@KO3l(E+1jESpx@jzb5&n?O0%*bemSsztFJQ;Dr&XzFEy|7O{C{n_ z8L*s@N!LNA7pn)$wzz9?=b7aEBI_4~>=XFt!<>Ushr|iK$(GEuNgYm&^jvItsvmpj zL5o}kdymFQ<2fbtoVla)qrof{=Xb00_A}afzP3DRUUcA%e@J+y?Xuth{HwKb`k1%zCR;1t4vwDw z?^DZ?m+sSS;{wlDt==4a!JA!rxA zQPKP6ulMO%>bj-5LHC{f$~hJ7hHrRcx@kjAeVl!_rGDApJOa8dcrobZ<*jj}CY5EK zXx;bt;B*VKrBCg;2WCarZ^&p{E4pR%?b_LjRd=fYUT4+$Hf!panZ>x9nxdmSGVD>UjGD!yyW&tXq0uV{H8YDZM++UIYNSAKb6 zJ-^Kaz_0#`mulXntz7{j9NHk=-xM1MOC+S#y<{QeYVtMzBQgc!!*P_#D<09Daedn89%=~SoMpMh+YL9bYpVlAs67Jq}?{Ak4 zt%@suOR8wPX{8l+$755LkIVDQE1Flz@A`-`CJghPv0}8@oyu;PQ^YQA_ul>uObe&S=o>FtH_KVZh(@rIWAI{e+wRp1QY`?(!5B@UQzGc~^ zX^oo(_|B!ek>0pzfVKx4Hw_v$4gRm%G;qw->JOGoKg``U`@d@~XclhPG$@zlAj zwvv@GIGc=RR=~>LP1p+7MvAH92;1>3Hcq-Z?dQFsb>-2g_qTjFf5+v*UPxFM^Pe%SIG!iCGH+s+h+xX*}eca^ix$V{qLl(Up z>=IzI$>ZK1p)(Wx!nErR7d~2GF>Iz!uwG~V_Zoo@fAo89zI1Gl?|FZno0b#JKU8Jq z>b3iK+m5EY9{fBKGwJJ#?jsZP7DNxp`{KT5nx*;smJeJeTGcJ^*w$*&q3iMVDRSBbFU)rpC_SeU{n2+;&KCTMh`gmPv{etk)H+SudZijF2tyoN4 zMmBzGnWh=?dIbzH$M zxPpUPw-NW?@tL4_bNpGW%<-;g%<&!z{FEv|zlRp?4+o9|YSEAJiwK|Gi|`!oX@5G8 z@V$1z4`li|4->wN1I=-a-|Io#URzkE31T|c0s$FUdl_befN(nj=fFZ%JG?kD%q9Iq4od34eq zJ%suHMhG-d{aD9*x|xr}|4Xl5lxE*dd6sZ_JzQR?|IT&9->#JRo;t$YxLzdxIVTam za}e>D_){NXKCFK<6aCm@KH(jEWXiMVal$8^gqQWZ{RzT*$_PJ&`3zc3_})>O`EK8x z@IBv>eq^~SstK>{o|*4M<%Ca`X5xQZMtINunfT%%gtvX4$$#^GgtyNidZ~xV5rhx- z&BQyoTpE{aPcGL6#yc24iSg@eiBEjbOnG)PKB3^J-a+)eQ;2>t^KqO?`0l}kha1t4 zjqQvdktye?s|asrJ;RLkOfknq~TO!=#BBYa{q z;iW#?w-CN(S|?*$hSzDw~x?Udp~~> z>w)c1(r;ipjI$l0o6wK%4`F&OQ_im+CVUsS-;_PDojVo&TUgJ%ik|;9j`;K_a=v#m z;a!R!u3~+LS)Z~Wtl32L&Z(Jl9=nn7amC*5c!=paE|PY>7Z{3m;rTAJU3VolZKO6# z^E_Ij>t#HtN9%)%5cHitXVQCX2(Kyq_#v%?ckM%RO8xh;-%cv+bltv0-=);=KGO+r z>yugD-^LQYQ_-jY84XjHgSD;Ck^C&aR{03g_i()r<{M)VB7Cpn-*&M5*cCtaIqN^J z=>Ne|;*1WeRw?SQ^ud?))8Lgeo*o`j_WI~)YlES61|=K!3nHSxGTs%KyzI~@=N`f z!%^#gSkc2RLB{V#^2mJeYbJd6uuS|3?S$`A`n40eUkG!*Ank2i2hoR>ID8oQcb!W6 zJ$x0*qxkbZn7>Qme+t)cTq)PX+>dl~KO*D0@6TZQ75`Ar?ZuIm>vrEzQCv5)-#r{uaKi{j^|B$1Je?sY(UvLq=ONrn7*Rz}xNS`oE z{W$3!!aJrDUd+*pW8fOXCr1-r+SOlgCA^F6e*}?h_g=yL4<>r)2gW_c^lrk-es`lFyNl~x#^HZqJ`N>5DI3l5D0cV~^Y2vnmveh} zaC;xcdKl^>J`Tmdt+|Wvy$+I7#wP^R|)Zv_Va6q@NvZt zOyzcw^?Oe9gi@3dbxV^w#>&N@sMBKHf7Qe5L{vYm6 zKU{wxK1uGcB_G$FgzscKk#a6(|LIZu=V1pDeL}IHFOi*)v+E72mm!?*E$o+^%6NG? z^LHuy-&;<6oJ#*VW-Q~CIPJOlgbyom<5leUlI-{PVtJa^5Pc8(VJYW7jv{=g;{PMp za=vWOvR;ny6TVC7XUE;g{MipkJsd2Ht%#m*nUf#{P;y=+6Z>i+6kl5;oC*ABr1zV}_i*D?MvbW8$o=XNLU`96++ zx)uBX;!i}MIFjr~_RBXvOn8mQv*_mZ;{*1ej{i`;lK-rQL~m2#s9D?|ySY6|Jr8Gp z)y4iw@_FqV=A*R7(QH31#eNRIoas4^lKlI!ecBcKY~PQ|rPRBFY|1I3UoM(`J z?xkXD`*Xh;SNhGpMiKuW9{)-^tUs9LD+u4k<&u2bSU8la%}us)@JhCW7k=dmOw%tk-{ z$?<6U&r?Z`CF4F(!tj})_B0f%KUZ~&wgzr?=L1u7&*QxY(=bppzEAjsi z8wua7ls9=H;S=nCP;&kFbA<8Add*eG6F#YoBlbCy@SRFOIHiK{J<53Y*C^o~%KSs~ z*~~|Ymv*w9bg`XCIcIa6lTgNY9}HtYN?h>;`&AqJ2WeL`IWCPeUe?!ZET^62l=g5C z^LHuseAXPw*E4|XSMtB;7{c2XJ1kyLcuncos#+M&b}somcPHVyI9?sh^)-a+H>}ie znB#w28OZ~;qaWX~KkQWeVaI*M-=^5xZq0=6RK{7SazEb9>z-0?i7uiKD|{MQpWTXG zO}vumJxYJqks!QNDc5e?j*|1JTnA8d)s8-q=xvHWj6XzpJI~*u`t)M~w_lIae)qbR z=v|6GoHLT}38kI(-^BS&C3&PCMm|jVPR>{2kGzcVT?+r_xnJmHzq%L8xgYm;arQsb z9=^Su_#~Bit?k?%dzAKg_sK->RK{6vy9sX_O7cs;{l+7N?>Q>dem3xUM_WMnA6d=_ z(`!sG`@8B{#7AR2%XV}e+fSEbKfAHtb}8-UW;A@^pB>8h)XC+tbGan{x7bcPPbN8k zVL6MrJ@zW`-cq((k7Bn6bNxEFex?39*l*hud-#mUeICV+4S1B~@8)sqIF@HD%c(8Q z)Td`W(Z{(Tm*u+7Mfk)(qL=dccpT?Z%5^fw?QvzEXZk6`C#=}RyQM6rGH-d?euTGi zd?@`-i0doN^(Eu34--W1QQGefJnraJ`s*R=_j(mS^AX2~PS%6uAAZJsonq}tly8^f z2kNU_B`D@bl*rzDJ2q-u4o{m)nu7mx{ZXUYTdGak+Yx@lL;K zMBl}6uk;&da=#N+`nA_55xrJK_8{AD*9^jUD&y9VE@63;@z+3Zj|py%lFxs+UYttY zbsP*pw8td-E7>p4;Bsk7JpbZUqE9IC{C@0zx|Q*WRz~zrC4N{mkMJHP4!H>e3wi8H zzjp48gzr`Q+09(AuN>c@pc z2=7$JK_ghtE+yW+g7fWF>M_(td=koh=_GD<-HLuT>`(NaiXV7s0pWXi{!O;`qqtly zrCi?}$n-paDE-(;+#lH#J-bgNdb{GsJg7KfSBV`(x?YZC{T#52@V%^m*&Y|NzYQz? z_EaR-?elHo<77TX93MKBcJ$CuL?2iBm+%>c_wf9@jIZ`)`87rUXSg4CDt>tW-NeVn za!P&v%=3N@#ZR7cG0{5}KlZm%nGgFD$!Fn2mQ$m4A@SoG?^5u4JwWt5T#tvae&%pL z*vsQD8K=E9is)U+{N#4#-^u)?-*|EZ(Z?0Pl(s$E92HBxIb7MsD&?~`57kHt3&KR8NQY33(ZhJMxIQ3H12nXGk*1Tgzr@5 z6~C(|eAiwir?jgz_Yyuao$%71|1yg39>xC0aeR_g?Ee;yE9^>KaVq=CE@ho%U)E2(M+l!hlFBQ`HE+!(yq)Ed z^*Hey!uKflfBHj&mk;!znEDako%t(q#SZR|G^JnuJI|}w`jb3T56>P?e7Y5TTOT2O zr!wxY=6=M*{m5LF=L*(;Qqlid_A{E|pBHx!eqy-yK+Vj0!r4#pRu;6iUb%J^$I`>QVYlW1=G z@dd{ry-FN1>Q3SlSH>N`p&<+YNu~dNg#AE*+lADBl-sFIX{Ud`miTy-cC_Y6!gnpA za{a<`PCS(Gc4eG0@Gz!V>V0!7;p0laj;+jx$I-Gqez=bC&T*OT;$n^u;~XFE%koU% zc+#%KlZ~T^Pp9I?mazTA75kadK=d|cAJMi(!Y7pVyOza-ciA%K{AB{+o$M!N|8-Ou z(<}YpfF#Y&CvPYJ4|COzBPSDmT+#E0nS{5OP`=V%wR69dIE?UUR{C+q=|tbDjLTNp z2_II*P0wK@Dj*+=|c z%wOv55st?+B_8{^lIT53ztFdV@S4&dE0|A@qKCiVL-b*eXQV!>?_)k3|4Vxdqq*t* z8t)rFgKxB+OL&hxQ_oNA!+exDd<)aZ75ch!h`v{87ehGTM40&h$a*gE5WRgU;iX+A z&mep!+cU(_kLT_sd{QazbncIw9^&&k(-pziXOYs9OtY@2|XV0U= z$HDVj2=nx#GC+8{vd;d&R>F62|2~N6KVd#S%m-rY#~<35UeS*>kMK^#-g@D;Mf>gj zg8HM;%;y#a5(01M`65}~<3}-`;{jP;ZQP$amHxDZ{YcjGT6g}K< zJ<<0l@!04l!YA3k$#}b)=YL#EoPXK@MDJAM`95*N+t?1J-`l^I@UA_{P9*;M6@*VH z{po*M9=l=>N3tC{6+3)`_25zFfkTbNKhEWn`OdhX^HuzC!xfBI#?c??{So#7a$F?q z_k+WUKB?5#r$Y(f!|h$xd+F7L_i($Ge)Tc7pYDH?{rrdRXEC?ygrd(<_N(F3NT1TL z{%|?*53~Hzt`C&)+m-d)<86fR;qpp9dBQ=2*OWN?fz^a} zjLvM=cX0bka{H3@zsD@1?^fDN6^}!ExPO;+wcvK5_e>%Fleu1oa=VBt?czD+?@;1| zH)Q)_|GXdbxynQQ<4S*Y#|4B>j>@#RE9bL3JYJCcpW-Eam%`_AC*kc%|2ymi!gq5V zBKfSsKuPZ}*dI##c^e2HR{Z}y?B6_!e>@x~*;C zII3G2*PP1nj6)gczV9Rcam8=%-o^PU_VCGtgzr-9=guby-?@YY`GxE2H1>NvETG&U z_&EF9ZpGgw4<#rp45^nY9l(c3tV zg&FI|`!T}D75=YmCVY=FuJkq$zFV|TU%*E^Uts6BVif1QxQ@%q^7Li=zc3IMe7bmi1b3|;vv?elP{t9-)0tkG zZ~63Q!Y6tBEB*YMHH6p3P`RX>6S;V&r{X6c;<(Yn?NRFa zgPVwthvkv^?hOLHyu2PH@h`Ccd)hMfzX$8brHmir?B6;$zB-QOOim~M&OXFP%D*N_ z@kyN5J7jy@x{d0~!_R}*nU9a<>{jGF0Rjv8HDw&S_Y}hS6lLmp1W0vyW&R|{c)Nl> zyqV||ieEbJTEaUNKhw&7pqu9hz*avVnZWcMA4)y^B}Diz&r?bKWVX+4woj>_AK6a2 z6gvs6Cq8zi|GFIsMR`5SJjP>OzrBIX`u&RSyoc>v+QTvD5+4_rOX43smGBzd{{SxU zFgw#L@ywfCF1s?mxQF}q9;JVOljZCVlAJr3|L&I&|AaDbZDM~LSN!eHbBNxd$lt_% zuSfBFXB|THJ#6Q)UhaaU5c&+iSENfK-a96anLN(&C~?IYjyE*MOFlo zhL!m3|G1yh*#ArY_xBT@gff5f*olO9D)R%!dI;}fJxjkghx^UAV$TEaCHlA$-~POY z`73_x#?uJzQvBf!4>0{)+Y&{tjn~+F*StmRptTC=YG?}{bpP!AIE!KFWLuG zFQtUmzH%{t?!V-?z|HYzLWw`CpC-Tbsj}|#!)BI8>3`Sy2=7t+!>d~euPJ(~`V-;p zye>JK<=nyXe76$6Y1|$=m44@Y)f(KAP}7 zO5FGbj|bxlpC%sfxRieUI&K$Ti!=4Vd=m5Lenk3#e;>y3D|+bIM0l4{-mkbGd$}HE zd%TJLNw?xpu4OxMDt6*Lg!p$We)5A6ERQlS_>{}lc?y;5bFSaX9QQhvxOXPovrVz* zl4q!$+LhFbcPGbE#6#egki5lG=6#OY1kM*#g zrCnXxK=ckCCrbRZDB+VzJh?N@{1toHj)Z!?pHjbr_%5X%*}jVK z9wm-Cvz+ObcHK3b@HVAgAF!10NnZDq`JQzX;hoC3bsgJzui{TWV!!9$aiX;Im+ZtR zuEZf7cM#sr^FL@-`mu`bs(Ui&e-h)TTuk&1W!~u))=#&hpJkU2eV0OiG`9<<(k^Nc z@CbYBQtbTO>4bOnA$erEig`Y&Q|Z@^okH}zihp>P?Z21(z;G_tAg-70{={Fl$GKcD z-AcWbT}b@HiogAZ_2%IADD`PGvm4-obh( z&Xn`)qY0l>`nAU|BYck%H$E)mkQ0f|FPyLac%n}zsk8G)6XKjU0IJFu#WJ(Y*(_q+|$YQiabfSbC)vD`FfD) z6+1i-h9Ts%^Sr@e*3YF=3E#>3mv(r|G{W~P_{T3Nyp!kmq?{A)VgAba=s#h?ce6aw zk9~L%;X8S}DfLr+FXI(H4{arU{AkkuSeE~xam=6nGt5js?(-2o%>9Ba*Yn)(cXPil z@fXcwdc{r-+MnwcCihobYy~f4_(0;VvZ}_OqTl6+U0H|LIZe>f9%h_tJ{_a^?m%KFZKSZ@i|oAd+q%Zc8m%xg7YM);)SZx;?Dyocjq zneQZS@15MFpls^C7wKX zHQ_bxe5C@D62sJ#QJ|H6D-bBkPO%)1DKkJ$}ykBOWCBIL{ZAFn;1O%x5>Em-TfQ zk2k|gT)O=NqVHDxVe^s9hvSOA%zt{4;ucL=zgT!2(I=IBZ#{?bHhZQ#7px+@Q_0tk zj!)>VQ)!RY+`nr||6XM$dXEy{R-z&VpD?e}Nk1Io@s2|o?>x``q?`ST)Z4MMiBAv5 zhq8YEH=povZjX|_j{A$G(qFuPHqj@QcxIn-3GY<&Q|lzWO{uRTn+e~k#M`sEztFh9 zK=-O2$FSc^Dt>R|CCo?ZH>Yo-`kl*hsf?4aX(IX_j-O?D*KZ_zLYW8n1_~GYcXB)+ z{pZRK!n>4y(8>CVEBd*Z^%GX~vxeKRU1`7PK0y2(+%9%!J$yQa@OC9m8(m8HB){Jh zVT^uU$NEVs`q|0xq=(~4JJX-X?bpujcQ3}jc|Gy(RpOa*xxI8N?PVCZ_ud+6FLK=7 z=VF#$k*DZN!n?R#$aZ@B)r9Zjc?M~>uX25Laeblt(~o%^zlD|f?Smo2$HVPW+QYpG z&R41Tm28K0w!>SQ|MOg~&XcHIzc9Y^TH<48{YW`?p2++;{*?K8x!!GD?~;BR`~NVv zYe~Q60Oq62$IhKec!x4?d@#qoaV75E)qghu?F1iJwaJvfm%_1o7#05`Ir^M;j{$uW>&m%Qb-Wbt(DI<9UNF z?%$<8YqqfbJfA7~d{;&IF6J-o;bfMxyEaqK$+L*QhwV_(d-VQgPc3xqw3vA27< zzw1%@yXV=i5{lifV>{7!Kg=P_zjY(=?^ODoyVxE)iak8%BzlJuN6oHeypr!~I11sH zdUlXKAneqS9qi9F?gyoQzGeTGQ2d()$1eCJlyP4xx6@vhNBYST9-?=$-;?%s1kZ!n z6o0bUDx%j$QGLnw`vn+^d|gWY{(LXtyOsH(LrNI`7M1r)E^oiB%%A6xP<;L9LSUnp zOIcsrpX0qAW!&^L#}!>lTyYh*7njmr&ie!L@8Nhx>cPeSzmxsH^yk~-MDJ1NACA6& z@SUoD7~iAdHMct)YmNB4P41<>4tFE2Vm=^S+Co8hH0F)O+-|M0waFjR=G0V|SGcE5 zo;LX~E#PlxoE!~Jo++-PF`u{1?G5{Z(cJ!^Kc=~xanjt?_4D0LzKE~I51L5*{E9#* z=&ScO1dxTeXMV@sxY8>acmw`3g)Ff^)ZOBXxxGzIX(^^_fu=(P{$TscLs!n4dFad; zLY!$@q)mw06bOZVK_OdHfbi`xf159==_jpisUT`w+U#!*X>~&SGFP>GQBaCg;SB^D zyp2n>h%e}E^J#7Pj|U%`BR*f6yv`S^k91UcV~wqnK8lQ^9c@~huPy3}rAcd90g?`7 z5=DfjS3*}SB7UeGvT{WTfgPIr-!R07e2DuwG>#GXz~M(in!pfoZc|S#I}=h&Sw}KTtJxx;8fkLQx+w zio`H}UFr`6^n|Dxk;?09Y+mVgNfJrqV0j0 zXfC?&sg#)wjHyz0=X-6B1_ZRC`6EDc0{yL-D-?wRtQnk z3w$eMQgBO#v(dzBywUuGUn&SuhmrXbZJR^)@A_ zBt>-)eE^)gy8>m5`SicdUg3@0Xj;LL8!gBi^M`_PyKwdXh^)_4!rA^{)4~WP7wpq4 zxO_5$5VBes(H*w&7_BDbZ;+WcmFP`|CSodW}_^F@~XB5r8X;9~^a z<;|=rGb&vLnW7oU(u!4(x_(>SVLc`JjWmT6nY5n9yE9w4;YN0YHQX$(^EI|d*k2m( zDVwRx$_TfrT@}LgD?;tkrJ2@InqRu6WPNDDNOiNLGwKIQ_Xn41FdlG4KdS!=G`0%Y zrC&F;wuPDu=4Mb38;S5bRORg9^3hdv!o|0FmkN;t=jLdB#)dT18c8VG{;VaM;|uyC z{>HQk7NGh%2$i=KY5f;O4ws&nzXUZ83y9=CxSYh<+^? zZ4DJ#C~u^()xxMmC1y4MEbYVemUtr~x|lCQn-<^PP{`1aXNXxYT~~E;8M-HgZ_x@7 z_*pgelo`#fuXp8;l1vYMK_nCqePkYuE}B!3j|5d%gP}}5via>%e`7&JREzmY=hn}! zscVnuon@XvR0o$Mw8#k9GIX8N-C}=}&oKBgCvtg39Ow<8He;yDyyjaS#9$y_X}~n* z$5<>+Nk}!WfY%Uv>PduabT4;Io33)uZ=itGMVon0oua%Z)Y9T>T4;=i(~OZ`|AXjD z5l~MoI&D9q7t!{)lR(7cuB>v^R#lYMSDB}(_eI(;p55hS5e#);bBja+suC4zDicH& zvQ>Ix-r1o524$vH@KKT2;*h`5SLu5BN;3VV*n3B^tOcqzFKI~D!a1wSZf_vqE_ntjZ2NgaI$f|8in*AB#POg z$P!;eS=evNvm7IvpnuS5rPhxVheF4QeSE^+7PyzA0gOb+p{q zie7y|S-mMiSrY=*m_MppswqXSZ&^DA09C=pNJlv46GM*N8Y`Sqw2pFbQ;V<0x7=qK zqi5EDXxqMsZiCizET!n^d_sAPYHLhO1BJL+F}t)FeSNb(yYJAe9YpmjLbJV%;8qnB zrkmBrNIIQabrag0zqv!iDQ0a&W-5gAW0s_aa_3=;Sqd25d~XD1SrzcN_#6BIf6Uj+G0Yw&7c}I1y$+yx!( zt1Anw6Qa*$S&A5#eQJO??us;=#4J#gN`Z@|%N#~j5%31{Wh-d%7hO>0DY77`_hva6 zDKC^FD9RdR{^f8Ox>#8Olq9aicsUUAHWi*CrE{I}>`17sF4P{u%uGI0Nt5Q$LbW~} zR2|IC7ha`0C>o3&57dP*6=vdjWjS?ia7?ujon1zxnCW3mNb5tw(_59$Gy#GHm&sr& zhdlKmz2CCRMR-xs2R1G7V@l3K*(RbO$j2OGrQaI}~>*uGeV$9t~ShVV&Mvm?sF8(z^#zh z%o*n~XXGu)gsI;cW){SprDbbG9oPCBTU`iqePZdwqU;rzc#jl9m};xPoA`JiIS?yR zOmje@JUZdpBeN((II?1)M@+Njh&4n-hr(zl#5!*+8ffztkr>xwZLmPnT!PLE`GdmT zDq6j<+7P~AU{TjKKJRj0A%vpj%#S*?yk$quM46|u>M*I7X01=qSV@~9L{7?TOxsZD z%bGCUwPKhiTY~XRXvIP-SD|MR%dl2S1c~HLiSi`S=Z|(N1=^|bHohPRU1Vrcp1HDY z+O@uLprbxS)J$wuf>fg_fDW@&VqGX_WmiWr)CXCP{;?|9-sZyy0>enA%;iq0R$T23 zf$XU()`y#DbQg)fM>GXJRLoK41?_F3HAF=RCzk)bO|FPPgcY)me9ATH_R=##^`RDD ztW|U>`8z_n`0k2?Vxh)RpgPFGLU|;#V%I_dV|G0GQ-=;9dvGWf#y+tMqr-66hq8y7 z{Q+M!7H}<%svNPY34IP78@S>!j8T6_ZmD34CTSM=jnhkdb6h0m^2KP;kTseIK@D2GZll_4-<~$;@}kpsK{ux39@SU*ik5WXJqD5|~b{ zr|9)sUQ}J<4YstK3{j-2bTZ!cQ5{@dYhID&7KPmdCX=bU&UiaXb+Fvu;zB$OrOgjD zS+otBhpw%}=7olkH)3IV)zQVZbJ1NOPdGD#2Jo^uYZyZBVhWbJ0<);z8gF~B5gm%T zLd2v$OagT%s@j|;-+eTtYS;3a?5p%)yy;|42!hrHylT(x-#B}MrM1Li(r6PQ&O7=(rqmVeJ<8z zO-)ADxkyD&vD{ZUwLWKF6KZJaFpH*)@sI$yPqz?`AQW;0id5KXH*p5!drOzdREO*XPq_OlefmnUa#TX7p zmmwdWk)P=Z$SU&!YN`_38)Lp*5`*-RPiR@VLaBpIx)5S$G#_8?(Osvd(@T*eipUO0 zjp{JEn?gkvy#R&sg@{oYb*4IM{B79ESwI;{dwIzv_j+uD&c#;JiH4_dIWIs2)7eC$ ziY3#62(g>ZM>U;8PEx+;OyK6im`U`x2}OUKvSV|DJeG+?yzXnN4>6Nn%fHr#y}Qv? z;WkBs$&uKFfa7`Tl6IHu(=14+gXh&F?Yk-r#j=cyDWZB5yN-;<1ITz*$l z4nm{9CowP7{+^^*-)h7neJHYviS;e|tmy*k1RW}6Q0a@oDP&DNWqFgRo3^T)(2i+| z0y1D@DwMn$_OCY=lX)`}SyJNX3kIl=Q8#m^zXgadd{N=V`j(Z#HA#&17Fd8Nq<2cA zs8es4)ccCPnW8~XRXG}N!zZTyVZ)s_d%`h&AN`HsiHUE46H~Q%$dQA6MuIv##*l-y z49^pU^}%KiA`ETCrWKKY4ysxoW?QkNNDiVyL5rF&lnpzx(*JVsM#nDb=7yrN0`!Fy zDSd-DX4vwJz-?9bHl*epq6itlzu6yY(?u5>_i~Vv)$D2}b?prZ%Sa}4rQ(4dB|b{4 z&CNa8l# zctL5*!{$uU!8^Zif(#X2tTHr#y0FSjk<(+Ixy9Aj?xU?s@ca;-^a%??$en->5#C4-o(eV>V?BsH9*gmSUO{z1xo7BA zrq8V=;nj?_AyXBUYet3YkwufycjVDCSElJ>WSN!*MP>*o%vuZx^0mH{d>M8VE$(<#i$D~WPfx&Ank$UzUiuz4i=LCs9{W9(){ zu9rq=Q>;Ym8-21S`OK?JqJss($|))d5MTNKQE@r!;4y4 z+42_g2$|q&oh^hbYit$M4f@L(l$uW$6&V*qr||;Nre&K=A*{uQ1d=Mh(MrlZ#>PAi(&o+IKwDtFi&=vh9-Gcn^~P6cV38RQiJEV| zN!gP~P!J(XX`stpADOiJdZww1lnRcDH*OfX*M%b3-yCbrehY+E3N~D`wZe1~IfO9e zBxEyrNV$+S7$6C<+$Jou#QI>mHcS_I(^)PGi^qBl^mTZSH%C)MYxOp^8tBod<+MPw zK|E}=u$g^4Pgq(f;pRo}$S{~L2G@9gHHPV*+)9{_iB3+|WJoR^6|v3~79n2c^Lx`8 z@{w7yxYk)_lrv95gLtG5K?hfo{bGxBg=WxJh7kAXsLc#wUUu3Q{+y(MYz!)ru3KM} zkLLG`mPr*?Nxdmvh#@AAXQ`UUZr7+A&(@{`e=CZb(2B+W5Z<|!{bY?~QqcjMZWOO9 zQo3+Su(+(E+>6=#tf?TK0BYyvAm+oZHdsp1)XcDvII9uJoYUn4#!9@(0A5`Z6)$eG zmaxhj!Ry7aa)GDzTJc&ZY=mBE7A|CGES}(-jhBNg@%fhGnGjPUpn}R8^BD_bNCaXk z*kbBlC*x9pm1!=JnTlyez^xFxV3ncfh0U_Gcs9wDW4h|fF^6N;GwE`P=drQef=55F z>Jo{xhqHo0oebPk^-$>xo4iUzlHn}NYJ7DU&G+j>uEk?F=NZvyc>Y5!?-rP57ZUJ7TVBRYC$`TNAi6Lt;eoMNJYOlRqk=GHRt2Px3-2Nw zJ@cJMkxCXh%*)6+2N^7c%4HTD8me1(MT5ffQxn&8^_h(nGWpN6+7D z($GRT-lWC8&dSG7Ip3F`%cxIh0XIcf>{h{$Wl0zP&%2UumT#X3oK*7NhG03Np;UBUaG0YE;>Fv+A*P^hNIu0 z7+PHNU{SS2oeGs>AVK(rS2D-+C)5fc=Xgwa;&_#S7~qAn-#clNk9eZgM-RdkkPSM) zg%LbRO&db-AVgL?!&wBI3}vxkoIxWt=jczh!t(V;K1>r;`kL`Jz&wiN_bK2}^n8>k z4v|onq>IcL)=-V6)YcWGke7#=ItpbiXvM(7R0qtLw1r4ZDlBIre%o?^3Q?yV zli(yecLT>-?+RfWLJm{GsToutR`rYh%Gi2u@}44{FH2hpsc>mJNAZeu{ksaLd1p#m zAY((>MCe}Dg84AB;ggvz^w%HyqKo<6ixuL{llq$ovH}K^)MX7(M4~YtH{&w5_PU$WpLrJk%P?C*W=(g}Z(UChoal~x3$dC#-xbq;c*dVL9^mr= z{$|J_rndC=>Far`98;esFuhy8#EZwuL;8DL&GRf@;9HR~R5vGsYIywWYZ9hLuRG2f z7En1TkgUX>t45!A1B3{`a*|Sk^|#OFp@$qqEbI^Sc-gBNB!cd@^(A64j0CZDvFJk- zDX!*(S^R3mdqp+8E?NJqh6Vdd>_*4y^o(*)I@8KBPzM4jdGrJp+GA}j=e;f{W*+KF zybu)A{KPz${=hkd*35a5rd7&?m=(Y*ySW-c3^xlOD{Rszi`l3r#lH++bO^N+B(#w< zr_4sed?L>Ybc9lW`Cy~5mG<7@FYX8HY;PEu+lwd9c~O>I!6 zwa&58BWzUA{E*n6(w_D1tdunK@md%>ree~=5(!TgEf$kXX7f7cR8=c6D{1o7Vg_}c zUpzrN*NeTDjk*UhuNKiZswyk+98p$;MeIo5a#D-hjYLS3W?(5!%79L62(4se z!v~fOUj#CBX(BJtc#w%9p%tmol*JTqz9jV#Z!n675ZL|YX#>eR!$4vcP8di~r}!s6U6L~t$V@?1ixQjLvVmAoX7re8ZBUG5 zpcSIz>AnrrS)5J#VnYH$hpg`<6+T4o_7wv*Pm;F}HzY$rbm{VS0T&%lG10B1B z99>E_aRL2Mb|TAO*+42x-=h08QswF045YBe{9RloefnRXelATY_VyLr*<~h6S%P&h zl$k&pTcPwu3(MWx8S*Kp%QMiU{ONy|eV^{nuyYUni}9`fIyyZkmrXzpS>S0Z0@Irj zed|0>7Bh1SF&5g>v}SL<&P5O3AI%Quloa^O8$)tPBv!Ub_?&`6n`tVu=b1&`1|n>B zG|JH}QPjNtFF zYoa|8#Fsugsw+(^C`Dt?NN&QKP_RX6!zB0?qNSMTvd$DyF565IWm!*dA?xw5YCsg* ztO`<0#mFFrMFC;Af0ei+@u6Uhzt)@{W46d2ILTuqvIHOC`52HU;5>Nq7l>h5Uk)mJ@T68qC*VQ)W!r zmqTQWU4No5B%OF*&5Q+VE)o+rNitIs1SudeeFT)XCGO`p>2x-EY(fy`v3x;d{fMYR zEGyRs8x}m+JLM=hduIy@-F#`*gq|8OqZ7kuOiu;^B1c1=ni>@m7eWa`QOL|PXDTj+ zXg~R>OdmEBN+l~1x#r=QMQF(~UEUm;Mj^8L5wl_tvw}#mCmhcJi=tSYKBAa3fz^&) zHa4-ABZ$SrsAi&pR|bi<0ngZ&5psp$efRQxS!PsewbA}udcUiwMWuGX3-Qc&86M@e zB7yXXCsLer!!ykcgoROwoJ7B2r7UrucXykujtQ-E^~iNaE3(vmi{h7y8Kcw_t2BF9 zr@v9nOj&j9QM|mPNisE6g6>si=ZMitquJ+mbi2VE^Y5j@_>WzRsSGe9?4B1>$HD9w zbdfh%P;MG9!S3$dG{h$_87j+2W3u@~2t`lJTUnu)IF=PvwWuchmG5K}MiOCS#+`dK zYekVm;wlE@D!fO2+qh0-MIPo zht!8$-C}l1a8etykqw3)c_`+D%F&E-@t6}ri(FyyqIF|NXi&5tBp0WumH1Y8%s0=w z+#A(jpplDsJ~pNzuFOj!9998D;@LQS3O#pG^*rYzrpe22Nvkh=eaEN}ghTlx14-`k zfdJd&vX-=r*}zWbh9dqm#Ric8b}ZvdExBX|Irdm!9A6+YB$yp)H2N92!zXQri}CUm z^Utyw1;vMw@TnWrK-l!TVI!%_8^t$gutZc4wRp}rXOYoN^3lL2;+dhPWihL-UKquL z4tWm^`P|e8-4!A+;Z+AkRSuC~Z=DeD zCqyfhbdC=j+7Kt_(>d0G#pj0FLU{<$1*OQWOJbzTQPUV|Me%Th$kLh@rrBD>#7G{& z^l5$U#1v#Fw4*d8pX^Onh?uPwl$9_qbg7Y812!L6%fJgsGDR*zY+cPGsc4`&CttH) ze5c&xWt(Y1*>bSSlxa<75_3@JZ$9jS=tIZYCMRtmontg#mO0YqOsac8`FOCkPFX^1 zp~XfqYcgm`gxsi9{X1p_knbw3&eB={y*M--z-l)crljkN0@?f(gq~0m(SlkI3DTtH z=z|I(G<`WiT1rFi%F6*%M@Um5g_j@W6!B_pQ3+{5^*%_S?Gc^RwDb+ijqaeP^$U~e zo(S;O^#~eSqZf-Q*`Iw&FZ@pd~5n$iO4lHz%8bfsbg3b8RA6J`EHbrXQS@pL@ix7txW}D4p2~b#yvJEIc6@ zj~PA0JCm_=`5&-P z%v8sQ$~Bk$ENYvrd>QCScJyAU5n zOkV7ouIp49JVAkP0U2__D+LP9svuf23523zKb}dWG7%~eiW=T-Uw}9wfw2(zlfdhz zMG{m0W_!D`MT6!1o@5!hnK`jMITe;fxTI8DPm5WIGExb%0>Es^W(V4%t+Fk_IqX8s zW_y-hNSvYj%;svxW+S<5VlfF_){f5&gPy((kl(J5L1vOdwMP!ng?T{J z4IV>ZC*st*9yV#;kMKHF|g47^FGfbOC zn(!^P^e2l#c$z4Ga;^pRnHBBPSg1|^vaXo@%%7BPPQ2U5m^Obx=-%)YqQzurw$4S{ zuzq+kn;sGKuDBBH;IF`vm+3Qi+06A<{pug5VEJczm--5chfw%JNwWhksJhU^$cF|3 zlmHELSGq*XS|nXm>B?+TN)XB)xiyR&W>{MQ5sQg%@fuq0t@Du!AFsO^t_2agWq%$Z zT}70MUVb<<#^>|#sYDoHV|yhUfVjvV8B&@mOgFTlG$M_djK=%yR``6u`W2}+Q|E|Y z%vp#98eIL|6;<$2o=M91f2og+I%HPLFj#{ zurWk)`o3V(_l%loAS+2N&3eUJ2QQM1w3#yy82;Vw*gBlx+8Fphhi;*H$7{ z^&v)Y4`u}YX+iSR$2tN5Y@pI3k}M~a$y}1F-VeY8RXJ`!MG(1fw$Wi#JV3UD!4nq2_rS$}nA$u`( zJ=Ytp!P-5Z(MP!G9u|f+QKMggFj%h6UHEun3Wg$MrSSNknz$pLzw4tS5~=dt18OstFksO z8F4`?=$5QXJhPMZxxKABBX=^S;#FkMGZ7lCLD=9kBs zh#vJRau_QrB)sFaN;jG{GE)rrqhwu`OR zK5wvH-Gq`_PIbrJcm=a@zFvQpalThPKq5Oov1kKAw04v{BOZjd(Gvy5Bh7_Ti0Qa= z0hHu0b^UqV%syWsg0T6Cz=?em+(>4l)#mQW=F&S4V3w ztyz!Jx}|o}G5A{yw_DH$Ql5V7r!ueKEE-6a84zYspnftb)G>~}l8@aH*g~0>)tsn0 zim9rZnDIT?7YP}fr#Urp7UcPu5}9G1%$%E^cU~&pD2$VC=FC_$lWAFVdQ_*FNzR>r@rK>a5g#533TpDN_yYy<-^_pXc3F8Zih-KE z4jBK4>og1fR{Ad%O3DOvme0g&;l_CAj zyq|fyIhwxP*xH6KZ?ZNT@z8ThQ-O?s(T+B3?^r4+jn{2UgJ~9+ZpBO_^J@=g(lz_> z6-d5^z3>P_<-c|;hG+gs6{u~gL0yqH!=EPc09{Jn##a5-$@O&$raIj2#+56*4gTd* zryh>$0OlawF}&T=?Z<~Gt2Fn?_0_+J>Ik>H8RJGo({A6&MxXw@c{6T@>2!_0CU<*q z1-3ge4U)G6+l6S;99W>n>$EU?2a)_Sw=WV2MRc;o3ko8uE0oNrjA>9z)L(!;au8;! zciL39dwE+yEOSt1lz9d!EEI%xU?Pn^cK{C;SgB`bCMoPKlepTbq}+TnNb~W{P=0Q{ z8Kn978f$B+sTpPJf2yTPF3vP?Q(g*Z|Yn&!rJ957Je;+id9R8;9K88=eXl0|(=MnAAmsq=izxptr;O6plxp$w2Mz4H% zzWMYc`D&yN@P8Qi*NZDrCq^2Oq{rDdhId0L;5n+JH7k6k-xze(EK(T<|)>&oU7 z7diJGUNhI}tXwm|xo)1bvSehUsBGl$Vr`}N=_s4MvD{fQz@}-Po?)GqIi>5Y&+g#vump-_Vtd^&X1Rl?>BVP zDwI7wCi&-)eT%hj%`s*0VCQh>*gk{%6=|a;*_t)YQFQTzgJ0B~#bp<^URk%Qc)yYM zO9z!ForCIME{<j%kLa-(^hG=!6i=3UanoK zm5#jh{u5CheVvJs7ggH#8#8+D-0>$AjUQanr?c2O$hHTRq7|2x_bnZ7OO88W=+)Yw z;?paNt}HEgZ0c7UPgZH#@KSBOt)$r=x39WgD=%7GoV;LK(V*g0YX+<wox8uBmuUF!HCg5zqIe>Ek z=K;iVKArU6)%d*-a1mfF;9|f!fH*D%Tn4xT&{1LE)apKg)*ALCCo@c$XW zvw-IS&jbDs@FL*P0CBtocp2~)z^j1Q0Ivhy1iTG+7q9~$j=uqV03QMV2{0c2!k&SiiK{x8y1;d(CrKOX1#hI>;#iIIg@B6y7c=fsoG$}h4!8o)2~dx#6x`MLe=Xp8z-y`6py0n-4}0W$#Nn8|1Q zm;OB*_*sA>0mlHGfJ(q@fH>yhJP%LLx zs}1;zaD6f062LkGUZlGe|E~aa0Xs;7p^}s{QnHsp9B7F zz{z~R1a7B-{~G_l1$+niAK&`{=b!lcXFmVJ=U>ISkK~B9jq84Z-56JjbAP}9{x2v8 z;(AZOAi&;?AI9f>ao(S=1?_=+eGtwg`Fa%2qXA<8V*%p;2Lr@`p^J71Ur)q&65vq4 zWX4Uw*#Vden8CQiaGnX61vnCLG~gJ3Jjx8$<+!fo|5Z580n7zd1C9sG2P^= z7C}*8r{sTo2d)5XVh8-wfCYxRr5q2kjgG zZwK57*aWy6a4+BifH)q+`A>jH0gnTo0EpvBoSysRla^5=QjXv0^VZW+kAcp=l1~b1OCRizw`M6oO=Kt0zLwK4EO}_ zDd01}zX9U-lFvJF?ge}e_!jUTzu8hbjCN~>;tp{{D3n6#v_3LLB@p*_m=T>JD->Hc_q#rd~M`&F5}L_ISyE5 zpt-0ha+T2V4OVM<>o#@%1%)zK+k=Wdo`Nxy?>hteA zlkcAQ{L71Ot$8D||DH>~xq9~b{UW80wv0Ue@e|(|yx#xTt%rPi)QZ3V>#`p!J}=rk zHv8VU;~Soz{n3vLAFUfXamljIr!M?v&eA^KBkqf>?f%P62dA3D=z-+{|?@5*=H-?EPw6&RrhFjEqLv&#Fv}q?7Z|lZ|}tK z?%QYnX(!%%`a{2cc<$fg>u#KKjdsh2C%ohE9{bA9K}WCCCLHk2dv)iWH=}CJ9tX6Y z-}HiQ|G&gm_Wfb?mUH?Y|9smd?fX?vyGJ}6nRWHo$t%yJpIU`y&gX^adXGq-;O)#m6gx_VezH`TgP1xI{5R~|N89sor?!OchD=>ee`<& zTSuR?tfK18FGj9iwZ%5ucmD^`b6?I%97bo-_Qo*#eM%+>RzZU6Ay_wU?m>t$c>+57b&qYnBoF{QX-sIB~(0e`w@ z!=5$Y?_68|*nlf1?fux|J}15R&tcUikR)BiVj^%q@xZw=HQ_R>pjHKjZH&h*~+?SdVh zo#Vdyq4KuZ-ub+r`$^|dgIh;`9^L-^MdNqA{lQiHj9=5yarM)mJ^kULpFgee}?dapAj9|5v}}AJ5)$)MMBE_~56(?&K5S z{`agu#z(JRKceigXxrvbzi6#Icx&v2{g*wzX7`VVwCt?;&)qNowY~H7i96=~y6UNO zF8=73C9l0Scl-afu1-FA=XJLpca!sw&ujj$ssDpB8WL|l@QVMlH@9|#7R7e|{<6cb z``5}32ff!lz(AxU*CMUt}!q%@Yifs!Y@^b04J-UWWKWWezcb)O~w~{A( zQ(aROSv{hC`qP(=JF+=2=%3*}OGhr*^yBv<_r34MP2Io#JYdIRtG<4C)W9cBI`+>u z4c~n9s3$Ia>+(k*ymtFF2Ys>b4}CxSZSc-*ThB=xaOv_Pqjq!cTUNf~@1Je{`G*U- z+An$YzQ)mi-2U{x|NLIb`*V+Z^y$5Sm^t>dzrFhHkn?(zyDfP7g0CA}mX2-vVf&O@ zdOx4FeRS3E|GnG&m!{RjM=kvL)ob4``(pIRM}6_=ALbu>)q$IipYrm>{a&bhbkWl* zp4#ifF(tmeCmi}z!+E#uY^{6$%RB#e!@$wSZ~S!Fmq(rcpZzXcH}?AxkKVFo)kE){ z6moq!b=;f%w$EJf@y;`^Ie0(s*6n|{ZJzmH?`5%#_l|gcTED9=Au|G$nO=j`>}>#V)@-fORYJ%^tC zAKBeC_0^W&CG8*ZUB$sYZ}vO(OZ{X0>o#3E=i+$l=_l9roZ95Wl$##9Y4+UnT|RQh zFFoBo;LCQ+o?Sa<%k!hUOntc36P8(1mq_n+?Dl$_{ZhL<&nMox$5LKn+4Mtm<{tm9 zPr~CNWe>J(u7v9KydV6JNMMcv(YqO`lyXc!{j~+j`yZ`wyJ9`X! zKl$BjcePwLF7czU9(bg~r11w|omqTkRr!j0xAb$JTsZ%W{fQmzVehvZmX&ht7x$aT zJ`3J*aBTn63uYfLh}^boYIKTg_P2K}3C&&g-5qhs4~%?0H~Gh@omO=GdQQXDx7N77 z^A|T|otrxE-qq3fJoK~49y~0~*|@>hQSY8Sxc5q{)}tp(f3YC@(!CL7bEk|SegCLB zZFbDfeyZht#h={%ubb;eOlh3|+6QxU#+-g;VaunA58txk);4pul%IK{`>DkXI~(TR zzht>3>AP7c*3D{A=Y?HEF2DI*@yVf)&#Wr{r2KooJC_W9uFJ9)qT9ZCTZ7jPZD%by zTl8`1H|D(Z$d}vo+Pole?}L4g-v2MpXWxBwaN0|u_0QcCDYZX+O9#J0$&>39{N!$Z z=bjIBD zCcaR1=gg8HKJ~m;Tz>VbuS+Usow(|n{`m3%Kex9@Sa)h@O2R|Nk1n>E z*7Wep?$f;|l)rE;sPW8_giFB<&l`qscw+y>VQ>45dh4mfKZic~;1^-<=5OCRC%RVW zpHDBj+0uUh?h`dS-9BUEfEthAIXugG%hsmL6N@H1^K*LCz26>kHm^1QsqnWWPfnZJ zWLr^q?QUJ#-+%eOMwOncj~;pK+OZLB?X6ADrQbR~oSB+%b`a$D5 zm)+SQYGduC-<<6q5ghsR4{yy{bNimcyoATUF1&DQly*G>nwt_{%0BT!`GmYRQ))h$@WGQ`>^MAXV*hP7)%mXH=?y_r%Qxd{-dEVJ z=egRijW}?h@#r0E0?stpdZu>ZEq9;T-78voFJ;8t_a6U#;nRD(To-U`aF5(s<#8) zZk2obTK(|)rnT1&ejcP zZStvS;Q{-y=O*qtQqpTyzi%dW>p3-LT*90QWjFn@yI;V#jz`u%WcNFhQ2zW|*P5h< zJoe_pOQY(@i7y&dFHhd6PndnIK0ia@PiMB zzqG6Wl?_WeO@2N0!NhV$dUm^x4W=zBXlC)RX-W@ikeyiL;^Uh;_vy6n{l~1B zXVt&O+33*~BR^?=@JikEucfZ*-=A{pgc;{Y?->@J6Zqbh_vfzZkvGJ%|INNve{?xN z?_`g9V1X(8F#WK{KfiR&VAv3 zZDOzWqtATw$^0REoV%mu-t^6ugaadbp6szDCHeG-70!tnVed_U()N|R*1!(8jUN~_ zI=6nk(%07YaV4}_Id=K0$-xh-UNGhPK1U8YP_8I=|-#-;|wb?W7sXwR4lcdj=orYkv7s z->g>C*4I3P177o6zgg?q;v>J#39I|_roHQj?7Qv!eW!0be)5ZEYZkrn!I$md8a{K^ z?t$UCN6KEEc`>N968q$wzN3y$japec>EY+ztT}MiM@1WFWY^qw?_GDbfAzTmJ03Uf z+_&@A^UE5=w|X{v>eUYGzkfP&&D0;4yu9eniiwr)jvg^#)XDafa*p)qx!3;Cm(Izq z8n+r&_*ZTkHs#sF?Q-wlF>%qr*IV6k`cSX5B_BP0>dH&CH+5O|(=cn>*lve@7@W4C zc>KeUUisH&`HeqIAGhd>+qb=Q+ZB#kam?#(lGA1rA?$U$-xRZeOut{%g6fMAiSqogA`ix3oH=ZO35~K0kZM z{@GVUYPNbjtfqhd#kbejP5B{STD3HEYl_`EWAk@X58Io=I_>P+AV0p&mg!j+N6v^? z&~wVSd)|v&GI_Ff%k0xPO+GSs3W&RSfnX0N-_YSsRFbZ7bPC09Or}@yJ@%5H&^Z|=)O0n z{IhxQFD;LnwCAbsx=4lrtxvsZE8h0fcb%Pe4onaJ*Uh!%O|`#oIl9rEMFs7YD=Ruq zZJ6-dsnPB4N`8A_y_n#i7XI{L=)>mx&K*m(e`L$p@W@AFMtrtpsORCxtYph}yL|W4 z_cVU8ZvNna4{G#z?3Il&(7$xxh~;m24y_xJ zaAjPiqo`zYvyp@9E*%(rPt0~}#DbZ%8wMl~4$5>tHQ73)&VrkwT5Y$_v@M7aKG-KM z{&MFQ8<)2(S=!*M!H@d4y=DK<`}RyMURcp@NU_bgzhjk?!Ti}{OH-;w=RC}>x6Fa|MXtF`=0Lh zVSqiY*QDifv5QBXnA)RpbAQ|-%70a#k+fdHl@AQ{pya8YpZ|R+oR39 zr7JA8FKvyE|17Eb$D7mVKRGseX#05$B0k!CU;CD`9CxIbjxN1>+2`vI6+JQPlL5`b z9_n=VmcpNhrRT)W$$9FL)Ma%pXMH;T_Di?Cd@5#S#;hZYKKjwIKIP`&i~0>}vUbR- z*WXz;;qAlz^Ln@bDP@3p+PV*(F0P+C^3_4Mg8Pw|nb^{Qa-;RLCp_En1Ao6Q@5@gG z#6C2pQEu9UU;dchEdIim+XtQOlz(bpOrLc_hNee)YX5Sq-Nx6m4dGL)1M)9tHQq6~ zEA9^mKWU8WRPodoX>*#teJD@)%Cn~H-iIDF#2;L|aQdSMhvzM9Sbwfzcl(ex?!I>M zkn!}|u2arz&z-TcMZL=G_)fPc%{rPjY~JRg0nuL_KbE_2;MMe@rI*c19+-1}^URlf z=cEUX{K0c#n<@F3^5KcgZ)@4LOY)n`uXcWUWZUAxL(e?_P_1nz%w1<^J#TyD`F;&8 zk3RNU@7kj-y|r}wL)V%`pWpIG2S3{*b)F2o_M>Cmq^X-$E`D|Tru*B?95*=H6fyQ< ztwB4B{F|(u6&d}>&bj$6rSa=G-~Xj+nE&jT;}X|?Gx8;Sbe*E7CtVpZBxmuI_!iLK=Jb=c8;z_8wd36g8{S z?sa2l?7U-TN8Lp5Hv}S73;(r$K{zg?(sjUBgX1*}0p2ii1{iR(hx66K6WY`CjQU&+ z+%@$2yBbHo)pHs7^EIoa?PvJs-#;brPYL`}0{@i2KPB)_3H(z6|9>rkN$Gdu0JD18 z`Q6W8G;~7PjQ>gapLX+zS~Qkx$FbC=MAtBM!0?WE8jl!(BmDseI!{FO^f%wBiPK+( zbKoMn5sR;B_yS?N{2=Qq+$k;`|JC?F7EhR(UJy$UB;%*Er5e8)UrW%ZHq%Gt_hV-a ziJvZ&zm8WPVY#%Ci7SZx;+#&tO6Li989y*sAmDC}y9K<2;~ND${4@1{T#10^alA~x zwarsSz;n3#3IW&jxgg-0K8DY$=GVZ}4HEDqj)w`jnGSQ{imC=r67VZ@-~(4mHMm2- z>+r)NGXy-Cf7VwWbZV~WLIUXk98h@04&*S_y0bjuJ1Ofk&c(E&l2!W9G@ZJTR5I0;M+K!C*a#TULfGRIKEN9zvp;~fbZdW znSlSuaYexQar}aSdpK@bShal}?+23vyffz?F5vfbe1d@6IPMVecrHI%z`Jw)83Nvujt9{}GF+PfAIWjEfRE<5MZi-y z9wy*pIUXh8<2Y^;@E16qAm9@@o+#j&{~0deuWkS!Jc#2H1iT@~ z9RjZLXA5{R=bs_qAso*Y@KBED33zLcy9K-h$2SVN#$PJncXR$S0gvQ(g@AYF_yqyK zpW_xfIE+hc?>#skUJY&&@JBd*f`Iqqc#?oW&hZohAHs2mfDh+*b~Sj8fIr9i^Qyt! z0zQ%RmsEq73HW5rUr`NiSX#9_r*eLCHMm8<-{bt@)!;S(pUwFb1bjZn9Rj|Lct}9^*MidHMmW{wQ+btHF%PMx8U+qs=*xs z-j4HUSA*vWcqHe~s|I%qxQ+9dRD+iZcn{8BQ4Mb3>x-Iyi|732YH*8y_v8HG)!;S( ze}eNTRD&l8cmiKn94_G598VGOoR8J@%n1T+`$WYZ0zQGi43Z__Ib6?d0neqEMsUp# z@I20+E8tps1p*$%<+ugBjLX?5;F=yK0!VDYC9?ua7_To7`2RR1%GMTA@G~Ws`_UMc=&57o?Q(-L%?mRDu0fEJ9xUe0$#z> z%@go&ju!}c8JFW0aIM@n3b=!pTZw?@(F<0%$^=}Ks|dJ-*KdV@+cP2fuX5OAOT$39j;0F3o0WOPxTR0vj;1&D>2jK#)=@}*9ZhFZXmrcMm zJtYCx(hk!dco-N>--ZKQ;&G~Z#+9 zZmxi5bABx!T77Blt3con=jGrQaIM@n3b>8)mk79)Ua5d<>6Qt25|^V0xW->0;114j zSXp&^?$uksZQMVb1>C{$5CKo&c$k1|`LPMOHXcY2a4kQ>1zh7dxU1^#r3L&DeNX^b zh=6Cm$lI5I=bTo@nKl7;a6C!G=|eWS90H!i`Evx^#`)bM&iTs(+`{?Ui_J(!vqSa) z9ncnf;aP{5(Z_Xg*#z9p@gxDa@N^vlp2PWb1l+-Kw}2;cyiC9iT)ts#RXuH--y+}^ zj@ty>!0{vjFVo6j#80Z_kR#x3j=KdshvQ`e?%=rgQn^G&lb-av;Z=20J9Kz+9iFAbZ`0w~I^3eeXXx-2Iy^^* z->$=Rb$F-_&(q-{I=n!Kx6hLffUZ%s_>u^PfchKP# zIy_v5U(n$ZI-GqB12oy;-8wvoK9Yfp@OxND{W0tC`*e7S4#&$E-piuH@Asmxe;wXK zhllI%UOGHVhtoMg?Xv0cL=D7U(&24%c!CbULx(5o@Ms;Lq{F-F@ZmaqfDTX5;Z_|! zL5FwK;SL?%U597saGMU#*5P&?K0}B1(cw8dJW7Y>>hLZ)JWq$;tHTR)c%%+@>+ohe ze4`Ghvz*#hqQmdfK-^1pct;&xro$i9;ffBA)8Q35JXVKa(BTj0aKjp(o%ht?K{`A} zhnsb{q{BmWcwZfE(c%4dc$g0Fr^CZ__@g>JN{9E>;Wizfpu;5{{;&>D(BY5h@I)Q{ zkPc7M;qf|rxDLnRTkn;k!|QudeEUm>)0QvV(z=GBveh#DX5*@CMf7lb!c$g>tJUX3 z(~!th!sri)rlF9>&FJ@trXup>F?t%&)Mz|8jDDSH8VY%`8T~5JG^Fu382uvARFs|+ zMvozys?L+d=#fNIm3a~v{S?t;3LYDyA0wJf#uLuyM~Nnr@K_lA0MTTE9y6o65lyD& zF);c*qG>4QskjE9)!jtX5Xw`==(a@D5Xn=*=-Y{=p^?YU=$nbAp_M0((G7{FD)rUnQElR*!?xFB09F=oCheAv%oc zBu0-Ux((3@jDCt}8ZvoojDC!0>he9|jDD188ajC_jDCP<8X9@bjP6D>buAtPqwgb{ zhDe@@E3E#BrlFCijL~h0rXi81gweMXO+z7%o6$EDO+z409-|u)O4?9nub)Ka7OPS zn!0R{h0&XdrlE$%%;>d5(@@J}VDw6&Y3SmqxXkLGXdBUGjQ*Tx8cKLd82us9G(_>Z z8T}s7-HFa)^faQWOZ4P0`gNjd28QqO&>cTt*M&Cy?b%CCWORWBh zrmoUc#^|<0Q&;FIVf5`pQ`hTpGx}ztsmt}`F}fkqeTdFsbZw%k%lBk6+Mj6Z(mf7F zUkU|HUB4%V(Wi-~p{gedw6b>(KD5og~d;jVgQ-5ILXFYz;&@_Q6y z)sWI(&NZaglAJH+Dl<^8aqUkWv+nj}|Ds6~n{PBkj+ zk%T0lR5n!4Qk%1USUt;;i>j1K)w8VUEQ6|Nnaf$Ct7mzIv)oiY%RtU@W^1*Xj^ZpO z)w2Y1mW9=`oW7uD`t|BrwsDrHt7lojSz@YZkvU6?>RFOG%QbwayPBHr&ROj~P*Yn=JZNfkQ{SkWzFyr<|3*TMxRm)^$_<=H#aT5+S=F7s#L9=`FZL;r%Lh$WezFx9)8Hq$KcNhD8~^`T~XT4 zh%r<2S8y__>?cV!B;vtw`|*q`xC6UIZVW~OkdTCjRn~h|HBO!gToKoMQ)S+T_-b3k`)!-VM zTqrF;C|e;xsRfNG4}NEW+=^6`UIZ_}Px%27k;~FKsW|Wl6r|#qP%1gpM?fnInCkqG zq2d4e$}{yYc}>R$AS-{nCs|;~#-OTzo zN%tE13o_u$y@v}dHhxJ#pr$s)aR}lz2{)xSDIgo)LRiXlr{01v;ZiYUmWpkvc~cUQ zmx3skPO-5s50)R~=}Jyxy(X#@|4KAUJdtd=EraoEKWK?fZw8Oc*pP0~7gB@q*w56i zk$A1jGSh7i;$6q@;P>Dg{Vk}IUb6J4dk3pFIaB5C`z)W#6-2^SDlS%xGr z8_}M3SVuI6#%Ue;O6k_uz=cvglv-DETBkvta`gnNcQ_2P$Y=+j-(c-I?e3X~$mHa^ zw0ua;{vndHmqn`4%Y1IFA1pI92o6Vb_Q7hyf#ht-8CYr2@fXm_!n%L|`E$L3{7^W} z%*d=9m=>hz!Pas1VVbHIt5+Y+f82B<{>9buuj2gX)nU-j)9+h+xu2CA(g~5|mqLS- zG4wEI5kZUv$%c435F5M1^hZ~fSMvt)ZogjMzAe|uv-h|6v-h?4>DwEcgvB}6SLs zC^ovnlnFP64(?i|8QrHcN5S**2?S%&E%+THs#v-B4esgl-Qe_#i#~43pirzc-V!gL zh;;^pO3{U;%moaiB!c%~S;D@`3;+qmID5f3Z5U^km@=M&?ZwD3p#ha8$U%$~A-lZ6 zlt~L^ynVzDxym?{?#C)CofEsn$eviaIL;Xt8YIbU*(0&ej-i#oy>G3dX0AIP_B3Y_ z&`waVVFL%-2iXVPAGbfTF58rk;y?ytopnR)l_j2F#%QBQQ+E<`oLuSS_xw~&twU$; zwk&HbTG=|mOprPC;`J+kXYYfm>npjQ3+jz(B=bhIGQ$a&GM0nY*)JegK3QHDnPJ5N z=TJX7!SeD+P$A$-I~^B&H8sfIxxXdO6n8aFo*3RE;#~Qic2-Eyy)0>WKvU)FVXBkv z;jzw1VW#~4K_y4i5BSAI9Zl^(r4)TNZJXrmZjIA)fHQGC#@UgCg&TLr|cHoJCW6l2S@*5j{^7r#w@3-;`34bKNY9@8L z{*C;i>8O6*l;1Nb*UywO6?JIun_hM;R=#TQ6X$w5sIMfSrA8f+)l;kL)Pc^&>!Pg7 zgT2pFS5M~8>N`{^m=l~fleEEQV*kM zLe!dp5B2s3vC%Jwr?!og4=azd##^jBun~`@j}G(GJ)rZ~%opbWLX8u?FjnpnUS6oP zck)%tkmc{V9fzFjm4E_BM**yX%QGnRzNd%ZB1a~)8q$+ zv360c3@)O|UgSn1eoSs$^ug3E@Bx-sd7l)$AUGZ{}N<42bz`h8<73Xy=ezkW%TxE$~W%E=r+ThS`Xd) z2?P;Ke%NK*#74c?t_i}t2_CL77`|l~22x&ocNq(T6&d~6SjLq-16je4?Q3Fm)f;`u zpQwCBMZU|ZdL@jqj-ZjYIHD7sh!>zTjvcEH=L31G9X= zOkeO-FY8nvc!PK(Kx2Hmida(q#l}~BPfYX$wRDP%^8otc!4r+WN|y7W@(#_&T)}H- z6yl1U0T|`EAJac2?hqSZXA(pMQW{yvmz6Z?)?LHlh*V!Q9939uB0sP5Kb;dTulV|# z9udDt=yC#?3wpWbz!V|Q7mT<2F1up7S(NpNzUGI5j)b%<=tq!>GKyc@E1Lz z*S~aQe}NdA>HT8mJ}8R6ITGea&T*Y2tL;~5Sq1jCZ+pKuS7a0XWiESPxRRa}Kr3x! zZB)ZR1&J0Ajzxtckg)7P++f)cVnF^Yg5<g^kLi#*0|?dy?DxqVOd z)(baq79g(J_!@rcf(fPjg4sM+WTa%kHs2Tbu`ftz@`q>hAm&y+@CNZnXBKBDGQNZm zZ2Udn6Yu(hS~^9>4FJi;Yx^Ptc~Dt`5`c~GtxLuoxvT-*tXG3zdczN3M3)7NMtSu` zH8?0vzKXJr1?TMDmftl^rfAT`#t!&pCF81BaTd;}*x1U8aOYI-VIWXeVUwERd5o%eiMZw9kBE}%^S%o#>gZY2kWGpwG7z`0bK<$G(Z(w2CH!q$EQ27y4D71?b)WI**4gT=;)KJW(d5b!K#C^9~b5bA^Glyo6(q%WwYQ)HY0km}>7`d+>tw4iKU zhoJHbN(c4vSuLpVigW-%VQw;_K05A0eY|^v`k=WdyfN$CqfjCD;Gql*R?sJ7NMpPe ziNdd$GL{jYZmmI@nKC{F<***61B%`J)(EG(OF2Db|ZNO{BIk=dC^(L^Jv8j5ynQq0# zCb+3;MNz2OcsB` z6W*0B1plQ4W#f1RmAg?wu%&^4l;6mXfROE%!LBGj!j{U=d&B=Kx10jen+%JM>m1fo zBquG9mcr2AlYcHF_rUyf1Bf`vs1t)dg{LuJ1W*V-3E0HiHTq5CcF@JfV*E1AT=lXl zSY530B3fM>0RjOIy2-(IreeKm#4QJRCQ7b%jKvvU>g`{2D>gcDqpQfcfI`K_PdR|N zIlkcgzTiwAEH-}R18)!y0q<~zBI9_3P#16eo|xtfYUvai-2kaBjJ`;JEhrnyYol~N z-N@=S{ z%Sc7Wbka!5Jf0egxg?t#InzRDH|H%jKf`c-v{~uTb$@vUM*~e3xNf3aEo#Kz?iE$JYaFZs0jiG6NNYiJf>n${wRwbma1|%tW0xDnX zTTc)d#@M34Gv0=CJ#+&^&hkI@FCj?M9d39*YHYc^_?4nF)npbDRsygDYtvK`Jgp zxxnM&gvjcE0N7YeXx(tm7oyx>ik0#78o&+G|XD&~0(2WD!f7;^Azyp zY)Hm)6=RpPbXWp|=-0nB{nlt5E4w{I!C63#Mpd8D+WNzPzP@P@MYYOmY<9H)_Q)(& ztL!+J@nbfDi3tr+UjL5OIqC1SS+BiHlaG}m<}Yq*tU;|HGh=1#Rh&1$)fA&C%Uhta z7aav)C$n7EMbwd@WriqQcF|tce9)vlsmdChSO335`G-I&vHYiD_g~FHZOTFSCc47c z?+^SP^sjpZ{ddz|KC6KTc9Q;es?pz$Eeq>HFr(vvEtxF_!%zuZ|6wFk&IkcI7Js2SWV`6IMUT@L&) zDbqb5)y!aEG@JhgFuK^TOzo3BB91E`WPF3Tf(miSx(z?LvC{r=A!ghtU^{t3=D{Dg_$(jiPnQm-Ey|OCe9QM0Z zIxVmK3uQK<*aX*WuTEn>EW-Hqft|)~RcH^!Yx@0Fk)~pnubT2Nt;FKBp|^B?A7*Aa zwvxJlyf8w zkba>u*3`q}O+O>l0;{7-a^X-L!b(R-h;uDq5oE=&vWJ>{LuSp)j>&dC8~hBb$ynns zFoO7S7;;-m+hvszh4ZR(l;+OR8@2%wA8w)Yfutvh{p7aFN^ayhdvub~%=S&0%g~Vm zayCKN7T|i7dg}8#T9l~$vC-MA_~;KfYF6Bf(93^cbLZCw>Uld02^8;^7n-&%;(qA z$ifu!z2th{U)~8lw;FcxVTJ3F%FG?FbdrYbko>T`gEtD$NBC_{_xMX~He)hLBMK}! zM0H3t)A=WC7^j^^7WY>A8?ebiEt`zb+pdV=PbG(}%XIRy}0zf?#_X?uSM^q{=v9;vhL9`&= z6a%SFY-L<>cD9-F&nl~N;v3t)SmuYHW3n_q7fXH)2p_=XoOO*@`Ii+PNU9s9J^h+L zIXh&pHnOJ#;{;H|4w6Y8@M$J?ElDnaLz(xjPJe1EXyrKn>5ptVtj?>{+KY|D>5dD* zzC4(E4^E-{dIc8A_;)}IV)p@Hj83DOmU2)TScfyRARWPRTK9V&nWH|kg+EgM4ebXQ z{lE*it=fm~JjLW=Qj}1fQ2-*`I2?h7D>>f}B^nlUE9=fonW0!ILpjl7ssPMd>H>)KU>B*Th^FLQ|4l1tf*V1K`Fu` z=@*)hZ^FA3YV6&BVFfC449pD0{s}}Qg}IbM@nqaG8%_=3Y}~ zIqbB&1?;^LO>3l#grc;Z%#k3Fa4<9ajEIJMAsub9hdGNXNz+^Q6qNp zWdo|!H02HOgKNLZUK9}3%6&8-{eTI_aT+&7M-=5pq1qJkkCXS6w^hqA*7VW#u`7M+ zE9mh3q{){^-uUkJth!N>yqkPzT=WG~=6E#USicK)RGepkTHYv<$*cSfniVSVqqaOX zeEpX+i;RDCd7y@&xT8+!IecPv3Ttk{Yh z>l&QRYSX%3y7htcFjsUmLulgU?2JlsqfXi*d55(hi<8?g$34{w=5Nn|8U01NwGl<7 zw!ssU>|v>}?kWvZlnQ7;`$n+4frBesdGHYY7oAV?jlUh%izJJd%}g1+VB~O6!+|hS z$}99nNV@fLVlidhM$yjp1CC=n9gYHEJm8m@%JCSl7J=Pq4F@|L*_!Ye3N(q7Ik{D49d<~WlRFXC`sde$7OfG%2B-dqwS4 zU?UHXaKBnz&XTT0~X`6V?fXXI&Ga4t3;Ji}9RS_5cBrP#PhLyC?0Jct2&#=R(XXK(?Y%G_bf zyg-GFbGr!`CtAM&SZw?NzjQex$0BK*`Dx;1_?$7Y!lCCdp40S%+2Mt}z{>0bif~$! zz=`-YE(^h@v>;f=DrM`a>_?DZi0kQ7^&TEfZ2>7eXfKV85c9wf##I35D!+}+M>wt9 zy$o-t3}15wj0L}~0Tt9CfTAsJ?zM_Dg%l8T<$yioTTgB%0Gm1mm%Vf&KdaTOtxKU3MD=!%Ut#DcXlExEI`F2ZS#qX=6gKt%|LW<5jOd$g=DjTIMkqaxe| z6lz^li*Sv`L+1>2#kg3-Q){z4nMoh{WDFLa&f&Cz(>ei)Vs3CJl?j~{V*4CUW9$+1 z2SXr9>kq=$Qx_KG$#aw6V7#97d)54*9#{p)M^Tgo`F?mc$5(cpkUq|IvtmO8)kdhM zYA2wcJMqKEOHONqAFGe)5aJ9rfN(~hqd9Ta`q+7rWZp(kH^dJ_UL+=0y;%0#Mi9=( z)xPIs<2*dUjG5<8W-7Q-?~Fihjs;bgIIYhDLP=og&5q*5sLjW-?+5Sxpws$*%2QKa zpZBa|anQ6M(t?-Cz{|X-U!0xt!-TSL?})wSVX(6z;|`1m{3@jM_9YZBWxfYKY!ZJB zD#LBcaDpr{E(E2{+AAbeq486Yu;;Wwr?Dl8cTei-Y=0ax|A=$w9cZU82r#l=iJ|BV z7<6Kx?xbSbN45GPvI}?OQd&U6^4GNeO{eksL;8A!CugEivC8h%D9r(omYj{Xwz|~c zcy^-oLx0`@8a(S%+G(5yHK5Q<#l{STNC!_7Z2tMusYU5L1uZrENnESiPYNaT4NOnx z-?3ikmDf>|SBwpJpS%^nK;BTDyc90aLh>%}uObij;K9_M9BQc3co@M<_k;(Ejim@t z$+6ZUIUQyY=u_bzt#eiWG&)@oPifJHrl$KSp4PHeq>>HK8`}`i2pvx@=g9_-(>R8u z`N|#8ASl`z1uC_+Z+sSU&a>$fGkSIY4taYHLf*4Fc^kPr8|1-4L!8Fus?595(@#8x z+`;+5Jv3rx@7S9-A>MUd^3fr_^dSEQlAorNU#iMi%D#r-2>edt5-eY2x?gFIv;uu; z1(N$kyU@~s+!mr-2iH3ba<%md_&=Y##pPbzHF@6sF4kEXzda!6dMDSlqoyl5HGTgB zJx-BS6h{E7Pm~vSS?fx2$53`uxg78PfoThv=zzC`mnEDOkJe>mMMXc9Id6T;6y>RB zoNrD8`k40Nog8?TeQ(Ga`Yy15T@X?3P0^~}NOK!ov67!+1 zV2YE^6&dFqMGJz6_i>NOvQEcMIROsXNdrh-Ny84>+jG^smWO3*$@J_-_%(2`aABO> zz5oQc%PHD4st_!%6w&54&PLVS!Jok-z5NW8?;8$bxuf1!JY0$g*U~c<2wI|J_>hP3 zdf2FS#1IaKnK}lUhZQiaCk7iB96E*+3Y$i4Ak8a?A%Q}LfS8g=%4B4ghU8eVK=Gu8 zhGN3%N#`+9-k8c2ss^b%#^#MV#AAl> z7|9#+4Ug%^V-mbEU+@?A_EoS@gXbX@Bpdd0XF*cP6UBm9B{g{3vtUgM-p+zJ^=a@lVL`Tf zP?rU35$eZ+w4dfVhi=op3MbVKY>ZpYzJKUoeAt8RHrm3%7F-P3sag7(GPw?~3fZ5Y@5gOVS84 zaID@AMmng6c;Fbzqer!N#)1YoZ$+!Y*t`cQB^M9Spb_h_#w*aF@;hwv!S5J|Rh`V7 zrRh0saj;V0i~6Qa`c4&XaR9}#o~}aS9M$s7!uxkFHNKFLO3t)=FKAkxxD<%{lQHjjPasyruk(_3fbN3&SE2pu`kF-yK5YFucget2j zug=a8MUS8)GO2>g8++5oBen1dF{_KMSc@|Bpn=|FC^D~3^md^ibb>mRce#cs;|avm zBTOYz#z87=mvPt6toVXynMpp)Gzo%spJ4cnZRt=|HRTSdRn!9ysMVACB$UDmK__bSC=)chDv5n=nE5X616kLhm3XrX0Ksp=7MarXWojACNs1 z8_U2#SH@oUCJQ3i+N>*b8`cz&h zc(G#VJPFxXZh-{smzy#kWrbnQ%LUiHI<6g1ML7$AabYS<>AyoA- zb@D9JE(&@n-LYM$JbsjB2nnDugTj)N)k-S6B70(ohBVJMlX$1~4hGljj#Qk%r!Y-- zMxMkk%N!1W6f-Mhj+kBxQHu~KBi`7}iwU*7xs38d`xxjUjdMSx^6HEw!nY9eE)fFhx2@aClM(FiwF6#6{`dC*(gSye$NV5KWPJb`>q zE}t@_ukn>T=k!o|1g@hoa~)|` z+Pw!d81JuP!+A67ALcGr9rgfZ7ar-uT_l$I?AA~=!d<=``Nb_yIlqufkLhdg)0>J2 z@1Em-@NBHB#JQfv+xKhJf~kmT6oHj5-FhW(>7zjFVVT zWS0v~8D&gid>Zc1&DI<^gUB*qc ziXk^Uja!@?d62DXL@7(L6GxMTx-s%LrDp})yY(wX`K4l=+FC?RXbyI!cmi!4%j+-* zmI{wrsNXFtkCMM9v``5L)^YM-7@R@gjxRD{ce7Om5>7QbF4i8`7s8%rDN-*-kJ8=& zR-()XsqjdYRI6C|9K%4R2L{^ZHDSLrazTEm8x2s~!hbXRG5(`2 z`1oI!l+-3&!qWT7FJ>1-=K4q$$RZvuGW{*$~HVXq~Cv*F?$45@sS`=erNt)*J4by_MsV3BH-NGp*) zx(rf_5~=&{AAz~9+BRpwAa&|-|Bh+DSuDV|8QL};Xm>?SyR1fD}$IHagkW)CJ z>N3WHFV4v}GMYMrp9Y2MF=>7u+d4^=W!Yl>Y4)ou{|=Swij0|qX+6%L!lPk^MAdAA zp&|a=D)BE=jdw*x%%+awGER>@u0?tKOZI$!_4&rC{Ai8IE5DN(AEd@Ny)M3q8ec<= zzetWA;237w7*Twf&Z+cyP6E*%tHU1*L zBPP`U4wnBCtOrv4Ul+eyjW1KqK^q^a#%X{QqrLnpM+{(4b~xFXxB^;1jZ@V1}U zYJ3s~Y{fPH-03aXcp)Yfwl;Am_UbDq0u*2CPjJ2 z-;i&s5|6G#pc4c-QJ|9qdbmKR2=s)ibdCcZicX&#T3yxC*#bR7pmPP925S1}X^f<& z-2z=A(4_)hCeVsN)A&p;|AIh=U=XX1w+M8YK!*!-lt9~bw9ER!HuRq~ZA&@>!^Tz~ z4WxYQTfbvpY%;?JlYIOY*aPD?EVI${7vIi`p--LZ_p<_-{aCd8(0UdQRp2cH%wOj$5#wjRO6jwpYw2y!Ps{Ufja%%W&#{U|+xIzcl-^Xybp> z=WqM3KWaafNq^3Nnbw@EOiI!09qXJ#PpC?B7sl%^OQ^4G9nJHrzUoa)?s|L9$Cm&o?wr#U-E&P{gxr@%Wq|lBGA9*|Md1m`3ch57typz zsh9uT{!-PCe5R(K-d}3@C7)Sce)axS@%Fj-T0&K<5c`fk3-;w3gmRVAZ8pqT{FZN(H)1pcR3x5a0IWQ!BedG#soc8?Y?f?J0J*wkb zvPZ4I{d4xH_TOgM7|GMhCj@kL<)hlB0~%a!pTBQ^*Ug`Q&maC?yf`2KL;Nmln?l~6 z@(%rv+S7IUNeU6>Q~%BOg!)LTx*ovhi^F~XkkR__G_6PI#?zHm)&pGDJ!`c751Sf) zcz#Rzn1%BBdzV*L|KqZ*Sgqxs4*2}h`TgH5|LS@{9QLFvaX4>7n{8oiYpyaO5$zvG zjp_2u@6rF@L>*h-aNsMG|L5~h_3`&wulS?=jXGXX$3Ut-cEd6I+V`Iw|NOV-mz6pC z@ek@NCtNokB05T-Z2~O`bb>%93Urb{4;Sc^Dl}U!n!q5NTKyR7q8X#L;M|FmZ|{{7F+XVmtp&S_M8$rIY^e|A3e|8{#u z|CP_}-M#l8d%uCbtM=(&HmTZYmSCSWchdJS`uUPJf5-t(b@K<+_8s8(@7ur2I%1jT z|MuL#9xy-9;&o^xViN zEzYM^dBeT&DxKn`=?s-`{sjA|GN0!9@I7|bAO4nqrRRUkzA66iJf6V%`66x~Hacb` z&Y!V(*iV(*VE~G{rV8w zPoO2moC+||c?+ypgQim~>b8U90wTQMyJ2G&I!@BVwAvkGT8+~!?1&4tybYM9@&hJ% z|4Cz=e8{6wiz7(osS>|@leUG;>{m@#OFTqVY`tOo`^R58U~oGB^T*D!Y4)B2A=TNtHpIMvy^$kj z#-4!yoA0RYK|LkH+K1kr-QWSc-k$$n>v!m%Kjrr3pf56t{vP`SpYZsk8^!u8s?7R0gr zyu6|t_;cv{m(I6vK6(yM&sI%(!hGOAyIz6({g>x&zvut?NS8VR^?c4B^#9^{RA$d^ z=90NRtIpp6NVQ+p`TJ-L%~j&X{(uzBG_!7PrpM9hF zzq);pJ^s1*Enjct>$xQF&z-mae{6n>{^vLBm-b7TrRv8cRqeIP%#`u}w!QxLc*bS* z%;ffIF1&$#{#pCO`m!dUeg9D$&;MEZWPiW49**{h?v3?7;qL!I{-FRQCd{|L6JB-x z`Ul@H`n&ln^mmmx)cbnpH#v-O;8*Q0a|Jq2pbG@rEzp0g|Ead_83nezOY5{Hc zRnHHqe`zja6MdSK{zOq-_T8MyYzHhAj+tIR~ck{yh;AvCzWJ-wHD4(iu&V~O`ra&H{nTL3SNEe z1zLWq@^Aiw{NMaR{%wDdAAi6359$Byb^PeBavVVUcq_))Ga%0OP>?i!D)#?7?~BUl9#jt>L)8p@-MaK} ze&h}Ae-f78*#G>_ewgS_{uiI#LU&d*HM#=pyMY|nq`dJ)q9lk?&0)_4Al ze$M`8{H3y5&Om&7>1{W@kCfh3{U&6pk$t~LN}m)&-)95C%D-p|O%CxlVCj>vwo`6&LDkP;&K?ZY1^ z!ne92+;F0gvMyG+^aQM6h%p@x!k#QH{Q2alEm$D)92ZufPkc0{+z~a1_g%zVD-$miO~7pc-+G3y2;cU|RmL|Cl=XC|Pp!cGI-A zElQ}c^yO<>UNc#Si|UCg>gA8lM`i`=tsi_xX-847hfuFmMv+)tMZNAK@?(`U6ivaO zx}=JqLT-&N1gZO>D_NeBgB!OB{%7%E5Os@s)yI>^h^I>ZIo2>~sra5PldIVot5jFB zX0(>8{((f(@+PRr$o}{Xk2Xk&8xmZGg;%XoV+#*ZrN;XGfO=IcHBR1*T8(h?dcFN| zRC&OWcIVROLq}}Y8RL_vs*fFWTA+5e+lF5%WQJ>T5F**HGW!mY< zlj=@F5-hr^J@O9+#>&6ammTpJs;oy}!$&poM~Np zQ;dyrpfEXm^&4~g{bf4L!9+n)4kR%-pFs{upvR+B3Dm`s1p4bhWjg;OIcrMMfvINJ zrr;A<(Xw$+&mbwfS5Vqvv>kJDbT4z-kND!)Pwab?E-U^Li$Sr`cNOImgT0B+yjdN9 zdCQyg8;Ren@~iBX*~C3qowfMd_lgEUBi#7gfhbYv!5p=&zNG>)7=rL$lE>pOZ82}E z%MZR%*wvg`%WEI>VM*AaSD!!oJj;)K9-mn{!;1Q)f%t2_^!dvKd}8v_ZR1Q-$T6}T zfAfp!4|iJJ!$6sm`vyp^w4n4$L1S+|w>BVc7|hD9&RBfUVErqu(S`I2$XVP?7siyO zmtBb1A#YD#i(zt?I??#dVVC-2PO49pZ^ZdsQ+|zeYyGo&1f?!BEstq*Em@Aa=~{U% zj8bhs&Kh!$AZJXYE5NUmXJ@B(Z6LXN1<@ZzH%O))YsK~{6*NT5ZZy-~D^ z31|Xp6|pK_DoDMYkKtc5B*!%;PKJ)w{rRN^J zVg8_up7YAP%g0$~X!(e5Idta09G_?TQ_BbIng7qo2M)~+_zC)TRLsojm-fCA2nq8E zf^AJLzUBat?TevdP>l>+5~D%^CqTaAL%92FpK3at-Xg-iPwlFZ zRbP7b2K8CG-TTg1+(+bld}-tXiz7f0?hE}^plndhh`DbaXtG$u& zF2dg(Uu^Xe#JbD7y-#&Ztr3v}t`wZ+jH$(Srx!;zh<4*vO8##zb!JXg7hBwpan~p8 zWcfF1f5em>8WU6IRNXIT3AbuB z>imr~n$ri?3GL&HMdYoi>U?GOEbL`U!G(*|vkV!LAgH0AB^HzaKJWQKF*nK{d5*s# zZ>~RM@%ZA_b!8mQS79ukT9Q~DDQ{h0+HVyPeR$a0SUj+9wHp6Is<5;qiBE6_rF zwK`?(ef!h$LdIo9t6P3yYfI_EiG2~&k|jRC=SL~7(DFrcKK+o zIHB%TuI7et>( zrhsb>xX+B^{(eU_f9(3=&XICwBM0eU$$g)1J3pW&pNe+butS#${l$G39gX4-nZPrct=j+gwuoQCb2riJ>aSR_meK=9##0dnM0zBu9JyU{}a@K zu)NSoC;JFhJBy1>^ z0}F>6Nql{-n7F@yJK6YU+R+E9S66pZ??IV_I?Yt2o}3yi_sFPAFj?DH6~T6xV2~O! z9cGE0I1*g9a9Z!8w9*u6GusL%zqtzAeS%+ulNw6 zC7;|Tx24e1H>4$cd4bllE#2m|^ebxR#?xzaMV8HtWV1p#c&C}VU=7@>udGp5ykcMB ztl!S!26!$Av|ewrHge_Zy>QnO21Rq4VzIVUIY|C_I$9K17`+pw@WB)X?puK1>0u6O zPK*L4f!9bJ$uF4hSP|Z$0;eyZt8??2tH)co|6<)nbJ$~HZqG16@i#ZhEzk*24=g-3 z3=e_m;7V@vtz=X_ah`0JZ#UzxcKtRi-@eYb&p0OE`WFsn<5<(ZO_l@r+oo}$u+{2e zP|`jVMXeC|oYq{q;~?XYaLGjHv-bbPV?hXyT>gWa#vLV3P=p-*4@`p9D@C{-)bE^ofl2Nyjz6jjI)vn0 z$NeJ#)jsMvzB7@9gkOo^fN#sQ^@L2ixMhodcg{vCa(lFCvj;D8J%Y6F2m^Wkx!+iN zQrP;uA<-EbEV`p1);ThAwevjC$SeQC@twSjz9BbyuXRqOSUCCD02CWzC}ga&3;bNz zK$A*;U?e$Xaz);0X`FrAzk$UTgi$ zelXJ6oR4bNg&&C}j5SK8giEQM2e>(s-`Dcqv`Vg%YN^rOUT}LpKO{Cs4i%aVdoApH zOYYB!sCzEC8Q{LyNqqNt^dpElM%qZe7x|i^@doEj4Qgbc*qC9w0ynXWPsz1jrPV7% zbcGyiIr*96(1*vy6icO$A_YH?n2&)Wz#xRIHC&Yo3*R!%SCBq=QEbdhg!8G;DTvr5 zpIw6)$lTSN+J7FkG-n@v3Y`5im9(GLrLFx{t&$eL%@gWPkR!lYyVvI7pXCVW0~%DU zkn+RH0a+%hFLFOqveHoC3{1UGWE9-+v0 z)k&n>f8}2yzJ!^&xl*p-*73vZI9^z82nd?${yxr6pP}yYc}P{|ium770PtFeqtT;> z`O#5~3!#a%rtiOUC6aS;Sl?k4>4R#M_>%q8N`VfIJNSqy1~WPYS3wXEp3eHIT$sS5 za6|eBs(n33R2D7pyysM=j(;QoX{^s-erPa?anExW5|vdHVDBxVbMie?UBQ5ymeULj z@>l-VS41u3SM_QoIU7tCM=OjwmO%-=7P^!>vpo-zDUAGWE~WQ)xmMbT zHsxk5ms!c-{gb&Kul7Y^fWijOK_|<5ewp^o3MMeuSM)^vU@l;TxL*Pa4F&DG3lUuD zk}C`7H+@@nK9%?0y%r&MY-!zk+`R;e;-f5Se$kjmni~4(i8PlpHa(E$H(AmozkoDn zcL;7>Ni*G}OSLq<1G@M^^1es9d=t6|%)$_Q==1Q-qstwO^2*Z-oO#kD#JB-j)V^kx z9=DC%33{9(th_y|>^wc1&{)0FLmJ-!JvNP(_q&xImzy*RJ#L3ZGxU&a?yQgHhP$4b z0xcmIZkCDW@I*!7e-bh?PuNl}A~jC>1afQ|?%8OX}W zfmsg7Kb5(}M4S9KPJ>*t>1$0pXu`2+!;R;VVAg9^o@_ew@WMVxoJ+ z_`pDK!z*)YWdJgEuV;LJ(j$K-om*&F@NGc6yz$+g_|E^R7kpnhVTbq*ZQMin{)~nN z-)j)3J%I16>OgpgF;9W1@!kB*9pZb!*gb^rpJ-U{{WW@g58!*x2fd8%0XxQbynhej zyOf3n-d$5M*Bw2{vwOYxbN`}kA;DHuHwf%0{5Nht@_G2T3E%Ai|C9D0{S4(Lx?y{iCp+&7xpSCq=e(bFqP*|XDXY$mHM+Z(alW^Ue|nqq znzROT-^NaP{0(bw67J?2z#X_`OxGe^T(bKF(-R9K^Q%`A(#cq5JTpCIdHj7s8+JIW z$(oWYBaH9*u=O;-qffdb-Q63zXiRUzEAy3J{c`EAEKJg7d3p!@{R)i>32wx634iY) z{NLLV{(Zjy|N1?NKaC6ize2_ALHys{5&j2?jOCbqyYVM@1j}>3SWolST^)ZK7yMVC zV)h{Z=^f!eR0(yj?%GA8_ay!_F8HT0b>RP{t=f+K(`VlC#7yZ};QJo>L*wh8{1xIyccSPdh;UsGZrLGkdC2AIth05{Xq|9Z z?|8Q!*Bjm^N~smXO?p!q!fHB_8h*$-|Q9Nf*s@g?6JEY-%}u=;5!L@ z$r$mU7cW)bs9D@CYFtAB0eo3J_`8mTLsp5h%YQ~f(%kV$jl?HB5|h_nsai#lB?XCd zn5n<351dNQrVrcqsRu@W6fMhocS07fRRnfKZcWWdyg;dr)xRq5i9nX=#tGZYC2A}b zIII6Bgk{x$=E|fiS-X?u5icD5SK(#2sgw(f&D#1(NzvulO|!P0>ujUP$jgTnIA-m` z3b_cm*FYvssXC{sI*Cf5LUy@0`W<*p2*nM-60L?Hf{RKT0zFwS2wNIWx3aUdGw;1S zNIwg(Qr~6uT;6A%O&c1>GKh<|>vVo0-7{8icn-ue{^qZg(gOeeUya}S>v#9~?MI6{ z8oxr3&ECfEAEUl_{JyX1{Qow7>!o|f@4KE)Wb(ILvXM7_UphYDRljx-J6XS4vQASj zKdkjFCWDxkF_k+EY@IW*H>%VCPMuM+UJ;z_UXqDnH78bXFJvN+qFHju2L*&|*`FZ!nZ$9|1kf&?NZnD$A z+AGzIuidAzcaP|efsch|qT9VZZtxaet=C<%{M$W#^EDwq{96aEyTkw6PE-f|q@>i|5@v#UoiyJsR=}zL%qWJ$WTwqxU1+@a~@U4~k#y zSxCRSfAH5=gj0I=57rAm_hw(Fd_!LDST?Iwo$j*vf94vRGb~v7az3&Cbos`$!@-v>6mch3ui?3P_kPriDU0-TJ7u^=+9=LM0!KQH#A z>w?HRgc(>6k;!9F6h}^gwuPT*!t8wJWm1wD(ha zaZ~HtVJl=iUdG>Lw?yqK zvK&)^m`~Q_$eX_^K<;V0h18M0?htmbiBZhvPYRzoNIqb0SSlT!?f#beLw3AmQ-ZLg znv|%sK&~ha<3*U^Ixg;rG??~P?4x`?vLADVZ$t(QFTKQ|k`!Mi>8e5Nf>M8bP|gOB z?JeciY#j8Kh7*EaLCE|51oi!4e6McuaBf$PNKSlM5(lM|kE1_N|K6;*KM#EmKbu`IRruAf`pYtC^o;3Zc_@_m)>PW{Y z3WBVWI=z8od*sIZ++J6mhfImu8P$zGQZtWe1@4m-;?`ux&t;(}|5^6uIR}D>nVRr` z`B1_0|9SY~-~DBYSu`*#C#QAzW)5g#aB57eG%Rdm&sn1aRMrc_wxy=}$dEN(uqLgE zUsUP~)vk#24O@+?q^{k#Wf~tWcZTmR9F*-}Xy7b>P_l`S+}Nu8KBsa>M~83LaC>T1 za`o&%>=28b!2aRLIBTjR0sEEM1QFe2+7m12B<*m1j5xT$EA8rl5BFVSX_EJ5+EXi3 zp;aa%asNZPbI)Fyo4dh7&h~)uBp5Fe?LuW!_B&pAQCmpINucRqt%BpdM9Jv1|-1Q`x=w zq9;HxLtaTrd~;+fn|J^%zRsxcZ!9=VEK+K~503J&jikk#QGA2xTcOS`3j<4PY$;TJ_z|Xl1D$gs>x#81AEvq{7U7%?4Q^oqn~PE%tb2%2sYYm;K)?v zIbi%#8ZJ!#O6#eB>ZdsULoa?Do*}P`(>GV)^BHRM~;wv0zw#_xg)!RYCl)I1|ESA$~|o&Kl8MRR%L=x0nFDK zxjMsqSJsb;Oqn&Xz9Kpl84yNthjYdQ<6GYl9g!(oLrf^cU&exm@JZuupKB!l!jJUc z8TvFw50$eL6f44CnQxy`--2JuayP~Y{wdWh*_f>-aHXP1N}Q{xYFb55puVz+yn5L? z(ixp+T8*WY1q(u{DZYh`RrQ@l;%8t|-C4a-_H!L08>>ufV;;5I8xosmziIkjG_5&o znkfvY#`=W73mb>gQdtOo-JZN}hJ9^lJkz=c#gkFj%i*oJk@EoCOLR%YVRL5R_$WApK|hC`kPjN75#6PdN=}M`5W)_ z8Oansa3MUc9%e_IZSLH$02^KaH`_Xb~52}>CI2)X?F4}3v^Lhga&&TjA`^otEPkt`x z3n=Llb?O)%pn$HK&3}%6dMUb=4JH42;A*$~XlouawO$Fg^yKZU2GFDrX~% zYb<`a@-9KrdY|2x&e;@+^Bz|95|uGtIpwq$zrHez`dUN9<>9*fDi`s>h(E%=vSctS z%+AI$!hCqH>>9f$XRnOKf2jNswW>FRMRu$FpHXl@nmp?Vn$NLMv9C8FZ=uBZ_@RrP<%-#FWe((W08P&F5u02C|v7N zd4cSaa!-S)(F+1_sT_wQFwVT$=}!bv+iMwItm2izFb6=OypeTo`n1clM7Lvf>0C~^ zX@i~BxY4~z#-3fcGPL1Tpr%vt@VWGh^)nJCKOkWL__|WKXOxlrlQ5Qo!?@{j#bhO$ z>)6GKG*k#v)HGR-Ga_L!=H;>;tpvlCP+4@Xein6Wc2l>ZellEdVvS}EjIVEq?4ukt zRI&NV8s3@KJ3+0k(Q`fMpHROxrGNDIZ*~8^O-A6N`Ij-4qmru;+XPY^>8EzTE)~?R z4gt{oWVKmzd!_8v5&HJfkJ-=Db84FthAY^V+-xLdVt7jG(&G!7YY(D`7qG9&+z9g?sI)caf*sY0`8@(`kulxkablz7ERUP9|` zR51YDkZP>hT;z)sn|9*^qAHqEUW?qrg~!=4dE#N(%4U61FwFkP)=8~hm8~7+y!w$a zRpUm}p3@SxFLCTkUX>Qh!uE_6&J}znEpApmcAi(6Z4Taex3u#`H%XsTb9VQ>LCG)k zEj!QOblw(RQG}23x0^qp+jE$mBTeuR-*0N{?fYl8|9%Q)Vb+L^@yqY^MGxm7ktJs8 z@(LNv66CHE*L}m;?){Gn&uIQWd%0=PTj3DT*8RzRp?!Qz5ahs_MVu0FmouA}NZ`E= zvvT=yKNe^B@W#d|r;O)m9U$JMU$AwyF~+G$d6eq9l;xx#cXs(F-caguY(8yQXO5Yv zi_R70u=Ne;dxPWu5T<#GLCtS(igU6Wu^RbU{DP6&PcUXyWM&PiE1IRH7vIq>Zuo!} zucO6JrdOrc=ZrvZIFX~iNXo@*ops?vmgBPQ5*eWhJt`+v+*&b z1n|fK3Z?~pf6J*T1<|bk3LqTrCQiQ|f79CbAR{neP5P=^R5$JdI?4L6C4V_C7ccl6 ziz*zOm8~xE0x=V3yVq`F$g=GdZ^(U5w|TUdpOY9vrGq$}XRY|IY5aTP*TQMbossA} zezWiXYrwcFy;hwkTKdo-eE29xA|1y+y*E2^-#UPS{W(Tuk;q%FKX<@}Zn~fgyyAhK z?XKLI2d_8J6Gyy?$O(IS?Kx#QAL>Alh6X&ps%d9)rbiY(HQ$$T#3zvv5<++;jQYTh zAtggO@y^QwK}gN}2wiY+hTL^p9=%^$QZ}5Elk(`<VoS7uB2AP>PBd$u zj9eSC=MtY;;~`257X_>j1c`HHH!%kbYs53V|7Gf#;h%#8*423E@6E0>eQ8|$+Lyd= zlQmYR=2e+_hVla!neorJMV^z#@4N6@mEyNEfhEg7{;47BMgQqdR-3o8A&&ve!?nwd#EigVdD9YC#-gyQgYjY-&CSkNQ zWK*=b{cvK1#|Ol!H#ZI{i}bO_msyR2LiS~);kt*lqo0_IHt>QlS4#ZRNc@cw6+?uN zHaJrNLKMl}s^0DDt0lBw&=#mXg2+b)e~0q-lkl()@RE2s(q|eWH4!QE+m{WJ?5SiM zAGl-jX!G!wdDJY5*x+m=PLUs+g;XlXS;$`DfPGDwA1WKSd{3+f!xPyHEDDX}bv!E- z9ZYTx7=ew! zi}~Fkze9FlKpB3qC;`f9RGHNhYTZ(Lzc13i$=)mew9oorqFu}ZBy7ZJV%ACUVZU%~ zo8P#tHBb|-Jkm%s)6xBf(E*`&=Qbnp0RY6;e;%^0A3(&7yPCt+ph|Th3CDZ0sDL=Gj?PDz~0PwS(*q~{I*)piLd{3dYEZxyrU3>0X<;MBN%JMI`eN1mN8g= zq99;x3|b%Is;Xg;q#79nH_2DXhcF-WFOT`!z+9NmNN^6Ia%TQBKruWmfEGPy@OdGSp$w03Ka<&} zXRkzx<_+*~e#{q{tOhsI1eIpb4*NK>A)`(3ku*zuhaV@WibtEZdfI6u7Rb1(kgoOy zIeJ89FHQz8h?QXx7p&c8)pr9ji(G%CIA#(>S^Xcd1``fY@&Wn6_F3NOaPBR#ksBOF zCS(_dt+i@=24}{njl*r-6IJKh9(RQnzn7jra!-e#T0PcXRB9wUh)1|`{?CPr6hDRH z+qN0W2Pp8y&%G~$kMN_agYdbxsL~jX7I01nuSSW|Sr~=|ts)sI*w($6O6m)DI=}o6 zN}R&KRQ^>`b{JDZ|HJa=J`5qG?3Uj{sXW37Abu5RatkE@kYz0fR4HtqTdbOvXRoad zpyZEl?jr&`|4skDR0mV3r8wYuP4WQMvA3O)WT)(nx06yGvE{t}Cdarf6DH(2bU&WPi|TngM&K3B$>2dUWwA?$eA>IWJ7hN%n00biMsl%KKoKWI6rf6#zh97m zNE()$DX*;mqA~}GUabIr^zNIYcf~hA?@k_BM#OJKRx3-CS4{dq6_C{H4$+eMTfWF2 znHq>saG=plm`s*oKl6F)qOo2~c=q^|({kQFTR8~X-@Sf%oTfh#iC zzj-{9|Kwh(kH>&JPd6Bp^?x_RCuY$1`^_`7AXF*llkVkFl9Pg_Zp2cI+XFEa7~ zD}glAzKE4Nv2^{#6fng7X-!U?;gGYbLJJ*_O^qoaA}d4U>_Vz zz7lOR?XfWHY`W`+{v&Lg&T9a{KT)@I#UKKxv@Er7rV)@k63|pEIb} zmb#+uGVkqqw0E6Q$w(-;1&b~K$Z~*L$C2F$0huURd5ClEtkaoBG4dJ+^s4&`Xf0xt zO5JIB?bEiqO(7iPYxTJr!c70t6yX8}x7+w%Rpy4USJ`@U25`HOB(gU!l)1A6XWieW zx&FfTL}1#&G)(j2U^N2)x5GsktPaz9*0fKPfkXamKRniIBsTIcWQFJeCnE$mZ;ddw zK9#|EHajR`JH(vPPvh6(*TPoTM0E5x21v~xYx8;1sXuu9D z<(CDziqTJHq9DDV3>I7E+r9kpTFlP5G#Iu|rMcBoG@7D+QPj@Ie9rwVA!}9G=BwH# zjKq&94O>qL($uY?u0UzpCzpkxQ*t77=Ak@b_rpxd?5Zo+9wUiJOoi1?M&CeG1@;Kq z3Dt=%;+!QRHsGENxTltdXi2~hTPsCW`^8V9TcN2CM(LAgx>d*|#_=maK9GGKGxB}4 zM0!ev90_R#Muu#(3)zGe43g^qX&Qui=zqQZl0if=L>m`-k^BV{Jb+5{{VC<{ z2a2bGKyJRduFM^)WJ3lL?e>g&ta=0v-V24?m-KsD)ePe4M<_^uYODKDUVYfIR82B~ zu*^LZT8Vs2EOW2S$x+xESE@V%79KPBs>$J73=1BV+lS9&{uZBIu2GrCi#&a6Jp(?- z5BQ*?%G}QYz~iszbQ#NxlIJsz&{_dA61X-Y#KNpQ1x7xW3TE9z$S8qDT(v=lF(}v@OU6zZ(uOG_XMO1-_O5N1}D=@kzP@+ZS zEWUErW&L_V(mhBuNww>F2^{6_HR{J!^+wx^RLPqpZ@~N79udFu9h!^Z4f)L%$G3m< zvNAsU`}nrzSr>#;1K!<>0KTq0YH{A3q!5N`vX zPzAHK73~ehP>t}y_$q6Z*kjW&bN4}r#Vrx;`b*Z za-#mc!*wv#(=*eUp}!LSSNa`24)(ON^!wy{k!oRc@K$pxK1XA5@lOBO?e|bKV7(?4hT#!?5HkL}+H8I2@IdXd>qj)E2E|OvNDXSH=|F1is??RM z8e-e}&3s~sOf`nB!jMTEmmu$Q{}S3}@}0TY!%Uq@YQllWQok={u8M8#H)|hU2p#co z#qR)~bXyL7|73VuSbLl~W0fLgJInn2Pa{8%13xRD@H6jG{LR7qesWpI0)Wa-GVcm8 z??Q{LlQRvo{w;}^y2$f`$kY_g^(k38_0uE6>MuvLrRJAAkD(FhZuX(3J*hl4dV2H# zo#r#CTvA^f2%4qem%j=Z5%Dek<_D;F z%k5He#uL56CO*a0Aw4As;eq?D1`JGN15n9*Hs;dlLsd{3GJd)7nm)1l{Z5ITQv2+@ukWzO^!GLX>ARF2UFHK`K?KMF+qfkN zanlRD@wBhuko^OOwOQAI_q>(njAaTHQHPBckQPd>KqohE#E@i=Xdq>{p=^IJ_dNYP z`>b=t;1Zc{d}`WPG{kDgMr(58Ej%gb6E~JSS1OaYeyOBSsj^mQ3}GWR&u)Y(#zvks ztxKTPI*C{?ssZZ3ezDP$qL+~ENJFB0$NE@F`VQ8b;f${*_w0_A=pho3IY25Ki}#Da z(-`aUsTI|LF>V=(baB7Jf(IkgZR)QJ(Gijsm*5enY4Kh)E40Rif$^F?P^)lxY{QP;!OBNx#WB z*4rEFBf!c2Q(}eA~<@8|UYgJNv?G za5>ecMK2A?Hdi>%auy%*fafEOeVqy_@%mu2z&!E`Cy{b0n)-!| zrcSqKsF0&4>MMFk`fFMKPi~$!n8|Hf{C$<}Q4&9CWQ0{_fYtmO{Sb(`=pys1=E9Fw z0FWrcedVPo*KN)&OKzUsKYed|Y!vSVUTTUyWZCKxQ6it=@ErB0BDr|N`Y>J>o^`x< zI)Nl=*~wq;=YATArQI%q0biBGYeq&2!9!~d^o4$ZpMD>G+r-fk$L9|+aAKDLJ&%99 zRZrPOO2^MB)>T2K@0Qu8VMzmhe4HgctA_bI={$4jd%amy$Ib!gwhy<#PDNqgI}>TaIDnlh^Sr#~F#6iwg>7jg4#p>$yf5&V#m_&tzGW7R9^f9cU)@Bbjzi6KyE$aKm~oq=Wx zmIrF82r;8{L?eV*5oL&iGxZFt2?bgv>zL7;a z$!aG~Wi;%nJ19CX#-Ztm7|E0QgpV&Zk`3O+LQ|lWw+Y#8&f5YbaSLyk20hyRR?Py} zFRjjC(CXYoB9{zSC`$*yqhhlzJvI+s^cR$vg6=AWqi%siq~@IANo6{tc+* z&iO_{0vZt6o>EmeWrUI1TLA3h1~%aaR^kR`9NH;W;?P1}hzDos{8(0SaBA(*_Smwx zQ|Jqt{@6)y@SD`KPaDMU7aepmZixemTEX5Atm7T5lKx2Mw&?TvtBk}PMzj7xBXK?) z$mAe7SHBzYtDhAqtv}C5UP{5z=^lj^FbxK+kk#%yg^)4E!aK&|(%9A@NYh4aD>5@# z{kYgjj^txK3`g*bFLkC;jQ2WIiABW*AQ1jg{*&gU*xq8g*Ds}-P?59xozKagl7VQU z9c4fLjct;bBuzl)m_8adHO!yQX~w3vAq$BlYi5aui@?SfX=+KM1WkIaYct-S=Yj(o^w02vSpyii(=db0=YKm}}qQto(Qy7(b62lo(Y zDEdnV09yTrclDPUiKqC>u2BXt+E;LEj?4ul*3V0wQyDlJZF-T&|3Yd?Zzu3q@Lf;p znULmJ5~Ik@Z?qnmZX`~CzEW*(3D{b#>BqD@OHt{8T98C#h&UuS(S&;)P+qvmBdSix z)Nr`{^5ID6S5GXh_2BpxaJX@3tH)>bc*2crC&G4!M#9}YR2~>+$umT^cthul8p*zN z>2>&-I4{6yCd;BXA_W#kNw(;U&xSf0>ew9og>oM2Gysp18`aOJM^xf2Qr-*^gn(1ZkGhY47 z{4aK$*smi|7LE%WTFV%teESudtqs@QE5Dr4dYkd9zZjc>ymZJ|UMJQWUyrd|R;aA= zl=AvW)DZGq!M%%I4@ThnG3ue1B%gsg)6P~g8%0^qU&@GpJA9}hUQmBU^e(=^l*w&5 z8LZv$JSch3)}ID;)K_-Yv=7eO(O4bhekA<`?C%cp$3MrAwg`=G`33*-OfHz1EPi21 zWsRSkLb0UDe^VHnBid{dg)t1PkrcN<@VOP{<1ZJ1Ig*&9ezrN+s-N5r0}~xe&9L=> z(qi((fOq8iovb4J+?D67B1<2VFFVa}OU_=lPNM;j|6GnG(RgPl;SAhYu^^;%E;>xO zP%bAEmoaj_PFPiDEd7v2-;9(wHh!ygqP?S5l3^7+L75RDJtjep^Va%k&Ejui(J44Ck8osJfJt@V*++`n1I|*TS!`n{Fdv5vg-K;M^I|j z^A)Msg7uJs=at{GtcbZhlI8wS3~)wYk8bouf9RRMsCXL$SUKV$cp z^J9KU7iZH&$Z7*>WpyMpt*p-Xq@s{qISx|uSXGmYjoAkFK^EqJv-LwrR3Pp^_ibQEV;v~)WBFKng~RwIILQ7t>9#}OC^nLnb$Rpn*Ya7^NbWD?d1hk@ zt#@Og)o%&u9(UhSJSkd6E`-y~$}Ay+u=C(1T~YQ^Zg0&lqf{XH-vEV@t9I$~EZZqT z(LIm%%7#is_iAZF#qp?8K^3>Z#u9uRgq&Wv6vFgT z8Ggs4FuP+c-gZuXQ&5vRrUY@$U?G|*d8{TFIYizbu10nlZ=>8L_ZYY6s*r}zAjD?5 z2ASi~op@yeqz-pzy-_XxkMtse%FJ7L{TpK0bx5>OQ)<5qo^E!((RT3=FKhsAic`lM z36iQ{P^{nm{IEWB&Sc7knpFDvS5RJ(^7iL1sk-PQmaGcQhj3;1?_ixBv6L%V;g0oUUHR@d>qRV?lkk&KL9WtB$QdH%y!r55;afN^LgsOBCf`* z1JqZ3Bk>0wS=ICM^kC*mEIl9xVlQSRS)x=9kanz$opI0mpUkI}$V+N%;(A`!!zzq+ zcSH@9A|6B08Vf{~#kUqlNqLx0oU`|G0NS`-KW4-G|b znQ`kh`V@z8+0v7!=6+LYu7bEbSaINN9`iU*rMAO??lj7qhzpgv2Vhs1UgY&~0rpyY zmh5yre7{7Yp$rUFVVV0Pwe^e9Ue#}9t5$g>X13%){V^^I@GoR+^>Q!o7R=#I-0{KWC@?RZYpwS4lq9=nQ9(afZ;o#{zQMSo%{8TL zky?eyi>Jf#{aKE<+Lu@vF=AUQBjEo5JCJIhoBsxku9!T?m*$KWjD&v}m%GanXp`{x zrW}u(?VtqMMN^2!NaEOPmGv>D+})dzz;0%V0r@0X8o zA%up^3|+p>*gFYJ>BfUbvRxk3auaG+#etb3rO`F9j`NL##1*A$73(2gAHBT~^I2yc zN!>}gc47x>>Vow~Y6ibi@!%oy5Vk6xu!1%2Fm#;&M-n@~PsF2St@XBufFZRqVO1JIlC9}iU4ndDqtT7C(<2nW#zd%Dnf;&+GHQw=Y2Hd* zA$4w*=N(GKNiW04GLmP?d(Y2u!#rsD$%~gau%027_|Lq|z-t3dS9tMIkdcySD93Hd zD00hR+U_qCQ{=u05nx_T-z;%Id`(ECm4NJ&%M5arf<*FP+@A=9qTJZG>--HL$J?)K zB!=+DP0?=kN>|SM5L#dTs{CzrAMNszueHw4I33NpgtfP&jY1dJN!W2-yG8(! zi4DVksLm&1|Gb$x8BU*o6NzZZgj>O*@&nZLM@73>ACGWDGm-PP&Z(Gv4)>E*UXpcw z%3?L+B9}QeupWG``Xin1VaeyDySRI3e5-HP!8&nQa_Xguz+thMNX@ip>K1!aP2D6k z(T&oIJt04FVUi>+d~W(82aKZnOqA#AtBK|t+c&hC0h@JkA$)M%9lr7S#3ygNHgLOLYOij9)G@;zv*>=5VcZ ziLi}Ys)kar(IL(Tkseeohb&;PbzagdKPuwvX_>Xo|-OrxSv|HG%?pR&7GFNw1w(biGzICX_oH@lwT!WSw2JNvkvWIl2@AxpBcgkFA$eIpZ~s z7r-FtpDCjcMK_60N@K%yW6rVJmTHBfja7Axp!5=(T=LDwB-F!*C#qMs2cZ*ot`P-s zX=Q19uwVp_5Rpia5buY}F0`hUkvl4hPvyd&Hq``%;R*&eL@EUrpIr(|{zl5A=KI10 zs7`_=iPh|TQO@loc_a@+eL=R8v3T``R`Y)DHGBp273nXK7P{vWqRGf59B|AE$ki_3 zLN&9LR6=Fuj?4S}$Q^wCKA(2|Tu^sEW(JYzz2#-0Qhp|**Y!E?`3f?+bW)X8q5?y_ z(JXfVme<1fdu!q0cYAH2pLP({f%m{tAw-)Mq$8BGC-@FzIdu!pxy~EJgeMXi(;riqGi|U+^!E~9k{Fw}< zEAx68MXpbem_~Z?eMyZBJ7<}g z&he@%$=nV_4kqhNN0tK?)vQZu*i~-2e+5#t%&bCa2^!9*`Pr(IoF}w4Ocp1wDr1kn zCx+&n|1_N|M*1%8pcsp~z^yF*BAiGKyE_q6p!_?@!c#}aNOav;T-A_Z7W5k#woG2< z;){G(04Y!2L%H%KIb%CD;HtI0f<@M*Vb5ACTAf3EE1uoLsTecRK>b7P$6gt-OB-rx zBL~czY_*Z)`-09iWlw|Zx1r`@U-S*lu5;hmo(1x?$T4&qW@3x;$H6uFd!UoEMmhD; z{VtA3)_(_VEWN1Ei{!k_nbGr7!Bt}2WSgmQ=Co&(lt4bENH3J)ym__q2eY}ild9N? zVkG=jYA}{Qk6JtySmN)nZ^bObQ*?*Pn^@-zBl!jnK|}2GnIs1A%bpajZm}O-z4e<&ZT<&!_Ck>#jWMkeRSzoRzG zMce3 z2W^&AB!B#^&zThwC)mdyZ~GcAg9p9G@r|GR7e)c9e;eV^E9eioTkm7?D;kL zh9Q=1{IRWMCH$Ilei33H#Ds@m6Nu~bOFYL&M7&0+t9+%|`g{Wt5p))DQtJ8RNJ|Q-TbC+rGsSuP2r_ROQP$Y5ItkyG2$!y}Z z!ct%K51=+KQkK4NQGClm-X&}C1?g8a z{RX7tbW*LyfpMRJ#o@8)+MLrJpV;JNF68~A+m>Qrg-Tv+GDlP-ESpoWf3I|iIdFMjs8dK zs#EP0zbkvf@W~ZAz$5B(^Z5OWqet2kC*QDDLi)1^3{EXi`F+;-$&wL1E^@FnVX`cH zQeR$4QD2=Jj@p$S49p*{^PMTjib^zaQn?qESnCV{PgDlk{?7Up{8XEPf{}x&TQ)b+ z>F?}oCZ|s6Oy85!hu%byv*hyV!~T(pu(h|t!nmmq?3-%7hY;GV6YH-a--u@+(QCL% z9?AN}YmPAzvOXt~5apqc)p?#>*11y6kC2OH+;lq?C6h@q28bmmPvx(%*zc>q)JR-} z<0SbvW27{L>FBhgE@DfMROPz!Xo%`BUx9&Yu zQE_NB4}u22SCTHPzY*5gH#igUPf%$|b*fV~6$xfafGC>lTFE)m*~XfRYRhpRyt5i) z55V5`0fc6%;~nLZ5%rBmLdMHje5NloiJ4xBu|O7~IVawFQHocaGX~wscCF6!i|lb_ zB>5VP_qC|Ucd`jG))9y-7YEBl#{Ekukzr*BgR+*WmRSBFL#;Q=rRvohBDuvKxv63h zL(4`?$yb)CB2vRSJ`7@rupvh2)Iv9<+GdJckp6{Vow}UtDD;?382AL}4QDIrYlFzPt&bVLr?FC>B9r2e>~)<#3)i8x!jZ zCB&$fYJMixQEV*G$4j*qrp{PJ=**+>+%XyY>NVP~n-LX#BL@p@i7c^mm+lp9-J|J7 z+3ldL79rQ};nH!2pM>^8)O|z&LQ5G`U@FwG%o+(DS2AyYxaLIxv z*hmvk;vdtTh`al!{MPzUv~<`=E(a{}Ih8-kGLW&@^u@L^&H5`ZVq3ozi8|}vk)aZw zBXVB6W+(^Z3*DJ*`5zoZC1dd@_VryJ-QP@^KL3L!N`u5Rjm1|2^$K1Q*X&TeaB$-x z+4;i+A}-Gui>oE_AeAC#v49YL+tbH@5IHupIECJwRPKac6CB0G+>3nk-1Z*P=z714 z==H20Y8DFg50lXHEboW46PF|!IrnmN#wL1X31=pIXo#E?w`F%+PN+(D&TGnF(y?X} zU~dh`Wi@M^*~_v3xYsGe*8myNL*`)dRwdzxr(unzOTbfx5_pJLryr^AOy8iMa{gr3 z^}ie5bs*6BX5OufYbe0Mlf3a&kUL;MDjSM|9C(pDmml_C-1rgS+H5Si5n1$GA6rjw zNa=T3lVRKR4^7|3&UIc$zt%|J$_HMeqlACGWamGU6&+$phaLZNKZvvPOr23GXUWL7 z?6=19oZLW@pe5gnt`&DORx=~Izoc+Q4N7cw$7Im=aQ<_R&6oy00dphRO9IpZ-ZYLBvQ#H zN0j-EL#e{}jq<)0o=J>Q4vgyB<&Ku?!Sm>~|LUE{lB^%hHtQ8QB-!FCb z+wpCM#)8k78nG!jA%BGh$lZF;?SSuc{G7wbm3%tRTV=igc+MnXGi^m{|K`UzAoA>L z3S^S2x4IAa2Fx42p2tlsU?L4xcA$>d19i0Iq;T?OeI*H8e*-+syGN9lp_O|)uqeC~ zJk_r-BQg@@%)~s9L}ht^jM^DMbTcjLU_R4I3ESxIgn6noOjXWwy3TZGhczhTZbpDYatJ!X&6BASw*Mg@ zg>KmPIeap6IteVizN8?q@DY(0IgB?kC6Q0_*uq2 zXnDxcK}kbC|7#&JEiEF&VtDqqxC{rsAE2k^Q0JKZJ&%+O1%etLnWZw3}Es9zQiS+9J> zDSjOFCj;>W`-Gr7+gOd{)WZQqhvdWZlY`p?4%EszWq4#D%TkS<;nWc++z_4yrw3|# z>W`E52BlKd2oVOXN9twV^MvJ%dI(s1edXYJ&u1hsHDDZ#_?HS(r`aGC<1f78;V;V` zJ&eUukvkZNYEL(?U!V08Zy+)%%46@Y0;QIst5>V>NdFw3_$i~{od@DydO1DA8V99^ zXZr7{Z}_0>;9cYcdfq#~=ZNlk|8%vc;}`UQ)o$qj@jq{m`JkQbzX}oCB{=~9j~>YN z|HvGL|1|@8>;F=b!#`>{e2wFDNkXOn;a^z}>HqJ4%Jsk0qrcHx|K0S!nB74Cv-9bH zg&zOXo$Nn8WEbhL^pG;nSz{GK17u}#SL{E^R>zFQ=+4nbcaHN;&2DO5TAiy~o$G$| zG5RYthSGx*N6K-#*b^>j# zsbs>^_H{AS$C0*(CHFLMp1)aT+IPwXp>2VhE+}Q*d1wA4p{ujGy?Ynppibv{)wnVo zd5wQXXEx&r=A73tLfzTK`kTa0G3|TAA2HjNw}T|h?Mb-fENIGIQ=>Q@qq;@KD#P}j z;t7R0$uhgWpnW`ve@q>1BV>Ilr-GSQUykJ>PkuhJ*Qw>*_bdDm-D_HpNRJ%7&3?Dx z!_}{1&OVa!h~-r83XG=ii2HnmCp>30fUPcdrsH(_<0lo18&=9SJ^aahpq#P=0#54O z&QR+cB~8AM(j^y?U;9{)BYV}^hv)4UEs^0=Twjss3N6tU&bI+B1Rm>}YTt@3G_AV@ zQMW&vn7j#dosm38wls-XQd3U|RlQ<5lbI|#_mIyk^SKvg2NB4z#f7*|SWXw_qcFz> zhV65iEI0C85UXK#;G;}>nDeqb@E_`zL?wUEFZOI{26HBmh-_LfdrxysdDr1Rja2V8 zJj6T=xBU1;zhz!I=Wi;O>D3rO4c7#mjK7}2-g%B`Mcx28%XxOcCM&n8A?MLh);IYs zjpLgP_#(MgdgQ(RchNquYE-OzJtKJ)x-mTCB51`f23A3DV77}AKaU#U(bO3H#r9I^u@!@MkskEF{x^C3pRfDx zx0C%pvfr-vza+nZ?Jv#g`^Eh$e)z?{yWamH`TcwJKje%1SM+~!pIz_&2fxar|71=7 z@-OaR;s4Fu(En%mZQuXS`s+U*u#S!Ioenm4>MKroyRzr zvvw>osf)_NPlt(b#Oq)tEa3}N7EL+V23Vsz<@$JG3U9{U7T7;)mR0|Rv67b#ly-+C zA%_3q>52-M^vTVacclMWrufSGU-?lw=Bgv3`m!(`SPEPJ6el6X8NCwr30q_V5o!us zTQVUj{ITen?m;+8UVI@y0Gor_qxD?GZ7g{Cu!4dQjVlVQmj!-Oqm5yKL8B-!Sz8Pb z8Wl?QKY(zebYct(4{9KHke`rQVy%+NFVWceI|p%)&&*%tTl0_UF@WFH5Y`TxI%>Xw zq$g{Q*}ASIWdtL%j)Ri<9dit{QU~&ETSR2k~|UBZAdPX_C~mZNRj&~+wFpK zADN01llgHE;oAx$c|J2k%^w6IIn4Ua29C`DJ!t?ZP$G@aNw`?-=LN>D%h}`iPaS{47T2OVZ|yTxzUjf=}(uU+R!PCa zyTt`}pJ015{QUc$2p@RT?M1!z&GGa0aRB8%joDTEMbC%#&~yJUwx3F_SMRm!dPR;i z&tmTR4+rjK|KHgS{eSkOy#7yDnq;T__nUS@|5xVs@A)r#eKG!O{5SR6Rr+g|+XYt6 z>7996AI#Hw=eA0i`#n;4)uR}*n3TLii4Mx?p4HU#DBNH;@kltq|FE7yUDHR_9D0=J z{P`;}XIJ~sUv2){G~-e)450uS19ToQ${f0?$Mv3E|KZxSk?6-aU)g_B^Z6OL04fy1 z&RYapB`^GKT^14!N*|a$s_XhgzJANW6RKrCe=T2rWq8yYx@>Y+3w1c0O0(;A^z9ZC zO@TD|XZmx0#Xs3~R?p5YBBJP~*XGu2_mjfQx)pwY7=>qeg_(t7hr-=rg2gDFt%cxh_k-7RtS^Y)_|<#CfMO|-h3qnZ zwwIf|UNS!DU_o$}D#Y?;%y7p3zIS`h{svMFJ}xYtMh4$kvclT%dd4_10}Fg`y0S-k z>0*B)wRyIe^ZM1|`VNyAyva}c_jyWcr)EuE1JW`$+2x5T|Lw|^ZypNS8oCv(lEUE> z#*^ZF`jl^{c^hT5@(z^MBRE$?|E?)?eo5u{A5}e%mwMG-IeLU@(O**|%YQ_|@%+om4X;l1%~m$mB13}||6 zHc#Tq=)ruHM+kTe)^w%Afl_$2SD2wgp%k9owGc##7b!$abpDO=YHotq>wKy!9-_~8 z9gp+r*M0QG{I!`)Qglf-fbF{*77(!biaI?rO?TDyVil=TPC~Ep(M7M5 z&&PLpzQUJVml6xYcA;69x!yxaV-&!U zmx0Zxv*^oW1)Nc=B z8ykBb*eZA)smrW$#XD+A6qO%au3;M=W<(v$%7I?}`T&!P~*< zL%n&oq+Le#lRO1UrIh6bb+rilE`rt;#g%%~TaF=P3p7J+{rFI>hZaLu5c)974CGx1N^D)fl#W>4+^HCa-eQT$_djh(tine>hS zDtc~k9{fJW`F1L1UbK2I?v)pFq=ntSzu{6ntH0D8yhoak>gJ>FBW#P$q^oSaN%)8T zC5u4oK3<4l8UM7ek(>p=WgN@wQEXgDUc{s9@F4$2er+AY{$75-tIRSAI6Nlt8nRYj zZ6r^m_@QEW9CF?q$B(*f5pA2Q?F7a0Vo1I8n~6kEr;K%`k{#KXoIj{y!kamA0QSuI zgkR2X?#eeGC0^`K<~gi$*z&gEoJeVXvyptSLtO?a8ZUCFyOyU+ewgF|s2=Q@uh`T2yrCj#%VUfk2aQ_v_avVmT zF(;1Sw<>*)GEhUAeyAhMlS9-mNqrm0udh6+T{s5*7rU9=dh-T$@hi8S$1mBi(TD8c zVsD7MH`0+mVVy%q?hCTHOOH8+`F_^fxifs9x8cS;m~W8RvRD>s|I#^HRrSx=&>k1f zB4d=Wii50$?#l%sdklr$F@L*hQ_=T!mm7NA!Ra2|(Ca0QPc0)8MpSNS-B-d0IFT2FPC(ePi6fFeDm?#*yOp5Q&+u4d_FB-@fZ%bv9FAwWTEx#Zhw zzv>}^oBVe$j>FoZWNExz683$f&R)JtrXksL!xI*dew98luuzWCxMKQ7Gc|r)2xLgx z-O@gOqclYM)O9pOHm@V`0b|&KbWVz|m%^*U1oUnUo4%1=X8z2rmTqK=q1GcRrKN`j z5|!eT$!~FPW`Rz{tOBeUH+a?MJn7_WcF^yBN2S8cOVP%l!o8dDwH(rK-V)oIyK-jC zsgn*mNcI3C$56asxOgh zqf#pqWIUPGIkDEfWDQPDb{|*l*iMdC{#$ul|xvPN@DA^`H$d&UfhR8u*-GADL z6-8&E=Wjfu>#jE#N=#YGr^~?Na3Y92A{H-{y5;N3oSr zxvQbBo-cyM58%!E9xY=4YuGLF`M04m7!0_z`!i}`Yw&6`Ntx}UF)48h{j-C7?~aUq zwgpD4c3~%T^|AN#q$~QS%*}O@( z*_rnkVuWs(*tCT8eLr`8zjvgZfY9?AGY!goHFeWn5o<~;^+m`L^q{ib z>!2*v2OK2Cx`AT5zjZWm9t%up!w-#AB2^*i8lab#6;-m_P_4?qKa*m9hMnWf@uxyzAsxp z-El}m=iL4^dUN?rIT_`JD-=0N-S#gIBg=vm|V@`vl$&5v05Z9$2xQ+C~QZT zWuGrYai+G}h!0N~<3-+tcv(GY@qSloE)!1u~Lr6vznSIIsK9%-w0|asmYySh#r|4EKisBFEv{!hw($A0)K}7hVT8S(>E9&AH#r`T2~T1_o4O_KQ6U*%`YYHC#F9s=9XsxKFsby7{8JVGsr)Og zrACy0EVcSGY=gwfed*gErH{gz-JV$olKLX0M^EiTv16A!T7Bzds&_XfXO58fFK3d| zcsD=qH$P`c`^uk57nf4=)+|i?VHhCoQ6bvnq`Lyd<=7}0_dc0_vnh#x#qxjh1)^rA z;}=4ZI*%m$ksQOg9qZ{uwLPEgGS>x_G9VK@?X;B)d33T}7xQHTMkeLSxl8AS4xZs$ zq4vdT%rOopfnMkO?}>KQBOz&~gNI!HQHCC2mu2dJY^gsdyBl66$iYOO(W#sl{G_NN-pXZ#A#U z{XF%^8B#lXeuRZ1`pV2CF_k?dNvEU%OF`kuiPG6nQVg4v92PgTVKzmt zHXgO;HhG0O8N8BhP~%xD6IybAMLEJ$XUo8xQACE2vSyQsN7+`fMG7r^#4X<}zTgi3 z!SkdwG~fSK+1;{lvJ?{~f4@ulRBKH_hf0hCtU`w_GS2?yn@s<7wF*Fe`v?k?rONYn?!8JS$b4B8uBc8G9C_)he_z@5=a@33ph;^iLa&-%K8{ad_| zholmD7%(x%=~KPs*wObT5#_-&$lUw4$x1-UqKLj>B7DZx<<{PMvVny+vVdg|HSF5R zWQLsOo7z`I4Ne;8MlK$foVN(Haa5K$LwSQD)8qw=Pv1;Ylt5^!ZdE+Blj1N>w3cO_ zijSW#LSyo=f3qGDCzBDeAYm=1U(`ev!owQYiBjLZ(Tg`V(Jg#Z>{U;+Mr~jhP-JBqy0OE)YkZ3l*vg@@UISaluZYi7 z>m{wRp#oiI%i?bu*fQQ|II~{DXcUGQy>$GR(9-8tJ2XqRoR#c9dr@GqYH^J$6{8YpFGQWj_b4@Ac|tlj zxNfxCj~i(}gh3~4KeiBOT+S4OUT^fWF^HYXLHCm`c;bmpld*^N{bx}X_*9_#nSZm{Ox9s zM%K|@Amwi8U8UKqWefxQGHMxvwTAbU>Z+odL7HExeq@p=*#$afA3?uiJ@1?uVMp{M zp@U_MZZfrUs)clR-zTze3?f*DqS`LXnmdSnPd2|!TZg5TioY^#;KAydut&XH&f*9X zCC%hyxoMm|%fWJ8J##2XagR?XK2;x1xj)eVy-(*quJ-z+T=Ymma6&WZK_HT153$*+ zD%PZ?Vg)qEK+a{yss)_wQBlNa{7tm2k&yHr`qY1_VnT+A)d|+EoIi`Z^Sf=hS9tw- z71NC~RxL%7G(DW%pWsmstIC>YfOB-}#GHDoRu;!*QroZ>jQYJ2@iSwpiB8Ra>=O9b ze^Ew#W6i2j-&PJkb7e$oTTS^v|126B=fH+W=^M)sHLHPPd0XHI<-#T*e zt7&;kD}7!$4ymYeb}u6yIq$&FUG;av@5T<+9~BdPFR53TJ>!Xv$cYZum>-5CEPNKV zc@QUj1=sy%(GBO!k$g9>=F1<;)BTU>w@wTv01|ZhVdpNjUi< zI$}CU+&#r2P>rd`Y(2DVuU@iKzPB5!mu%I;FX^GT4(fEbhy;2H zdd(f3dX#xF&Nk=86J1s`%pK%kJ*%UHYG?QP;Eb!>`7=p2{KyV+L`3R{`0(Sqc#FON z4J_t}axt<;on!kyJ))ekXvK41&PN7kVc(rMsJJRVl5+7z6$5eSMg77#9~JO`G7cs9 zyz5;#MZN5q(6*}B+4Ep$M6uHo?!ze-AZ?FhtI1=EE2l^n82n7so@^rXL#h9f9uW}E zEhep6;Xq$cEtOOfBaUr7Vo#B`p1IuVH7?+Wmk!elk4ut|)j;i zdEGfpO*MzBH&VPT4w|+}mob}}Ynk%a?@_Cdq_}j^tw4s%NyRuDI-s(bw1?;aXspLr7HaNjnXK~VNd4yX2IJXUpF@J zclO*zy^MdzKiXfBn&Et6@f@6H(&@Ca{bre%q0ff`s^OP$TVk z^l_<2vAb3fp4Cl}9wmP3wYXH1=GD|zNn$9|+;@;7uN5A*^X@)A(#VRpD;k2rE;#{C z2~>O|(WTit1a^Eq5QVlZ)#E#sydjWB()(2fW(CH-5nUP`_YH_I`0*ur_ZjExP4`=#vn#Lni{Ww_TX!1Y}6+&v){dn~l6YCyU47fSQ zjgsYPjVGqzBFy_jy~&un4aQ_uT;l90fF`j#)RL}q7Jaj#-rsB-{JEZE2|j==tWgk; zUsGJBy_U0%q8&RpP=vGRCWGqHo3otjJy}t=I*zVLd91`A=drRXLcxpPPtGsP`O*ln zq?#qW^+Y>7<3-khH_1v?(U@{bXBcqF0 zbzN_4sINX}+m4ei-?e?82QO=l#z2|p%ABIErC#h8)w$dg`|XXQ-S>FH@5T1aLc{gE z>L1jH_9tv;tIHdXS=I+8Z6%{%(q1!A^>gt!(i1xB2z;$Cn|vdy@AYptgHv9d+_TjM z#>q`FWfHr;u|=Kd6%{S?IyiX2gmWbn` zrHDIlSN{XlqLjSDP(Pz<`=wv9J1_P)>v&M}=kR&b*1XWJqgyrzC8R6)k)IyTEpOJe z)c9^R_p2H2k9~{X?n1o}9n)@6Z_ocYYLI*RBSUG-J>GgyZ zTIexK540P z?*ZeOTh?Lr`WJg6*Y3S0I?kz=?_K;&S$^g07F~_Kd$V|Bi@GoLUqbmruF3O8Wo0IF z47cM!5ATp^hVqv>-w6!US^4A5&X+^FJ#qvLqfEnD{`p70E~`1c;cNGDcG^L43NOj) z&li2iyB?l+no7eU^-n*5R6j+G`1W)+K`Lcsoh#{aJm(GP^}mqhx2zIPrW3fQ|1U;A zz?;pAl^F+(Bq#nCQF7>7imevAG#Qh@gi`0xox9>z$?2<#VKan^zuaI@p+kiyba=1Y z*>{6fG;d_o*YKe`CqGR!cj}=x_adtP2~<$GBvxSc@1mXbci-`!=ZWML(YI5ktX{rp z@!#|kLLPK3zStYSz#E-cU?u$Ldq_B7CcLxJ$iAH6=AJF}BU&-k8=g;|Kd07i57on0 z_4lSSHdBxODyWKOPpp()la%c^J-Q^%T(0De29dUkGuyqEWNab6-d>|0bQ zDX=X@ImCZghqg=6m-Z-Wa~_|npy5wqZ(T~j4cd!4H*Wmjzuur{W5gYFEON%aPSUCH zoMQcS>}*9%P{MDK+Lsn7_4n6Hgg&EwzE17VbRRl%VErDF{w@_W_9&rJHL@JSQu;^o zc*8i{jXf6`(5S8DIrNspPbaBzwUPIfK8WOczVArs6HoXV&Lge$-w-}1>Hrr>g^7iK<2od>kDDFqbB@kcj&F#Jukc!+BCRk$d2mnSC_mUuNL;3<6x)YKlJg{ z&K0K<#vOXQlHwZJzJ}_x1E(_uNE>!x#}pT}#7XZ0R#kCT?@_Tm=M25fgLD00Z*;yx4m#}(9)eBJvEC?nD9B+TtcwaBqRoE;q+*P7 z#XEG&JdHWay>iT#lolO>aYe>js1GPDU6qzTWCtgzEqS|)6QNfh$H&Xchwl8E zp@XL{9-*x518?Y#SNbZY*N-kAUsL|>qRTMT;?xkBd6E=8;hr6YBhcb!#m-JMvQHXT z(Qvw=K@mWfv0+?goO~)0LOw>AIo%Q3MNW4im->)x-On$1$K%)+pK&Vm2}jIwKOtX` z+r{~zzg0SWToi#J?`f=ZuK24cQdv=}NX;>L75^jXN%LvYAo;N?Iip#eaT^o~2Y$Tvgq{Q%S=r%#+m{-B<&WmcnVLLYT-&vi+pID$II$C`|*s zB#qhI!c*sp4Kmr4EEKxnD3;RrPeD(f8Hr%|`vi%1p0fWHQ_TeXsu75_?ZPpn_#-$3$=_A<2VYN?xZDN$Y4--o0*h~LZ1mf?>2-*t89t*e8F;Mj7R z@PcEK0qD`u-L|bSo*~aE{qA-CDk3$LMwM7g%~(?k9?Luac~Eco zXXEiE1#1X{6p}&LiS&W!-B;QuC}DB+n3|F zim0V2;FsglPiF+TmNeyKOL`K!_QqRKmIgn!<(5PpTHdzY^NriN#_eq5cDivJX54a( zTVj<8@Ue0G(73&4+}<^AZyL8(joXXH?T^Oo8M!6xN%swpmBOB~USkehj&EfQQw=yC7`5qHD|dd_Fv?<)3GE{Eq9@YR3#^Af@KlxaY~M-q_-wIcSwyf)*1F3Wen{0AwExZb`Hb0AHzDt zZZUIDXEFxEHtN5?7?A4LpR*cFdZ;XiO|t^}umU(O`f;gUoY;x_Lvf$65qLX0Vz5Xv%cmq4w+C;@d##X8n z@ljPIBmQryGdg>6%0sCrS%N$8eG8{Kcl4~ zU4E5>+9>_tifm_3A#WVd6MVdr2kg_wC1=j=2L2o7Sd}WEB-=&Yr{4kVmp>qVtDQhIIXVmJpRX-Pou{ z!iVVBnBnaa=ErYS9SP_HmqK5u@)~~}*z$j+(oY;U6lO#>cGriJfVZfSbC!nd(HYEo zZHtZJ#2>GmMx@ID)m25sL@yON9!SPhk>l){M;F+=u@K(GU$oMte0Ik@IGFjM+cAEF z*sue#SSfNT<A1GKv>uP6dve|y$jV<43oYiq%kJWxxK}_3$=H! z%1R9>LnxN_S|W;n*P=%?(TX3(R07YCO;TUB^+@rGgjE$eJdQuf#uJE{v-@#)&sb&L z!+mw7bNw5Tz*@(7P@s}WtA|B*#xw{}Y_d{6U8Np`5YZ8$GlJuFXU|^A>u>vdWj4pj z$H27=Lca!vpVdHmWc6CPMc2t&Ur(iwC4lj*5gjg6&2xImOMTUO3NK-jARzu%DkpW0 zfgaV1@xmxIZVXecQyMEA4T_hjUYc}s;ikZFR3#qO$0yxN^=L;2quy(cK$S^{N*os8 zu+DJMlMb~wEWx4IaHusL`l_W0LA5bd9@S?-S6_9pfPK~DjhobZ-ews$kzxV_=JxeS zt`w*^XgrCQHJ(JB8c$CrpI%8m#mpy?-o+z1pR4CeV7+9A(9m-*=|L@qla6e3FIb=Y zh(aQU?LtfekK1~b5{iEnv(T!a8;9c%f1P$u+28JsdGh`T>pgfV&)IW6lEYW&nsK7L zbN!4g>SR{D98&}NkIz<~y9q~Xq1}a^@W-C;-u=sEo}kh{B>2P|;u09<2`w3q-uaN1 zvR-fWti3%9#9e6AX9h6+`R-sv7RkNL1dtvcE5jDb;#51*x`P`X!WSfQ#3oN<9Al5a z!XL1v;K1XIA~OZY2j|Z_@7`3|{YU>3die+b$EXm)vqB%|brtR3L}mXkqF`OuPg&Jd zxpKn(OyZat487O zSETj$umr>|rvcu7hmql-?%-oajt6@}OUG;e>rf1ZTY?h@kzdALgCeJF40N63vZ^T0 zy+3FK%yVxGs(@l>aJ+ow`1nq$RphGhgor=?Fa#cszpLt5<@jVEQrlppRuG(cUpFZ@ zd;UYBj%c2*+#yMMvV%Lg#TP;9>a_K=ckC`!^P64{b(5ixvK)#p{D~*L-xGfMaSIFKs=g>2QJ`R6_?tdtZ|LtGAuQ(A-;3Xx$Qd5T=I|bG_|^UqLZ>$} z!@=3fp)ErUc?%`3c7C@DpV4-Q-iRu$JM5C(k5_z!v}Mla2HZmL;r1RXk%^x>whFh> zw7(h8-)j|t#m5rpOcLnqkwvX~H23JYg~$SkJXoWMR20VBaW(l=6`j7r0KX2{ygGEm z;k@%%JV$Rm3OS*Fd|q$tgD7vLZmWw1Z?_B6W`uLaLwG^A?`Jsa*IvidNxn-~R;l3d zuTO;v9y;H-Vi1`L{bLY;&vmZoNCke_>)4eXi@MY(tz(9w7@^q=XXB4bL*RUJGV_MB zv$NwHd6o1=5}mLvIwMf7&zRZuZ+8)cYEN;?J3*rDcKzFQC|Nn?zbrSSL4f#yJ3lnJ`sD5 zK0#W4Kt=`sCK{xf{(`Z(I-EW_!T4xm$zRh|h#HhOhtL{d3LQm-EWbun$bMAFfpise zMM{Otz1>nFU&pVdLgLTSyg{%UCQ>V zd)^oywc&$j$Y$X~cn=wMuDFr_p|>iX-D7#s`>gRhJjGq+tudfM0-dJsFbG_K^ivh-&GbuNnKH;vt)KrEy(xR8ZJLs$K6@dR5aIF(pp5ohl_#= z*;r(R=`LEU-dCyPj+`ZT74@q3HAP|ru&7sFiSQTZ{Z|6>{^&05y7RZYu5y;#S0pda zk_U_Ss{6x5Vxe{o4Q?z-0BD>9%f1h@(CP1@7~K1NlIBZ~vP9J010D=x@f-qFQyO{q}bky^HrsKgo$%%8JB_-s1J6<-vcA%ry;_WK zZ}`Rj>1I28Fw5EV?~tS1rLO9c4Pu#_?W%MA+02RB=@nUo=F%NU%btn7!Jw8Ne##Sm zXf^5U?2whm0v@cL3vLiA$Y11I9r{)tTN23t6(7~m*mkS-G9?dsAc6SwWmLy15rc=+uO3FEuFT6tyHyCmRv%j`fL~am9 z+8hZN$%->gmk3Ur3Xb`%`nb*>x42=x7bXQaZ(FxfKg4{;IH)R`6@P=qE&c_(TJqqY z$>+9Tgdei7VV}%qGsE}-1B~}j0y4MH#s~~oV7RgoS9j3{PzEjTApmpl#;+2fm&cg4 zTP-2#>75KIp@|D-cDb{Mzc8@H{-?Fr+y!MOb&WD4-Lx%1{X=T46-bHDhb~Y&oD;zNTBJnBB$oCX~jJVS0qun>q*`f)N zku~O=)p8eEBlIC6u@cM^{%ikana@WU)EG1i4P4=W&H&Ic!dLYFEne#u$*xSB@3+=It1q4p&s5z=Sph9_p+~0j ziNa#4Mjc8i%J*%0IH#z8uEd~6a*Arg@7IK5&xc`gp zCuvngM@kl|!UqTP!D!RM#n!qewP-}l%?Xv1eB*wiKCpj9QbE_VapV_<;&y+$^5Mr>huEy%rvEK)ot$ znyL?^Cz6>;&#(~0WDo6fga;kt9)2{_|Jsc38wR|yc0v#Bq2c{oOwQk;2z6(*Z0{D@ zS}g0OA}iJn@E=(r!&^yuP?roRDUCV9!+K4sFVZ9{!aZzw#AbVY@$EGyhgccKYU@=+ zs;&tc@t{8FJ-+vOFiKqRza%#&}Zs<18?;4 zxze`^AJwCQ0{2XB7`x>m1S{dLljY=e{ANtAg>?(Eg^-zfu};id48I-;#y#3$-0uz= z_j^|x_xslx_lMSTcb4=>On85!*MJ}2q2TCT1Mb;nz|p-5zPkh1S#odCaJ^eI?)R$o zz$N#uHeT;{DPIq*HC`VoRj-e%GhQF5RIiWss$0(vb&Kv&x4ZYM+r1k3be7yN>z17* z54qIsky38`SDE$(Y6SbUdbOyKFL@%Ls|oMo_+O=Ulj?Jj-!1B69J*3gGK<}BKK{o$ zvMe<>@R%N%^NKg(+vSZcjCmu0z23;J2cMx#G&SzuziTzDQ3XM>BGh>LrW8=bNa){% z*TO^2LlNTnz{49p;4FE(mKO=tVRB9wEBYAhZecF>dxfj;el1@0k}U+9YX%A$9_|-* zJbOCDC>2_IS{AU8_>KyVpQho2W~@-n&EH?%$^Pw9uG{68Y(NmuM6RRd(*C=3?sO3D2})c_&)w_t5U zX^(iPn1yJ`5%;?#OWg03Jk^9T#Hd0$4D5b0gAYm0kP1(Dc2OaIAE`9lAFt(35s7y_ zsy-#bwP&tEqq_`G(GK-`_g>@m?x1?TSEGHFWLK-#`wNWM``4=1hg`<%L+jM*Bc;ac zBfaYN@k(B$R;pWcuDabVi>;j{bArwi-)d*c!nMwlz`EExwX}LL3$1NsSV{VUSOtxw z9(iN|F;G4dUo0#G4F7ETNSSz}kL-YIlvDH}RaizD?J*QnvVdf}Tj8BiK>K;LEg+N2 zMiCAu9#_;>kmP%l5>=3;r zRZGqoFGe}vqwI;Tr=>DKi7`uoPo!d+vb3ovb)TZ5#J-8V%jb}W{c5!rrm306`q1qK zTHp(@Us7r9B}7^cv*c81%7~{b(Ab4C`5Jv(3KubYtOAAiAs_s!TA*^oe5aMmvMZ9g z-0@dQYJjYrE2)24SxdL4u`%i19u+k|+)(VyK+s~MFoL3pjr@DUY|O(%hvY+vW39+# z-3&c??_8yg-Y|^}$#YC+rnJAvu%pNh8^yR2FVmnDyF-U_{l#Qi_Kx`}OfVnk|ApAX zTcH7?JlFrMdn1jmn2^%0SE;4q*2hY+UVj6siPfmn@)l>E(+i;k?P*1;duDa`Dd`;S zoH-*pdkB^ZW|fR7Rh4g9wlMx5L}1WY6Rpe1<8b?t-td2819+?f6HM-c#10v$E?`1@ zWKZvsnKhC8@ar-RtwvPBjEXUzKa$-tE=}y{>0Me)D=B^R@R`xs7jrhuAb0uex0QzD zdgGSpO%9ebF`GXwhe)c*-&?ja{CfYxLU~POXifMf`N+eNH!<_lEM_(Tlo8~Mjq zj{n_wQZqY^~d`s100b zE{0a}9g-E&Z(Mpb(H~duc+czqWAkc>|7qYfdOmyn-S+V>OpD)s{d_Gx=ocTD%mBsz zTR)F-IOX;8#^~}#sI=tJ0OGp4!1-vqt9jFV*_F=q@6lj9rL@+Yp6JXxnGM^KnOu*?yT*3cY)KYz};4YDJ=WmDM;>S*iIDY_$*a%|)CYp{X!I z^2LIeJog3}^>+502g+{fJe|!GX=TN8mB)elS{jz4M)QiYmXREqBl&`W5>umlX~mY0 z)g2MGvF?X2xZLq`X$s8xI~f~z2E9w^E2F*HAh_4>#ERBSw(&KW^V_dLlnf5f&d!Ns z%lf=G0rbYnIrw|my?=@;$3KRG&(TXZd&@WbIkMwrJv^c)b`j-{FgVvA3_lf%ifzX@ z2j* zNVB@^9p|Gr6gJDcNMoP8;V%m9e80$+8)bC8o~tfTlwE zdm>^7F+Tm_yP@9kd_m@~h!lhlIRdYHIa5cEoLS>|nsVrT(;KNRu3`SX{P`N^q`!0R z<)Ut>K!ImXxvP-_%f=XVj#=c^Be@^UbI4ZV{x35(4vYC@vQRdE#QYfbGERE)sktj$Bvl?EVpP>*2Fj2@pN2M^SXz<#LODCrC18zVyCHGUfr( zCy4+PWI7DnT&kBp7kHZ{Vk99b2fZ(Mc_X2ekJApYRIzVzv3j$As;~%uKcwBBocfh* znXI5?{Q#JpySKhNt0KYN*R9!CI=g>B#7KG#MYruAWkpRf9n^G!qn{<_q5f~ySeiT%oof+d7iDw^0~fuNb-n~+gw_r1ix zT5Pi84((zw>V;Ch8)Y_2FW=!@aU0Hh=pPOavv{BBv?2pvROTt)>kR#tOnRa@rTTL* z+&6g|D{$YVm%qHY&C6-=dSlh5JTt%_Ya0DUIUdJ9)OzITZ7RQ4)0;rgxlwfE=miOh zWSDP|!oBD&7Dx`ti(L!BHI2iIB+o97BhKO(y*#@s#~bdV?p#|Ke~mm-Qt@v>c#SM3 z^+Z~W3Tw)DI=g>?lN?GQb#O+#gCWVueDFt|j7M>a*Q099ewGeJG^H~2AX=$VXVtWn z$h~kI1t>g)y4X>fIin5N!*81Wd{E{FQR;jLtOB$e0gNL2qw+q3wiuB$_`gj2%fw}7 zt8N;&v~!eZN|ui z=kGEbx_&)+Qx0|PuwL?%Uj7IF9ZX!sbc&Sjm$AdsH{dND2NvD3{~JVL8DSJvyTY5B z?|AFr;O09H$q3bD6fO$|cQ%h2e31P~hU{o|hpS!jF(Ry%#t{i&9pJ$JjB>?SqTlFe z3O|$W|2~?2F#)8=Rr{aT^q&bWku!zee^pg^o0@4xvidJq`jv@h&Hs5%gepOUe!6_| z%{~UG5*_K7y`vzF8_w>ZN)*Rw`VXa={xJwgvg!YKwPKo6NyS=xnWldgX5kUD=~sQr zFrV^|7Bac-lhwki;W56BIBwhCLsQOXQg9@l!k%vkCkOIQNuo7hWUQZ{y>_mrVF`~b z+P@jw@HX-ldW%kG4ROYKq3Gtgnh$J|GHXC9dmce}Box!)< zo2aca*;M{==f$3EI=}Li&mv!kUjF{#mu0_>ccjUX@3+%!3gm55|8KB=t0MDV?oGRg zRfXS%?P9iYRMw-U9C|BDkF*y$yDyU35(zM~u|IHb{Cd^YRSJTvJcWANqSWBSxuQe& z#8!zm2^~zZ3sf}sOIMJCzG~M&CRGvg!gwK}je1oRS!x^+ej|)yRghS|l~O6k*I-Vf z>I-PJJTgy=LQ{hirhSxMg+Cgt6%UajUu8Jt9FEXg!hX?k*F|&2d7g`16KUkOpt~V-)EdOq|9BE8&1P zsr+IA777K+Wf-ys&3Oy=WcwWchq3>>WI-6gdSGd#4)CgJyLkOGO`H1;pG!8Mk#!~v zxAja>JkBW+ec)Wd0vxHVGhLp@!a|Xg9@)8v0&#X(o71aM_OJ0-Z^*m!lQeJX+cgQDkNqJ3+=A19z zHL#s(f^t($>olwI@}$2P3kx~n_bCi|ew$;r@G3!*g@N=xz3BVi@Rt7jWWG)K9v%IZ zOiPHe{N)IlDiom+i{bmiGCmi^QMpD&obB;5ESH2`9&R5i%G1ANd*GfGkh>ROT)(QITA{g@#Vj$O)X& z>^>lMgpJSsn-ve#n|CFP&T6u4B-mf$j$0nZ|BKX-Ed?k-Z(jlD zuZ6uI;=Ik#|ArYavZE_zD{+#YKlE@H1iAA(+3@R1`0niaEtaB}T|!i0#bybYDG*(; zDtXS^uZ4#kmRPAXH9`y+8M5Dnj`)Z{OG1V0^R>wvVYR%J$vCOB(bjC5+`q`&YEW2n zZKP^n0sWNSQ2CyNdgJGF^bc#Ii&Tmf#~~QD57o3s1|e_XVQ7Gw@Uq=PqW(!a{+ zf3y}Al)c*jhEK)vIthm-l3QnbbxAH3~8cqe~!Iu#3wDG z$ZD({=@KpQ4%Q?63zU9*4!+FtkP42ui$}c2PD4uLrAl2G`d7Z`F`_wx1vvfRq2kAX zBxa~N9!P7%KFrmwO>YewB^qwX4rOSZNdg|ccF6d*QhH>0D|@`iK1X_V!BKByg0Q_H z-cJqXqYIITcnQxL_M9=|z*waoxp>xiq`hwtF;GO;Q|{A{`P&NhzA383k*#O=RBB=g z;ugE>4B=4jOrG`dxzp5ba&aVg98Xkz>ePE;lx?L6n0RoeL%VTK{APlz7RVUvG4kJ+ zdlXknUhChk_)6^_78y(wxpA3I%CtXadyzAh`)Pcq_k6qpKN8Lwj}fQ9P#H`z;+OHL zCrgPw_JLCr*B%Ll2{R|2MM%z5!rb~0b4%s_isR=0kTfn<{vkMamy(d&nLJDW)lKD} zr(^P8XXKwCcKOFOm4B6~`6^REs8-`?2K{3*=%1wMSIbCclc^W*EZ_ZoB5HXXFBIsC z%c0Jsx{G9De<@I<_7k2(L9@r+Y2V{n9qwrGOLoK&h3LN=3lf8#uXQVn_3{@2eCNn9 zzlH={C~^IixJ)xBZGR2+(J9AO1|I5AYZpuGXUjT?E5GD#^k9z^%Z@q4;y%uUuA*I_ z)p7*h(91swykYc}!v+0!Tjhtnbt8MI?qahXCAk&}_sxKWFF``=qBGOi*IF^e&dg`+ z?X{+Kq}rGGv($mK@%l`85mAW!LRvME-Ed$0+0!VsRCufZBa876olo#TK9YMC)WG+i z<-&K_zHs7wvJG>XQ;3qi0Uiux&qE>pRKMIM4*;?{Rg3+K8bUNH+j$cmtlGMBMG^6J z4~9Jtq6-auMd9!Ynd51b(u`3{k6#uZC*`nT)!D#w*tH9mAQeC088!yaihq|xllpL> zs1Vajs2ZiIr51i^3W_cOV8_sV3x!rr`n)KT3u()pK;<_f`zVTH{;Co zu`p)nMeT`w?OkC7W6d`(EGUD`vB7H681qbW5HA-&w#u7Nh(7+RLyTHO9-;Sr-l5mo zeUFiNu|b?3&V3n|*xyyZEX~W=H6x! z0}`0<|1>V63reo&O~uzqu}3F*B-X7Id;ARL8YEfG(>OK96TMxTy@jkvkREva3n_PH z(K(C=@+5+gL?8*G^Nw$^EPy4!gzGodc^N^Uss^juZ=1l*EFhh>@k-%s#T}`z%s%y+8 zii9zvx(uo>>1b)vI<=vTTl@=LL3Ee+m-w|Y9i8osW4e5;&0`uDHnq02`3x@;#*FC zj&%^?GU1w*ww9{E!VXVci{Hq?4V^81p9K@$39S3v~>jhrKOi^GrL;a=DX&Z!gq(@0!Y1tJ6UdNi$PDPq2F2%6x45?0sW_FQ{v6mX|;xNP4iLcVK%M55rCt}@1 z=FQk^8vKn5Y8q}Lo28}YhMzRd2;Eg$IyTM8h_<1#aY1QmnSs|fboyI-DSy*^{>AN` zMy!`>GwQ0{H8ZD9uJ=raLsqwRAtFrDQ~0 zulH0{UpLUjBq)_ojjyY#VZJR5Guj((^7+a1F@X$8C>>|yBE_#6{s#P$yHSBltiK7P zTGA??5_D~h=I;#nwC0A^E}u&)9(7Bg#8u_%YJ|rVT%)>1yNXA3m5k#oyl+;x+6&D*{hE!E_@a8%cYu8Xfy_n}7JkbnwqX)|F} zTE}>v*|tzh7F?oV-0&5xBhcw{Eo$lX2O3&k3w{0t?M<#ml#j1zsD}J4Y-vLjO~lpd zYYcRDwJh?rE*+{}I8<}F7B6V<7ne+Mxv2C_6DG`~@)EecQ7dj}Yj0b+uszV_YD3<- zIuPU%JgGdkQZG9jS|yCu5b(EaO}@rf|I!X$vAmUNH#W>`nV?OWpna>!*W7>-bTxF& zKVKWwq}>X5jk~jxDs|%=6?THQ$loxp)#qw%?{sO`*VP#hqO&Q4Mt@6tn=9SN#Vu_Y zYm!z=TQjr35-9m5lx9H9Z3aMYF1Fa@Ul3@!$>nc%X~!as?TaYKhBiOU@&`KGTq)1r zY*^Ibn%vsZ)n!CEKhV(GF^heVUP&=1Sv0eOE&>4sGqe zC5^rgJo^`PwlCH$xImMN+Ui^4(pnZa%%^g|ZUprr%~fB&$kzx*;6kxhEZO2J(dwsq zubbrd)*JOh)eXhB)RS3px>_3Rjq0JzYe;)h&ZdKUZs7%!!t`amDW~=UPS%?d%6c;* zZ?Rq*rfC;U)EZ`J{A5SNbBg(zQRkFhFn~Hm3DJL zyISk4(MC8o8yT{;w!t{e7JNh56M=f4b9Wx z@09k2M#-qdv;)Sgv{5N2)fwTb?9$1Wkv^%R%a{J{q1t=AEoGNiR}Tm>;H5kn+*39- z`CK!`-)w-`7;{_s%O#i?HmnRG@}7TI5d|&1Xr!>|c|dC z#qIIVan)f{Dy`kn*oYD4QXHUhGp$-NbmsYJ-F$`%0qeyUcXb8u?`!fkT_PVcsb5no z?F|ia1L~S6JC};RLwkTsH_@pv%ruY0+}6-qkEJiheG`2Fm8Pc(`($!M;{pj~P@y3D zPfe~Hg?^y);czz3Ui|PC$|UCr4mmQ?V9u& zO|9-S2EvTkD95H2`jA-3OO;F>pWdZi^^!+Mr~=ZpGZnw7@m1$lRNZmIoBXjj<8l>G zk%~amA#GBMZ6$_&JJom$-&VS2JD1|EDjbv8D_wy$)h$)Usd$?#W4tIHz!kCrCsY*Z ztyUjaN8JKZcwQ;m8s9=#b&Ba>p|`%J^s(op46hlv^`R zywN76h8(4X=5L@%A(j%pvAt8I$V>(%E~Q^-#@gP>P_7a#=vAXc)2nN)tE-;DD3W_> zb^2(LHP&Q}M9l!G<2x2RKVyY`MttttT01Wc5*qF^CQq-fo;E|j&Ne2-u1tS5GiP}0 z#=d0mOh&hqyFt7SMfft~(d+7J?L3f>iU)eVo*8v^UL+3rDSvuhjd#XOV;t_75xM%( z(#uSLweIPR$+^8Vs_W`J)27<;!i+eL|FvHCfI&Qy`6?Uc2$_gI`oR=Lk>lo;HgmWu zIj*JXHB?O6z*Ivs5VAoBNra@KZGif^PzB=OG2vvStsTiDfQ*VP^0|^d1F7pWq{1M9 z&&2=*GAn7c&-$v^`Z3BFMoOM>MnH6=LR#r;9OO`PFx2UY_3Z7-OtX{@&=yEo3E)v|35pApI&;{%;#!}>zq@&nhFz%<6YxM8g6y0 z9Vp}ye}lOH*XQlSIL!ylLB zUNJF|_%d<4$%^{zlM{*AxNRprx%*3vD|Q8RaJ7z4B-TLFaPl#zJdxPPdpGx=j5e?x zR~mP5f6%<1s2~j2Mm>>udn)N(Mf{}ch5qe4i{CZmam_gB;2L>pB5~JPgT6uLU1(i4 zCy_XpD~GFpb|UdA_xo;6Bo4R#>#va=#LZPi{OQjR^ZpK3cPIJb8sSeQb_T$>nid^P zM^Tr7)#3g>LFcK)`Ho53PUx-U8o4BOd6&ZLj7!{igISm5-_BpU?WXLvH2ZPf@hspI z*pTV==j8*#r(4F!7_bAA{SirjhQI966HH$Wv`@<}W44C@C#D$!ZFGy8sx#clj10ds zos^mnsn%Nm9Xb47BC(bGPA++0vG2%$YaH(Tz~3?5-@OZ2|4}0GC$4L`zRC6G4-$!A za|O7nx&C}lBC!(u5w0aXySYYi{o#j+#JcaJXSja(ABjXgS0UG3cPA31TyseKf8r9F z_i=rj%XL4ret1M(g+Di*FM1%6_&nE8u0y||UUI$0C4S{9_}}p8KNE>7!HT(talK90 z-*BzrS>ltpUcqnXC3w19`m^PC<#ED_`%>JV=Kg>4T0;8&H?Ot#B@+KPuX@T}%J>Q7 z!zydJuQ%W2`Tt%2vpl$gde+8uHrIR9iGy5H4?o&-L|qekz7&IOGX<`p^ z@8kM2myX{Xxh~@R7P9rf2=lMFmLDgsAAxtIxn1?sL}C##Rg7Ml#kGUyS}u*Na3eaB ztAi`ZCGXRrFa6pDww7zP>7L$pVU1}gynb|+W7uyTeDu@n$a>e|koDK{OZ@0GN6xTZ zV;@s_H{u!F3AD?RyDCq6DvQUzWtDIeZ@s?%i49lJiC0xDRdP{vAi|w@(2bawC2h<$jXb!%}|J<2lQbJ5AQ;XK4=kqL1GzLy_Dco$DAr z_S{abA=i<8)7k0H+6LvG>(FjD{7d|KCApV6F8P9%!qVMOcMR8{>Jt21&B(iiLmmcP zKqHLkG{-H|HOG&`D-KFFOvJ~X&0e{OcO zV`%<(Aa{N-UhqngN%?t`hBf4muQ~=MO8H6p?{MAok0S~76{&#yB>lr&tI_%I3XqY0 zwWLq1%Li3U`cv~OvsRG2eoQ*m!)Du(Yn$HBpws`rYFk*`l30C59E6Jps7YVupz!( zDY%D`sXQV2zyB!FjOLn6yxIpx66FF+$uC%*Rh3^D%AT6Fd_jcuT@#FYk4+o3*}@@8Jb^Mr36AB z_O;}*XBbHuummz>cOUU=AfE3FpyI*Pa^V;8bP^WY5G?nw8P+3T9^rK~Uqrj;J3VCb z`OYj)eql7*lkZxUqvsbd7kLd0nvy>^>#IYRoIqJpOsfojk0c&H@vQs^TS(#|t>sx+ z*O1j3@gaP>3-`hUM-q3K>8ujIEf>CB2XRyL3q1LGUL!iA{PDl!;E}{N!f*0pmOrU4 z&zUUpB_zF^QU28_dUA50=g^S^`^jtCSB0Lh4c(TXH^ri-2=~}0M-u-g?#M?5J=-8? zyG4=EUx)u$M^3HWrG9qfzmR=zBzo&t#KRi!0&v-!Sojpmx+Z^@BkO)8Ii^SPw-tX6 zoW!4`vm1Yvd}FUZhD!@CR@5x*Xkxu!sdWA`Jsl~x(fHeozu$yp>niK>H&o8a@2#BA zy^8m&wxRhhL?@j&$KMcOs1|NX}zXu%W}!?JEozUafGi7<8LwZnujBZ+YbUj^ST@&{kC%L$t| zSt$)aZGi(aZFfhn*9SjGPu59@G$ zPux}fw7XV&v2o~@RDAldlvvP);a&1Ka)_qQ9l<$fJX=4($APZ|&#WI_@Su(VCh!Mr z{4WDv3m&v0=PvQz1)g`-(M0-0zVP)y@JjIc;-6a5M1J*QG<{Td^dP6_zb~X^9v-5l7Gy@yw92b>+)R-iP3HHQQV7gACLP435PqjNYY*Cs=|F1 z?yJR}<|$;fXH$o{@krg2UmaH~{(ABEU9-M_SIYZNv4f&HUa_}4VrNu|o#D-2=g8ii zmA}DJo!{%2VQX>V#o*Ug;yrlI!1^iri_%ND>%5#qEUrsUUV{XJd%nD&>H zOE=*bu;azqJX=4(*MN6`pDQ5sYZm1t_`~2!z&|gz`4jrKg9pJY1ylN>845kat|Oe3 z({9|?;;!_xa-W*NHZSY?p-Oug3QzJeVmR&bmyag8Bpyqjhr~Xbm8KzE$4J9r!Hp^Cg`5llWRCKJXt4MtWIuNpLoW!cW4j!hJ99 z^yrd5!S4e<2!3f2HTb;|+;stZR&dHQl=bx_!v%ZoB<_bzcO`c!{LBI2N9Lb6{)&^h z&pwI!vXi*4If?tmleoWj68FO=aUaQn!YATy#Yx;}pTvFHN!-_*#C_vQ++RD1`{9$g zk7PfI6XyRU?z2zgzU(CKYfj?6@g(lAoy7gH>3&@KKi$Z`YWGzA5RE@|7~LO&CvjhM68DWKaewV3?uSp}KJtv?=TGTB(>>D;sskSnUogj$Kk?rRUTTAP zgBRQ2YrsYRGUIs|ywC>U4qjk`zXP5Jj`@@P2|b6wH5>mUFla^JW~MtDT=a7$UIiYr z!DoZ-vcdh}uh`(Lz<1c-_kllcgKq@y1<%aaPVfyj_-^oZHh2!qc)$i93BJ|_9|wMq z4ekYBZG$&~ue8CJfd_5yyTF&&;17a#*x*~i7uevhfzP$U_kqu{!H1t^wC9=pECSbU z{8xZi+TeBI<8AO(@KPJR8@$*CUjyy}SG)OOPkGThkgh#!`p>L)+fDzO^7#&UA@QK@ zmECcT8P8$x0xN!T<0tWt_?+QClMkc8#s4|t5o@r{jHk-ve^~Y99n*hieK~CU z&ynfPe%N*lZid^~t&{hAG4YUAGzUTlM}0(aTq_kkDM;2XgUZ1A1nc{ccN zaLoqKp#yX9{A2Sw5`3=>J`OwvevJHs@3O(0z+VB+l*477gO3KEYlByT&$7X1gV%y*=GPCd+xTAv zUJ0IAzV{u+|Hk9^-+3JWyG{R@@{q#-%Xk}lMuL~x;N!rHZE!ER%LZ=(FSNmzffv}| zcY){G;17ap;Fa3*^9+TcatF&n%Bd>42oJ$1+N-)j2L zOt%~S6`T0ifbX!u9|nKg2Hy_eYlFW7z5$%!j^t0;!NcI|Z2XV-67}5%9}T|N2Co9Y z#|EDbzS;)&gRiu~SAhp@@cY1**x(z%J8bZs;0tW<-QaU=@EjNQ*9IR6UTcGo1J`YE zFL?=s`hY_A_Q{b#nzTg~`0@z=mhNjDSU2VQK04?mCkYl9bo z7uw(z-~~2#9eAD%-U_bS;N9Q{8JEqZa}D@j8~kDLm<_%ie3uRW4)`nJnf2~4_zoL< z#FwdGHuz}pUK_j$e1i=>8+@G&?gxLs244j(_8~h#cm<@gye3uPAVifho1|JQ+13c5tuL6JC#{X>aUK`vGzQG1x z1-=ga82JZ(zy{w4z7{;Q{oQ#S|GQ29nepdbK>f0be&$Yp~g3kiav@>4=ueI^N4_vpwhhIqjvcZeM$J^i);HBW1>DGZ4 zgJ+gYtLZe+N9zCZ5CKnvMSv#ni7a z9h>fG@V(%f=~jWqZ1CCOyKLg|gTG?qe--!+8~i@-r@=Gz)kf2QCVzIC{xj*^4c==L ze@+SY(FPw0zRm_82mXK!?gd|KgExWSV}maPUu}cm1-{Y-e-J!qgKq_2VuQa1-eH69 z1783>Btw3NUqt<~!Hd9W+29r6wKn+w$KLxu$5qye-_zu_6f)F+BUBwB;)n$X3^+iM ztOU8Vp$!mZ#Hu3%9H3y3C<7LVYp_G<>HtwktdiB}4p`l&aUG#zbh9#I)hN-O#iCKN zI*V1JM0UigY3KL(-uu1z^W+)(JMTH~Iq!Lo_nh?8`?=re?|trb|4cGBiSNLh`>RLe z&Ha_tcyoUZ;@io`^hNwyiyz0gT6_h+#^Pu3Efya+iQ~)S7bL3hqN?U^_FMR5zdmNa#qhTJwBc>_N#bqw z>A_2V%>K{fw_E%mzTe`D_$?Mcj_S%kK?;6-o-9o7C((|v3Tcnju(rM;+ri#fnRCyo%oQ&r}6tWEm+*TmeiA=n@m2h8iw~c{__z2NewW3!;YTb!iQj4QJ@_Gu&*FDj z{2+e7;*0q07C(;fxA+Qv3*MYhW;NcNPa0SJ0i%;RZEk1+aWbrwC zm&F(G9Tq=|Z@2g|eyzn%<6AA>S#V7EqExr@qZ1HLQN{jEqhw$ccoyYHc z$0SH|i%;PPEIxzZ zZt*#Mzr`2uTkwZTyL5@bQG73+)4A;5)*JNxu8f!ZnB!_1FZDTD<2P%(bGE%dqj+hr zIUW*tTmGGRTYJ-ZTYLNP(q1$FJYL#s`eD4Z*YqX)W{aQ1cUycFzscgmZOlJ-bDYHR z5^s(Z-VEWzo8uvg?;zgvJ@|IKxgWFmwU+one5=J5@oOx89N%K`75r+8pT#%h&GttA z!H+lF8^=q$*)A8q(&AJ2ki}>4^7krcJ~_Pny^84z_`McCil4IhGJcQ6Pva->W_z92 z`SE6Zqj-ro>yyCmw)jr`7~b5!Y5Xord>?+q;`8{OmVAcsLl$4c@38nu{D8$*@!Ksv zd=AH>#mDel@Mb@>;d?FdN&IGu@4i;mzZE5Z{G2=dmKb!xBG^Z@2ggeyzpN z;#(~~($4W^@o{{M#k=^`7N5d5TYLt;(&BUYki{49`&KXQpHcjb#h3AWEq)q5W%16r zjDNg&Ttx8_Z~6pY@;8r{PK`ImNm}zaeII^;d`zFm@3#11{Fud;@VhL25+vTP^V!{2GhT;al+Lcq`P2AJuqs{FL#l$;b55_-4F0 zKRYhRqs2$@Axl09{Jx`?=HH2*vG_E8uf_M_rz}2?-(&H^_z8>K8fF9@jdtfi_hY>Tl^rt-{On-EfznH@3r^}ezV2T;=3(A zvX0}$;^X)(i+AxIhL19IQuubfT&su9m%s0pzYzBqaecb|1?mLi^Z2!-dx_zPHU2om zm+(^GqYOWZm-d;*WmV&kHsZtU?fntM+xnvoZ|jdF-s}%)R}WtL!yHFhytLQ!gZNg9 zFXGo&{5ZbF;w$*o7C(z`w)jW~^MS?3@gcl9Ke-ye-007g#;-7Z2EUKL12=sRKV$I) z{9cP6#ZOs$8NbKkr|}aO?_9wAVewJC{Jpwa&jfy##dqRIEIy6jY4LsdA&bxBcUb%| ze!${O`0e;ZjeefQ_gj1wzs2Ii8<;-~FUN5VzZqZeICgcrd7P#2-J~;*vy8@@$9+!Y z&EvSB@n-u*HQw}P{3fFvQuk?mm&H39IbJP3if_001b(f>cj8+uK8;^v@qPFfym>t3 zHQw~Yn!kCRl{DTQr<0n$>8tqFlxzBMC-Z^D$M7pHz6~F;_#}QG-!oz6--DmA_$+>} z#Sh}AEWU`}WAWqo35&1bcU$}{UcL{*tY_pxj&F;P<45r3cysYPEk1=GviJ;shsEdc z0~TMv%lCPh<&NU}ExwH3f)5+>!8E?t;+=~)zAZkA@3!~^ev`#_;=3$9jqkAdK72dg zY*!w?*5ZfptrlOxud(<^d<)*}hbn%xB|dyH~slh7VbM62Fh{buru3gP*bZ zEPk)W58|gRzKGvr@#FXji?85!Tl_43%;F3P&hw-g=v)@YiHJ12Ee2c|b@vAL9{07Fq#mDd~ExrvOviKx^AK$xV?)M)2jKyd1 zdo6wtKV|Vn{2sj7599a=OMC^t8-KVlAI{>(Eb);`8UGd^$IJIgneB4%J1stiAF}ui zeuu^9@B#t(g*S;*~zsurt_z{aQ;CEX5D1HcU&I4uq4omzre!$|LH*tJfd=%f0f1%N?M4k9fjW_!< zjo(5(#~S(c)rrq*yxFc{e6OXRCH!WKpTu|L&3vl(O%@;C#PMj!Cx-99o5y3D#+yEg zm;B9nqetV-aht_U{-z(qw_AJ>zt-Z%@vRnL!LPCSS$vDdN8ZfwYw>Y>Gu}LpxcHTp z_!K^5@frL+z8}$SZw^0W@df-|iyy^LS$rA42XBtUY5au6J8$9mwfHE0%;FRHT^8So zAF=o}ey7Fv;fE|fkKbYO!}tMvKui)3{3yQ5;>-9Bi=W1~TfEcFd}8rYe5=JL z@N4kq`K1%zV)1GGYK!l~H{;FglRSQYnr7GJ^du=rX0fW=3yV*Fct z9N%y8E`E!}r|`WNpTTdo_#D35;tTjq7C(ybviLH-!{Vp$?H2F6o$+t+QG6@joPQD; zZ_YoR8h-@CG*}}1C#~`3{L`oLrqAQoP_F5R@hui#!mqaYNqn=#SMe(?KAfhq79Yd! zTeY-4ZTK0BPvZAld=Gxg;xx^ z@^;N<7BBTVQT^5GBUk(Fjf&Sr%8lcte@+l5-@mQ#E?)Zo6!q7sPvK?!#MNJ`K7*I> zd7OA%B>x<~+mcTKzs2H5@dFlL#_zQFY5bVQJJ8?_B54 zw`M+3e2XPMfnRO$o%m+FS^qSCr6s-(AF}v7Ugq0Fjd3!Jm-*K8CA`eHrk}*i{AT(p zUglfVhp+eN+l5}{z!+ZUPdUbIzsKY1cJsbAg&(t&lfmz@_#A%3;tTkl7C(v~!h7?? zg6{(?*NLCj_=WA0HaYL$cp@M3xR2r`|K-wdT_irC@#gW-i60=|^lAKdi|@ntTYMhB z#o~wYy%t}>Z?^bJe7D6{@tZ6@+{5vQH`^P-cUa=v@a-0##ILpZ9(*hQgrGB_^k-J{ zZ!-L#=D*VLMa|!gAJ_cN`d9F4DA)9}_!hil);;z2tI7YXEokC{&btwO+OOwQJscG!i%Z_sgeiI(;A3MXOp;!E0;0en=4~_vft= zr;GG!1~2#juMkJSZfm%7u3u$)p6&8H;Z<|nubA5|{ZeH6cD64W|M-4GktBpBJ~@_&)|8CKetF3CA@qdvb6u-FxTfBEd4)Ax-rrnF_)XaUn1{jKEZ#+@Nv9+ zzH7$2c=`O;Ywz1uHD5++;-pRC2k@_ve7yI_%)e(Z>AlE~*KlbWGB4$bA0qzr#qmpy zt0Hl`h%?W#OXSsMpP75}&ibY+`9Fslop*jyC!r;*xu?c-9K6U8vF~TJY6KeL5)9JoHwsrGB;kN z-J`nw%d*{@zvth3>AgRW_g>0tFV9=>s<;c}J(Y`$B<9|ppYY9$KkCsT!s~yp-JG$v zI`0j-T(+^K_-P}4jQID8@XGhjAKv@<^u3;~w;8Wp>}cXWW{X~4C$FenTA>J84wC;A z`J2}*Mf@Ja%keXgpTJ)&^`XDE$_oK`Q&$iF2Jflz&i}-%KL0@N4vAxWzLo9gHOX7O zcvlqv%Bvxz{3sVL{lvXgau-LuzmhGU{7t6 zSBr$wY+rftf!an*zpcSPe){lT_)+nkC-0H>*4{1e|IIlZzxqB^#-$uDY$9vQUDxzx zZ^z9G@2#`v$-7s#Ezdc3H{L`3Qtl-AkC6WblD{tEtN6WmbG`}R%KU{tT4Ig)M$*M~ z`@-{u*M9sy(w(B|cn=r-==Y%4ezvdd(%=7Bu)q57&3LmOdHia;tsV)=DC%|&W34CS zP0kbTZ}QI!TyJe`V#eZ~yZTy4?|sM9r0XW%)8%-hGxdFy-uw5w_bq$xJNDj(x1Kk~ zzTf)3$_;V|tfjY?yoOn7;~kI5+qfRNMEAEb9=v&xy?gk6Qbu ztAVNA^0q*oXj5d7BEKo>alKrq)Xh)dZ#}TA@!4gob}ZwSe9NwZ?kda?@}I?Tl4q!r{>?ktZ|d%R(H>l2aEJyq>u6ZmIJkmJmdc(K8|mFEAM|< z(Ba};{MyU84qkAg5}(3%y?yEZNCw}5H}_9Y<8Av#@*CFe+`i5&;!F6QYY{=7Vi zpTR#YeuLa+ZPfdco9B)b-hs;5L|>r#`X(-2{Wns#ktE8Ay*GWJ_GwAa^~~Lz!Gibi z2IIKlJ&T9!I+=_J_u5bTDbhbA>2-<141W97dY#0$$Tc*a$Aq!5iSwUq&$E3u+t*7P z?>x>+sW~eL?^i9deIMI-d>fMYi8CL|OF&(DWvBb7NdLS!NnHCiyzf&E950`S9C6+Y zT;+T{%l7_j57gc&3A}oE*XJCCya&5$@dc06EB=qnr^LTd##`gAWviN{UhBL!Cd&Dw zQ?_5H?~U}z;}D_VX|Hkwut>jTh+BER&JUjT>zl*3__RBh0g`_y0W#Q-I&1E`)A!WNs^@BMf!x?+<3=5=czlc#4!O;cm<6Vxyd*2x?`}WG->nB~ef&Rkwd^;#_-60ibN-`Uj3&m5%&$Gf zg>E}g`y!Bj(f#3#H>&Erp833B*qz(N$xKcM3?%PP=Zy>2{QW*ke%<8v3dv6w*?;AJ zt_ShAX}{=u;=Swm#($vc@=U_Jl4!VhZW!nlr_?iY2m7!8K<$GvpBeklJ5Mg!e~mYg zqvb^4?WRxNlQZ^?clzpX3eBK5AN?~Q+bQ^VG!mTButOD&fiE=823 zynC6q^tj*FKsU_^~_7uvB+ z+VL)m=K`F4xL$_I>hrw+OHvt+Wzuz!?h}&En}>Mz#5~oxU>=fwnk8Hy z@5QnH_$RLW69;OqlfwV@xZ*0Hq4D!Fu$G?vy&WX|GfIA4ztH<4Z~u7LfwF(D_v`3g z2woKIDw2fq{8UM|i*)VsL5NY0`R4`Rb&k8Lop)ZY<$}$)+`FKuabJVAdc4s57<;q$9JjeF{qySswSQ>z>!rsD*QYs$^X<0f-h9@01-UPpnAiG!+keka$+{I^Zow^iDAf!{U;A_0uI&ZdU!;wubE?4PGbrGDfWl5ywd=RJS$ z?zZuI5CQ{H*r z`{Jj`w`EqJ<8s|J|7`^R`6PT7{lWI&nIu*5t}kNvH5T87m;4r9$3{q#)cA!yito|< zUnvaV{63?c7$L)WDgSRhFOuISH`7^6(wNJ?o=kREJ5fh*e z6zPZX2bfQqYqfQU1odNn((6{^{JnnFl+$>Meg5>WgeAX}X~yhVK|NsDELXApJDy2ckMI=FgX0 zPsn76;B?9JVdh@{|0MCoN9=%^xteqs3%pTTdo zZOPG?_hpZWB+Z zxp3sagh{=^cQgMVTU#_A%D9W+*WmZbb%2*2=Py^zUzgs!{DRzyOST?z$IAXg8!z_e z2)C)RXW3)n2ESI4K27?)FR#^h%+)7I&-6n2^;-|=^fGv)Gt!{A#xApy|U)N z=a+R^;_`#F{&8{h$CG!RHb4Ho zORN@!igRK6)M7)$otDd}5?^?&bcKd^Sry7p`jN zj)G3Sh<`W8vjN74e_s(NZicu`632N(&L!)c{EsU*lh4H^iCgokgU9s|*G=5tI-kk- zDG;}PO>ORTCcPeF{?Y5=x%q7-M(I)ioQ|`T6(>?Q9fF;cvcK(tJmdn zb$`?1tME?JwUMst)wSA2>DRwqZ=Uls9&bNg%5OvX=b4=3*IKKcv^c-{{^mAp+X_8j zb~mnAW*g9w8H?0!lKfgvt<}!8oX?H(uRLvT;F*|Z|M}+`>8JRI$!~3~#`LPkKc`Cn zxdNA1tGS|LE?p;kK~JC?n!I@+O}g!*J3`XwBJ)EZegOaCK*A_LdHfFiDdPEDi0kCX zyh)l>sYj9RJO3ZlW14iKvucZe?@`KgKEm}T`FZCZZ(f_fE|=pkrrW*ahyAS2lf36v z4gcu3mV2v>O$|wZg+~MWILU86>Gx8u-2McM*M9sI-n-sUu4=x3M#_H6OL_SD=lb*4 z2gdcr`c=IR{np*4&m*Orij;qLZOPvq&Pw?fAIWn(T6`QoVeu}0H~v^@9M_d^p&c@> zrto9y_Sc4_;j5Z?c)sBUP48LRA3EnHXP@|**l`jbmw#B6-S&pivMtTaUds7nU>O(I z{fFMM(!97PF2YZOpIhI!e@TBz-(6#PsfR2}@;CC9e5Ee`lU$nb()6pEC0@qPwo8`A zOZ~Q3ywvY@Oa3y~ZnpS7{9P8G$7d{l82=87FX1;?{3QN-i?8BOGrY7_mc{)j&$@gs z{ULqbOS`?}*4N(tlK&d%S|ncb+}LIBziTgD?CX@@vb9k@P4>rylz*X-PMj=8`HMa) zmhDm>+5dM^pOZxBQYU@;!q1u|G)Uh<`*#J2VV(573!k4XOn?0w7Pq@j`hjIe20{7n zGt!H9`ROryoqWd{w=Uo2}>kFot+==_T9=~XSknMKlP`I!l}e}D0INn7A2blLvev%vWh+l_0C`D@(s z{d-041BA65OKugYpLL>o5CjzvchV*NFG}>w;BdPQ(3& zc^6L`nj~(JxYd8vxCetc9&R{tAuAVI9bR0CxTzrSBlY8^iQ74=`SsV2i|`z{cfZEn zV2_ji*GAmh1N&?Hr5$e}j-9k{|80=Y`pm-p=Qc^Yj67$qX}UB2@6t(s43lm*>3%BZ zMS^GL3)w3fe7DaS7t4g18HUxW(gFj?)xzI}g$P&LGaaX7-L#Z$Er;Za;K*`z=TO=9L=% z(!U)q<7kxlHHY#!vdjaaMe!R}O|5J^fg3Hkt!3iyK7$~MH(ymrH*%P!t6aXYUUkz+ zd*VE=T^rW=?Ov45YtN_W+T;H|Zi@JQ#Q#w0_XR)x9{JmXyBm$qvNo)mS=sn%s%w6V zG50x*jEiCN+rH|6KmKneKW|)kXG0+a^MKf|I8r&rQI(vxo#;14OdsEkE z=l17$UcPajXvz1zdgXs3$aiu7cam39o_rn4@9i+ooPYK6{TrV_@(g`h!`H1R<_+F{ zDv)1u%>UfR+3M$4@3XLOik2n9&m`&AknV;2-+2$|I2NQIt5UxNeDvZ_+K8VJtq0RH7HjiuS+1W- z#0?R5y5zftxbFGuGpVO6%Wj*yzWd!(bJurEKe&;8ley))|KNYV=6rfnt;V|LI`M(| zeEj=Bsq=ZRe|_p4o$}I&lVw5qH!MiE=)OhjIzfM50G(IWO@H#@^s+bRudja^q(8(@ zZ)^XRbM0T%Zd@jN<3`#)(zf4w9^<^}O7pKjZIkmPgM>-X1&q4Tr1)a#G8 zEy!og@o(ei?iY?_KfFTHf6}jq>>rn(69a$$yiegz5B!dKpTV~W{-g6ghu;|Z56}Ao zelYM~oA;ym*St>abC^Faq&{VSUKjX7=KVDOg1|fT-r;yj27cMRm*eP)z#o_|H-UfP z94+^r`S?!!NZ>y>@6-5Y?HXU0_j0`*4!n+Qsec}Sbl^WZA3uygF7Wry`x5@-!0+(A z93PYTw+8<6zDIkD`0kAK~YN3N3qS zc|tt=Gn|D{_8XTxwR4~| zTdQ3Kqwr3cfIDF){60*>-@`cZN6=muCSXskR=W*mq4a+W|0`Hw{RH~Ak93_d4{wBn z_^-lY*7v{?{41P<$1t9&@LU+CercE?zq??J_0PlTbglL!nEG93qb z-WwT5G5YydI89su#^H})bziNPWd4i)p3iuhcXGd|)!Ja1_-(KNABUafTNV$WgwyQ5 zU%(#H{R(EtZ!au8TdO?>vv3wVaM`)EkL@d<%X$;+BcEoNhc6P6-%H>yJXVO0!4ftZB80lOSU>WA1dkf{kJRFBrSP}mo>c=ChER4a(7RE0u+)jVNA{>Ru_t9P+Mdo1~ zmbcQsunPNNB+Iyj2{;Tha9sSI^cO6^PR3mkreO*8!Nhj@6=vY9@So_1H08hqEWu8g z{s7|yW?@!59E24(3Y`y<4~)TC=)&mLq=RiR3sbNF`(OzU!U`OPPLA|224|rQqt}og zw!th+!2;}qB{&Exa1=UslOD$4EOcS?TGGQdn1v}=fPJt82Vn({LgyaR!x)@}X&Bv1 zI@ksaFa=An4_4qHbnYcRjKN9h!daMx(d+0>n1ET>3G*-w3$PCsVIG#?Ff79otiVZF zg;nSbu>Y@Tf5I4y!8VwHN$A2Jn1orFhJ!Ezi!ckvVIEds0nWlAjJ$*LVH}pB3o9@M zt1ttd4>68m1QuWnj=}^iLl;iNBy@TxA4XvYCSVqJ!aPjF0_=k&n1^LJ3@fk%t8fxV zK1};z42Ex@92kR1*ap)u2{W(c<;2jE!#IpV7bai| zx-bKiFb6ZR0JCru=3yBY;4~~k=iQVGqp$)KunIe&b06)85!eS~Fb@-O7`m_olW-EI zVHIXz_$JDQF_?#KumF>=2zy`|W?=;mLg!<&A4cFfjKK;_z**?RNQUxY9HyZQGcX0S zFaz^22aB)(OK=pHVHsB7G^|4BX3E_``(X?wU;=hR7p7qn_Q5pF!wejTSy+O3I0*}| z3X3p&3+2KXEWJTN1-!F`(X@D!vu70r(76?Ntl3X*aNCgC(pL+1|4hf$b?30Q!gun5zz1p8na=3xa6!zwI6=hL(wMqm}j zVEFx%3uDlQZ7>OwFb#WP24-Ov4#EN~!Xg}pC0K!FI14K+u60rtQm%)$~Jgk@NS6*vy7umYU|?S~N<*+%&=4inIYE=<8B%)m6v z!3-?GEF6V-ScU~S4U5pZi}GOVFdQU7|g>29EL6|!8DwN8CZo`7`}&c zVGI^v8!W;kEWsXFhFMsFgRlyV(D^*=hY?tTF*pkoFmf;D!Z=Jq7p7qfW?%+pVGibD z0T$pWEWt7?!)aK7&H&}YD0Kdn_QMG5gfW&74|@9nD)a69E3?&glRYq^RNO7a26I}YW22KK-#%)&ezgaufHMK}&iumUS^7CK+1{W~ZR#$gP)FacB0g&CNHIhck8 zn1Q1(3(GJMr(pp)AE!JRg(aAPW!MQTFb%7)4?6!w`(XqQ!x${V1e}B}timJ=e}eL1 z3}#>(%)%th!yZ_KSy+ODundc^0>@z$R-p40+7BZzazEw5I7~nnx-bQkFay&t2Q#n$ zvv3sVVHp?By=94o-hK#pQ1b%gDz}?NtlFb*aI^#3$t(#=3x;Q;5aP83M|1{ScZ`y%7t-Q zg)Vd+ru{GiGcX2oFaZnDg`+SD%P0M|L+5MM6Gou(FO&zPFaZ9voH%I|4O+q4hztQMVNvmn1L0TgH>37&cD-s7=dLNgVQhpongv@QJ91Yn1-D& z1Jf`I`(PgCVF3=qA}qlYoP=dqg%ue70_DRPbjD~ujKCy}!5)}^S?Iz+n1n@`hT|{; zD=-UZVF5ei(rT7=xoQ0n5;Z(=Z90FHsJR!aPjC0_=oE zn1&_T2g@)ID{vTAVF^0lqWv%et1t$`Ba{bY(1mR<36n4ldte4;VHOU;JS@Tj9EU|% zfh9N#%P{g~%7<}Sg)Vd+rTs7hGcX2oFaZnDg`+SD%PDsm=)w$4!7R+cJj}rYEWjchg(X;qWjGBh(Ah=#FbbXBv>!%bCyc=~ zOu#!(XF(7=uOF21_sr%diJlU=~*4AauS<`(Xr*!x*f< z1e}FQ7;`SFwDaeEWt@wg;f~&KJ6bRJ&eH&Y=Z@ugk{(RopIU^6L1iwVG-uxI4r>m ztioC7{DAge3?A56eJbm1^e!V*lwNtl6En1$hgr(76=1=t3QFbPYr2bN(LR^TA4!Xk7Yr~NPj zD=-FUVFE_RC?CdQ61p%AQ!oQFFbi`q4-2pWM`00`VF^ydGIaifa$yWQ6Vww%U=qe) z4@|%;bm1UO!y?SUahQb_Sb(#z2qWL392kdX=)ww2!79u^=SS2NMqmNP;3!PMGIZfI zOhV^T%7sywfeDy}ov;Ygumt;H8RlUH4#O%eLFdQpCm4ZM7=z&w<-r(qVH+&Lw5!F#2uEg9+&T7xjV>n1%`12a_-lvv3#|UJbFcyn(0P(}!5A#V zB%FpB=oxgG%!72>@k^Y>ezhQYl;|P`x5D&Bb zJ6%NA+e^*k51^x~s{3qJO-*+TnmcQ3X!AukJFv;Il z3`6&D;)VQOgtL$O@pljL?-LI@VIHPo5%$3{92C#rJxs$2jE>Vk{5?Y-%)mU%!eLm2 zWm)I%7-nGsMt(rMU>qj+dxR9s^2>Kw7~}5=3a|*PvYo#di2V>AO+59>M=1wZU=cbm zqke3Uz&?qCdFaAnn1m&mfs?QZoySRc4Ee$&bYT&uVCGo9M;Vr28CKvlbXw@o3F-%9 zFbQ3lfhm}W8CZmQn0Pt+OST_J|Nn^mU>26)AgsV5bY4LmjKOIke{azCWAZzZI9TSF z4996_^I1!kc8bIOBx zSR$?rCnX+Mq4Nv+|0&YJ7>vO-=)x4tzzod794tRYf57zAfm#I?_fnrJ>iHY$153|R zf0+IQ^@qiMXd^5iLOe_#O1aQIj67r=hX05E!!{Uc zq8wO(gD@ZF|Ij&{xTlGS30Q(D7#E5||Ou-Dyz~b?g1xs)e zmSGiEVE7sS4`Wb%zxwaX-+SQiJ@EG)_v3PYz|lI|7Jhzr&v{I zz3C(VcK#IGTlgDgZ+VgW;5*;U^;hZku*Tn{OsMz16c0D?89#sd8$Vghbl3Sh)NbUr z`d|EXr|R|#b^GDEexvfOM!GH|-DV?QTGQQP`0bkRG~J%n?Loh(UV@Kk+Wps%Dq^^(YehK0~t-i1zUeY{h@sj3q>W7u`)c_Lz)jEEc`mZbHZ#*TR$8`O> z%Ah{uy8gH_sQ-_2y}ZE7wkLG`NoA1Fq^>`ulyN5UztQz+rHms<^JiV3RZ6V<90=C) z{PAUuDlu7{ODxi8T5SKjT%OCGZANcDT~S?ph=ezlR$OLhGiWspycuD`;FKVH{g zsQXt+k)IRl__)POJzrhNpQiqFWzgQWx_*kz3*x1&XRCjmGN^yMu3xwyUecVWew|Y4 zE`CE0uMEoV()CM~a@}|Q4Dz{M*B6c- zsp~(gze6cb(qwi0E+u~|?Cr0+b$wt#yrlVvt}iTC^0`m_$Ccvb=YC!Pq%s%}pV9RP zltF*))b)Qg{1x(tJhNA2Q;}RtEj^eO+IekCgpG z^*>q=FKNoU{u8CdN*Pb;`lJ#6l&=3$8Pw-#UH^@eukiKS`xjl`uMGO3rt1wKl^pz7 zsn2pzwPS>S=@^iVd{q4s3W?h$h$g*%<&M7zQ_MrTQ>$3fJ-5#`W;ks% z$J(rJm$4{|IUb$+{5>nNvMlU>sq-q`ex$c`@w$xJh38obG1p&imW=4S)JY=y_5MfJWm%(fQyMp*+XLV7F@L}AGJLmsGk!?@UZb2KZbZMR z*1YfXYcv1vT=@mR{9dhR5YeUWoq4xEJ2cDPZCtL;<< z`+KjJ7nC1*z^{*4e!rIAuKm+(^i#d^XY?Cr`|r|z)O104YqUJGzh;8{^$9;+i*n}U z{(6vp?2CSVgY)5{04MA=)YDiCn&ew zC^v}fRv)bQ>v~Y$n3mVB$JI-x{7De8V57ey{Rwqq z@A@ILeK~)Aknh)#C78bg?%V8d59Y1li;{zRJb1?lA1!*f>bLsgq2M~BTMG`ZD}wmo zJT>LL3@H=}=G{BK<=5kn2mJgtuk|z3Y(qcyH_u&5ecf39Ck+hZgLhB{=e=_^KDdtP z`lufnocDV_;P>21G_G6csi>|8{SbT?{$}0&Qr#Z3Pp)GZF5Y+6BZ7ARRr@Qb=Zvmz zy4*Ljy1wsPe|^6Ze?ZqGx_@fAzIawzScCs>{lE7SdrfcA{jp7XpYlQFL(0dLPbi;O zKCfJHpWkmsDUVm4py(!% zuTkEj+@`!w`JnP4Jg$_JGXDIZflp?q5TymG}Sw0!09$}^Pf zl$R;5QQo55ro2!2pz9i zw*3!(JAL;-(zhQZefvSuw;UvW=pfstX8v~l#}2Z+`2^-yfuZ4zhjd zAlv&7vc3CY{d17??FZT3QfGT_zd!#3*VnBB{yNta-V$1W-g&QWS#xRT>ML(}L$c+p zQ(kjQ>&d5{dD2ZH-kmx1l*CClffKwfr?#BddfKV2uW3DNak5u;UiZ!`-+g0C;*>K^ zdCkeE-h9=~@66nM`g|UzNgk)2;BV(U9GUUt`ck<5qBn-Fy|E|Va_T9k%J&ENTz8dM z^c7e3UU$k(H=dHd>e|pTA1t?A)_IwCO*rtU<)PshZ%lXXKkM;ZKk$?OzRTLLI{L4d zfB18g4_)$uD@Wh<^y^;L@ZGOG{rket?|8}i$Cfg^9e??+wMRX4bM@Iz-SDD2R=@Jg z?SD9@^LLN*KYaW%yHZDu9J~I>S6}su4+#a=k&FGxP;mXf*dID~->}#p77DJX z7yG7Aw_cYm_FRSEzx23k3>_W{uEQ6{uL||+`E#*r`Be#UJ+#KFhQCSK^N3Zps{QCwYG9_>s?-Tlg`T3lS}*Wc^xxZ1uba zZ>!HCb?O~g{?lx?wQFY`{{_#7 zLt~%w^AGNezh5W*M|J$3AfKQ3`Lwex%X9qL>T?0}rp?PQs=l02v{|3SZIVn_UP-p% zcj)t`z+a5F?cbEfuQuZE#M{c1X)Smr>zC;NJ6I?FGj+Ulqb>hkb^Leg_@CABWzUC0 zQyb|(7I`<1EdO06{?~Q9Tt?XH`Nul`FLivAw;#iyiAM=w37$6{r+&)t^3I3!8(KYbzZ;*cf6WrK{j{wI7d=LuQXs-IB* zJ70$`Qs1oeZM*uj>eT0Yjqft%=k4md)%R;Y@)+ILt}kf(l*V^yd`F#ff2i@TPx|eW z^PDWts~=L|;p@<`TpoDGo%&1Fcc}0Bsh>{}e}(!X^{>_V+dZ!<3$Rt=r!<~d%zDc| z*NJ~n<9lVIW4V`l%krH%@&8%J|5Wn{J>};U92bA86aU>h{n@~AW7{uR*NH#Wi)S3l z1j`aUhd)|VKwwO8slp|GG{+KUc@^s^h;}r=NG$@%?rD zyX*LS>-bG|{Gkj0*}r4@JdWYvEw5HTp`LE>mN%)NGW;FtXAJ*Y^`Zau`#;F%>N@rL zuEsYT@xN2wV)&yONK*e+^}*+C=XhQ#y#VVpzTL>@ggW)PLF2oO_z$b^Hhf2&e7>Ra zy+-_N>%>2+@%={pu{2QHJ7D+@^+Se#kNOeA->-g5J*(dGZS@m|e@^|B;a|oFbW)!g z!>?B#s`~pq$o~fQ&4&ND`WC}~SADDDf2Y3P@JI8(snn;-@UKSqi;p*|!x8Z5!*tb5hBs1J_! z!w&cB*{VJ`PS>b!SN{SpH=Z-9?=td#ulio~!G15Q?^hpu9{e-)L+XQam+=9#*KdYD zLH(HFyVOq@{ucF9>VtdFkEjnlzj$7HSbekM|3`g`;aAZiQqNYypQXOt@RzIaQXlls z9qPLc|DgI_!#}RR-|#c)2h_9bEl2VJyVP^Yh;LUvV)*OTkEv(ac*}tL2_t@&`YFRt zsh=_Ya(0B&C-kSq^TWyNn+^X)^(}_KOMR>1N7T0){wM0Y41X9GrBa`6^}+G-O7;Ef zgX3kR`T_O9xP6!UA;a%bKVtZAsvlDy?60TQPZ;qp1oyVVEd{{i*AhTo&UUwtqR4||bc?tl@0 zlKLUTU#xyaJ>BaqH>;m8;&-T@GW;XzXAJ*q^`Sp69?vi4L9O&pv*9mL-=aRK&+Y2l zjrdQi?@}L(lgHI}8}WZp-)s1jxPg-T^c(&P^#g|gkoqCRKcs%d@V`?(X80F#KuNh1 zhF_(f&^EvgcM*Pb-0eR!rh~J>T+laqeeXrpM z)%P3z+v*1l|D5_E!@r6f8L7{R;V)M|X83#6PZ<7T^;3rbmHHX=LH``ajgOSu{MW_( zdAj-*!*{E1HT-t{tWd4hVNECWca((j~M7Oa}T&{Ram--nazE6E?@d^Pe#MYt>Jw=WzCx zOVx+=`~4jFThzCx56;&G^{wiI^Yu^Ew;O)h%l-Ov8GeoWZo_X--)s0D_5FswNBw}| zA5lMK_&w@J3?DwuujiQI6Y3`nf2H~VtZoslLmI z?@`~aJ~-|^sJ>tQa&NSRLSI)upniq=r_~Rs=QhDxj(C+{pAo~KqkhcD=N;-N4F90| zDZ^LP&lvu=HGa9lUq=P?yjp!TziT2(;6J0j#qhsW-)i_ZCul#b53Xk}Ro`XA-=@CX z@SjrOYxr-g?^l1QS6e<8RzG0)mJ|K@45<&!zn7>VGvaSmKVkS!tDiFb57o~Ye!u$A zGQU5AdagOiuTQh#FH_%Q_^kR?!;h$MH~i1lcNzZBxLVxt8!c+Wm zTMXZ&zSYR*F7@q3d`W$m5r05^w-JBNtNn6&4d1W6-^k}-^#ewHRsE3RPi^({A5kC7 zS8q~3VdVc2^;1SZ-%~$h_~obi`G*=8&kwIx-)#8zs&6s;BkEfX|BU)}!yj{+Uv8J- zJJoj^{&w}fhX1_!e#8Gv{ea<{UgMWLq&_%)*Qy^e{ATrIhQCk!gyFxZeoB3?zxJvR zIg7{hQ3=03&FX{kyk32a;crskYWN-M+YSFs^<9R4PJOrGUvj!%pI*bSQ{Qj+o7E2( z{sHwvhA*ohG5ld?_~nin{!H}~hJUmADfK}=XViz5FYf10sc%*v^z(Prw;29e^{s|) zKGUyfyW!WV?=t+Y>bnj9&+2;(|9$oShOeq0F#IuV{rU_U{yg;~>W|a&X|MVz^+CUV zNPTF<;(ptuzFB?HZ@*CAV)(<(^6S%T_|w(58-BC;F2fI~?>77{^}U9FN`1fK4}Yy+ zp8>&yhCl3eez`M-?@%9Fxw!w|tG?OrUsK;=_-EC(8vdknwEqo%jruOb z-><&g@IO}HYxqOj{rvk4zgGQ#;oqfx$nX!S|39Uj33y~jb?+O4O$^2s25bxj1TkP1 z4?W|R<$>0j5ol)Gl4b@BxYg>ulA39Cw|kixK>~t=nDrqCNHBy%ge9@VB115Q0MX+! zBmo{#whsc|dnkk?JRm`jB*fu`@cvcxufBE9t-EM4-)Fb_*QZXMI<;^2ZVLWQ@-4w1 zF>2!77W`)N9l_s9zDs^l!TkF&dEvsS|1TUf`iBHRL_RF|t>i_)?<5}){JZ2O!Jj;C z;++@#X7aY+Zy{e3{13_31>Ys#5d3iyCf-eQ+de;se4E^khwJ1!ue(+(Ot*9EVV zZwUSl@=d|-CEpT!mwcPtw$G0`Wa8Zse1d#e@Fsad9f-w0tLL5MLxTSW`7pU{Z+}c) z6!K3yZ0s2ke2%;%_!{}V;2$M#3;u2LHNh{RqW=Yd0r`gDw~=oO{#o)Z!T*VTTkyv` zpZ*v882PT?Zz3;T6!rhT@_E63K;9Po=cbK4 zYl7cGzApG3JU+heX?%D*2G$ zx04SGeh+z3@UN4P2>zg>#-5Vk&nBN2{Ke#L!CynZCir{E*9HF^`G(;COui}jqmCIn zx5yttKaY}clV3=_Oui%do5^pvGM<<)bnTL!{k=~zmpdQzp`ZP91;8=c}eiI z%{Tlg@ z;Ds4u&#>UvkQW6H$VUYKHS&_+_ma<(KZyDGL-IEH0rJPq8hbX$ZMm8y-y*-7^1npB zOK!`<2gwUV(RT9dR^wmwa3B zi(hE$-x2(3@?F8F$qSbnKil-4A|EEV>HSUeBDu}qzak$I{DK$JpX4@wuO*)s^0$z; z1%Ctin$Yt}@^!(#MZO{UMK>FJHU+4SbV&a__{F&r!!CywcCivUQ*9HF^ z`G(-%Bi|JK(JwXjYzaO|zAbo{d`Iy2lJ5%sr{o27fe!zyKhL{`{ulfi#Imt-L-3o)HwAws`Ig`xBi|PMYvenEpEqyf z-4(n@UU)>beJ+y^3I0a%VZlE^UKITM z%T;zwc9iL&!gN!PvPe_)+pL@{6ePP2}4`ev5oZ@P8uTCAa1CfmIXl&}HXo z&DIZ(Cm$xavzZwdZ&@@>H{2$VhXe?yw%oS2Y*F8QwDZSun9#{bsOzd=4kZu8}f0untYAi*3<8iuajR+`6twk zJ)7hJA!|ed{^-Mzf8p&|MyVMVf7W!cHlYWLxQ)+hXsEpc~S5` zCLa;}U&u>>?>lAuGcWi{$=l?%9eV}&I{Eob;ycMV$S)xO4Ed(uJLFq}U)nJCYzzK8 z@*TlXk?#tA2YKO9(fa;H@*%;0LOv|`wJXM+qToy9BZ9w!yd?M^k%pCE4w{$}zu!S5ws7yMtzHw1s;ttQ@0!Cy+gCHR}k zw*|kKe23iDqi>KG9%KAw`G1fPliT|Bl#a2dNN(%bi^xX=e^JT1;3zg;vEtE2J({N74mt(f04W`_+8{{f`6HOo!r*PUGfdVuQ+Y&+!TD0 zd`s|`k#7tB7V;gzKTEzV__xUmk2U_c{=E2%v1dr|tI3B2KS^E`{MF-q;IAa#75q-}!f@38|C4-3@E?&6lUu)C@$<&cqL6=% z=ImUZk1@)R2>E65lHhM8pBMc1$lHQ{m3&R`!Y>#**9CtP`G(*J$u|XWkZ%e8R`P9f z>z|L2?+E$7A>So`2z_+^D~+9n$4B#JANi2rC&-5de?56o@Vm%I1pf+oN$?+%&kKIV ztBjp(!JkLICiqLq*9Cty`G(+kk#CYelK%NT`8K&7hwPH?klS&{BiD?byMjN9yzm6$ zXDh!zJ|y^UR*_z%gq1;6|?#?BqVpF_SYc%8iPbLVN_S^vL@d`R%S$%n~pfBnD6i-Mo` zi^iT2!LK4Okq_zE3xyYu&kOkmd0X(ekgo~;S@Lzkzem17Zrg`Tf63UnDdcY?-x9n^ zzAgCeoy|HIU@axET$!+mVajojALI{CWb zZzkUm{Nv=CFCf_Ev@eciWW9N?0bC7&j@E&>Ls%U%i0rDZizeYYR_{DEA@fOLg zolhq(ky|@Ykk1P}zd+t5zl8aCC;6I?{~Gza;15|h_G}0~PQEGlE6BIVt^IeA?+E_4 zaj{w(rs!57JQ1b+wlF8M>4zkfnrcxtp9{s;Mx;Mcs(*fT8n%gBp@ zzngqS@NM#v;1|A~{uKNM^0wd|@-@LfLcT8ex5zihZF}{&Up4V=3jPxEEy3SLzD<4+ z{rLs*9U))%HKTu5@T@)5zWdxx>7B>1i5^Mc<+ z-WL2jG1pg*^N$>~#x`}sQ@I&Nn!Cy*1* zJnY>j-YxQnQhthjhupTSuO#0U{G;TBtE277zmg9Le&9Duyu*U8kQW7iANh#je@$Kz z{G#8aKLvj-d0X&0`I_KwC0`f(Uh)mWe@wnf{s{W>$_-=Bwvazgz9aaX$#==GqMk31 z5AQR6ek%D-$xGyqA%Et3OuTLKr;xYFH^}#qe~5gW{ORQ1B`-X~#QOyDtA5MqFOpk5 ztK{>7e}#Nq@W;N_=-CqdCFHw;zmI&lc%HVz#``_;61iIE52l{~OJ3M-(reRo?FWqhA#z*a zXUL1>w*Hed|U7h@*Tmy zM7}HdkI4(yM9cZpe%IJDB=`yPVZnciyh#3V=Hp%DCGsKizb2m-dM^B+iMK8Ib>wS; zuaK_`emD6BxosbQK)xm9pZp;c@3!DKk?)Y(`raciTx)hY_upgeDLlvMvG%-}e3;zoe=YflkpC$8ypaDU`5O5XY0rZ`!F(aN_FPB4O>WD> z0(s%T89SdwJ#Qp0l3z>yDe^Y?jpW}Z-z0x3`K6yU_Uw?qi2QlvBhNMQT7TY3UU;72 z*8caB50n2E_53CIwva#n_f5RJPpKb`u2NIp;g4Du`gz}T}r zW%U08`2q5+=NtaB_cK3?lJ8C%Zr>+WA}<^>{KwRDi{&N5zf0b*dIUd9zH?Jl{l-ZvFEF^0k*l`8DM2mqz(0`TQ+WeiM1A9Obu=kIYB;De__^%2&yU7YzR} z%fsu)cdAkTHu8-is^>lA>x+heg?c_r-d;A``v249rFvA)7s*FnX1KNIYvhGSRQ~VC zcLo1v@|~5aeBm>!7fr*hJr5z@Y(?cCO}=q!RQ@XP#}_Ux48QOGx%BsG5g&!})ZCGBE$9SGb-ZA=Gk0XX(P{>Bm4ya^so(s+l&fbeNN zt1er?ZlTubuC5eTf|YL23q@yWsFg$9$`qvXtV+|(b0?1OFDiDnvQR(0fBy~myHV+O z%e~chfP^o$CgV~E_Ls}2SL$&|>&sTNid0JF2M|%$6AH?@&332W>@Ak7%clz69vs{$ zEVlc-ZUJ5jnx|oQM5GKqR~qd~Z&`=Huf0ySjSNKaa`{ZB(k?Hy+Cft}u2L&3RvWEu z0L7hNt5G;rZ#0y`9vrMflou=YM(DCbLGQ56l2RSlwFa3pz{k5m$3{|Xte9xdbm}NL zwFV4tbyP&Pep{!Y3TR?i7OT9(j}2IcoT{$0)wd=rH}Q-&>OnJ-d%6scw%b#SM}un6 z?N&Ogg+=(JYc0^xcUMsuS9F@7t?WR%3#8I&YfW(;NIRJ&4B89q_a#imwwL{Rjs$atsHT|QiC zsv>6!5wc1bsBX2=TnyF1RvpDu6J@Ees*u?eWOB*~+4>Ix2*U zAoAmAv(;Q(Y4y9hRw%tzVycPu5e;5>9Es`$`s-pvwF_lbl4h%nx=`uWTg`&1qDbcT z*BaG>fCuZ%+OduauGEJrm>?TL3sIfzY0GU3w9*GsQJgA7QNO97)^G@gW;W=Y4mxFc zNmhE_uR4{{*d4LZcLHh`E|uFlq^abggh_Syy&SEf&JXd*=KP2hOqIq}nH+E7#qlY= z3U-I^#&@jHfgFfhLWk8O4WwjwmX)HLF5NJXw_>@^BD2oKqMF5( z%Be_0?bqjsnnIqb{7}#gI`wK+sR%7brJU_m&>3krC3_%rZ7y*%f0-(e2E8+_P98tZ zpv&q7v%OZQvgGs;ey7c12pBZG%dNbLs&uN$Danu3$e5E*s!moqs^>kT`uC;aaI59? zbrCbiY|}8cIEtnmSi{Ygf49zQgmC4wxR#t1n} z>`1>`ujUdmxtJw-c<#vbY`>%XnT$kCHBX}(jtpiZz8;kF6ZKlq%8IyDQDdY^1G(8l zR%RC8sU}9ZIh6M5L8dO_no^_UjLUT5RjXG%T|97&j7a~01ez|-*Enj4vFX;*QcyeQ zjuk`22(Q0jq#|6c1FO!kj*ef|eYX7Q?8_6ArJ2d`(YZ-~sJWoCf@#v8gLN>LN0~dW zD$u0LSg%bmS=lyG=~WK48kl8xrO-xodM8@-YA_R2TOHlf`%;J|rJX|zk&d3m3`5Nk zwYWD@NG1O`0XtUO9kdV0GFHK{cCU_E<7{Q6-3Vsj$)xB;`@Q8^s6?*J)vKr6S)-{4 zIvbVs5Hu18Tb+}^!f3nh)j5Vyf78+G6`E|;vP8x!m_9bbSiO3bafd72Y2;F~8^AE+ zzE^N;syh}eqr*NrI_DJ_tziJzt9Nxt^-9bHxArkuo@`b-tLh-nRaYBSR+8mtrRwMc<-FrF)85p;A*E$3TAV;%zgUm=9Xi_~GYfN6k^RG73vb!M3q6z=yitp;ef(MIglD#ood#CvndHMK`@Zk?&~hsV|6H|l&Q&Q zXTGGHI8;^81wB^Ml59|ZKa5d^rxT9_Nv=GOsHta1(Hb^WvUK3z?x9Yr-_Dm8Ms{i< zZ=M+Ww2~#vz{<=5isN3aVG!Ry%{m25tobvg5REq~&8%pZOm@OYU@TuZg!Nh$OOz#yUInQW!l zGuP7nR#Ggg6;*v;?PMLxR|(Gcn1Vq**5fAXl}0PA$jqu`c4HleY8d_{MS=W064dJb zm7K!UmCjO-N0yzp^7IrI-8(C0X)Gb7lvd^T*y19FK%KNY45PK=8O-ie66R>jQ=1ua zMsY^mDot1>?O|m>E%znXM&$8Ky}De&FgH;92MK8($D(v6kKmy0owM)@0@Hvbnc|HB zfiir;+%v@@k8!3HD?eiGEWKkIZ?#cRjOxr>bYwG=XcW)Y-DUP@(=qUd5lu7n`K@|W z6}R!_N^hpsT1kq1I;flu@(3bsD`q;i%(NqBBbkv|vsk(dRqF+sBx@r^q)JX<*~X-P zw1(+!lEP5zB<3%zGsm!_g`Po81CoL$5v$t-$_Sv>kM+bTBu8w-`UdVs;TufEdKcAkb4w7h^4-M2TO+) zEDdA{%k5*4nD~aC%qpZQd-uR(*Ho9OPg>8>P8zc=3W8eKjK$(>sakS;IjEjGh*~;Z zU&4wl((F&|>~iajss`BCp3~d3o|W%Gftpmi#zD=IpAMQ!@iBi&0PoWJKwjta@u}%b zbE)r{qF7gHF`kH+YMz)$NSe1qVW-`*n5uooqZU)mvHDU8<6$`MNUN6QIK2)%Gl7l% zg;u4LQ1Yg_CuR<#yFi?1%rG=SE1R0bC^CBtL955Hidvbj^qW<5D1L{iMSm0tD$e}_j7R!*zI0C!Uc`}<*YkRqR^dbCz^Ri}xnLB;7H zzqDR6pKdKItolY%uDvjV{r^DR^vkBGq#5N2ijkDs#jZ5YEi`7ACn}A8a8993pxFkt zj|Y1`do&-#sHZkS&MAJ=pXfgMoWiSd1nLHs`?)rltp$Zs=UT!%%FSnUlt)XaIYrG5 zGllfjFjLB%IcEwZB`7;ujHyqv(xy9g7U`zm22i_?X^PR{XdANRTz%ddP*UVMa%uuc z|9Zim%rJh)GTJ6CuXMAfjZjmgBi+~%o-c(;6r+gvkkl1+o145Qt6sni<&|`yNASv-6@`#;#T_{&hn66SkOgQFSO>|vsmF5zxH%|p7r&H`S z&R3lYnz=Ss5(5nB%``=Qt;NUYjy;Ki)p$Lq&9#iq*dQxi#Y}($#@%JrY^n;AVtfe$ zj&m^E6D+^MW;?4ph!<L36E)E?I`;_j8YWpfIn$X8MgFwA}fYp3RORiiiG|-)K+Z z#0-{$I8(m5YAkmkr5IQ-HbDBQT~p2cRXNVc6cKiM^ys++;}X?51?3>LdoF>wQF|^y zvA-cImd#O>oBCLm$PobxQ(o~Ylz&}a zJnLp2TL$K54F*giyJc!OUvrqR+M@iz`p8OtPa4I#2Ieru_}zpN?$j02^uFqBrmB$p zrAFsxw29$2oZ_p*7aYUi(cdVYSol^`oN&^HlUd=n6y509mE7T0x0l0Ty}g*gx@vnHm8xf_)Dblj2_yNKTtensIW_K6f5+M|D?r=qDMk~yIt%aJ&~-u$Cc!y_m>HrG z4t;36Y}FoXA{WqK>d5WLD>iPk(l*ZrA(GT$-q2hMkOgWBP0!$yBq!8nc55}=O1LbV z=IAs-7YnmZqYge`B@%^W|MfD;s01eCfxcjrEr#Xim`f6rjdoX?u`}3lS*3BWqBeK3 z?M0Ki6kjxrTVW=q+4^ReaUeTg4DHw~QjUtqL@Gz3)e%0m-J4OIP~KIFeLFqT1X56KD`BC_-WChz zyD{lKWD<3I2S72s%3O8WY_wU+W%W({R(#Cm0sHo=DR3U(Q4kj|Hx195XD zGTmIgci&w;iTgvi>C;vPAw7WUmKZFksQ|~@H5?d7m|Tb?YtcyP>?n=?NFeOSovsIG z&ME9%y_c0pPmJj4s+~G66vTHjGn06t-c2~Qof#D8J2MI#Iu2f0B(qkDMCs8N1BH=Ppg^{N6_d*Z}w1>zB3baQuVmsdKo+z zBO^wtVyz|wS+#FqU!>f0$5xfLb5pQ~^f|o+))nogZau(F>hX{p`X;SPpeZ`}T6qyD zJpVWHILgLTjUFWWv9SXD>{U9$kT@uhAYBU>0yBD>)qQ=BOh`sNn;NR24GXMPcA>SB8HJ5H9cwe=cVu(syIM8q z;o5XY*1J!gz$P;;iTV%U49b%VkV_D$bmY?WBPy%+Grdiub#PZb!NSo#+v?zab8op7 zo3NJ=cd2-_&JKN#kQcY`;K@_1Wa>v7~~agR5p zDx$Vls>_Z%`n1#%h&qVNR>v0E#&f|kIS8wldGLi(bTPTc{nZ|pf6|?B1Ph%ER&#=@ zt0KvfqC}|2-kfV#CmtI$%}tInlFp2Z2J?(RlpLB8`}~)1&=6*5M0TQuaeqo~MufTT zv={vhk|P?&MYieIoATZ4mN7A?Xev!_iU(q-#p4^L=W*J#TgF}8aNwULG2J?IqTa%T zr{Xtjtdj9nlY&!clMYx~(ZR~FD;%o?Hs&yW zAW7y;H1Oz>u6oE(vW1hC4xX`voeSLBTgC&8a1i>eZ@3VTSl!?|h=+rm41!a*6XG=j zE|{41uUHs`MnEkECoJ7-F{T2lys^M$lguOm)KNN5YKM zT4++D)<5vc%2`9FJ=EA>aOChb11mLXQ>6KhxLSN1 z1W1uHo3W7OBQo^891K=cg6OsSa|)s6z|EkMBMuajFb36e#`51uD+ar*GlRscTmG@? z?@04>rdsRA?c)rub4Bz3?tkcqdSQ(sXP7+%;6_`fV#=xGGdWCG1xvNSI4ho&%Bijr z92Bb@A?(Jx3TN(oXE;(sky5-|)hS{qAtqhE!J(7gs#SCxmMv|xhS}$vDMa-jshl;{ zH@d%~n*`@UTdKvlMyAO9-R!$n?UT9EL~)!W45^N}Ep*f)Gj){Vz%mr+meJnfAxE7M zr3tHNbA^(zcHH?%-8M>E40FY>)~$xQX^}!CvapvXi=p7aPhDkgyw7w5%5c|-nGkp! zrah-0)1|U0c9O>Yg1#f+%E{n&HAV7tJ?3}Ss^NF_#9P&-^<$1TVoF9Pp1)N3@yl#s zAiZ;fhg2p6hFUdI^8bo_Jke_4PJ#btm@a<~U1ru7lO4z3?ZrzO&>JJeObnhuyq8eO zD2~y!bBwOEXvXw`6n)fUwvY2k@tr_Nkt*|CM(~Xxg=FdIA+@kpzW&;?@u{CHYl@c2 z{M3A;V?yb~JdH@=k$Q`Zo*6^jg6S6SjK)TLaXAb#`~K1riC85F+gI^DcvmC0TOgWI zIjGvm=Sh>1%ubV$lV#it*IY|kuJp>xNjzcMtm~(hEjI*)`Sx^CS`cH58_;*p%rz?> zVzN#m|9->*|Y5C8iLw55cRj4 zEHn_L(fQ=HRIa4iGQNk@*&z86=t}2IW9}eU(34W#=BkHkim{94;cUkvVJeRP!Hl8R zpPVepvZ$qyIgSX1UwE)_Pv20>AT|jAN`?SLc;+^xpHFBCVI!<`Gp*n%1bEF0Nk`;Eg+KX->sX+SJf} zbP4NWzUhw;uvNKfE_pjhTN%G3M@ z%@J8&<7qrCC68fM0ITeNH$oWAEZSIACS6;6vz~$eQM?SHwUo_FlQjN9y1D_1sXxx%vQ7G-WGTG-i=+Sl^bfl%u zr}Sgb&Kg*A1doNmO&L!QYecwIbV4mE`A%i|r6$i}Rnl{7F(N%%S2suxS8&#{s#_3$ zwy3%>IWdlVM6nT;QD^*Z7qyJOQoj*j9Px?cFM0t02STZWg=@dDs3tlV zwNQw1kx@~@_878i2#x2(cpqPDEQfMe2w_46iRKIn&>GGE zKPQbo9Fe5qgd>8C!Z-CklGLF(LNyr;*1X zS3cVwS2~wDw(W5nmh|E@XyAjs3Pp&S{kw^N& zZ;5@MZqIOX5B-b#)P9Y_HMzKeX^<5+OA%P!?C7KOKnW{zLKqwEp{(!8*J<)-`@8Ys zoCtxhJQ$KA#A*~PR#-;IN_r&X2p6H5hfNO!xqGW z16z6}F^B&h(KNjW>z%H;;FnVzlB(um-efQLv^?i^kt1B23k%k*YW2xy3EZH{a2=0% zOeix^3Gb70m1`k3dA8c4MX(=92Y>XYj2| z8z@aHodcn?vN;fnm7a7X=WtoIp_1*(f`L*sGw5McKo$EWeMu)dnNEGk$Xvg)OU?;V z>MN^i9Z*f#@#)znpicdClhNjCa<-c1YWOz)totkKky2x;bExw;?z5VdI<=N+ntgRB z2ro>+LjV%&SE6P+Fy@lM_z;sl*uT)P*&l3gld;r+ze+c||Ztfq?f4 zD7~eA&o0toG#O<=31*ahC6ata)G;fm=|dS-I@mg;q?@gCf`l%z7o;NZG=95w}seU8LS?YJ5?)L4DDX%ko z?7m`>*x>hs{druNPG-iLoPzFj5_CRfP*BesB{3IOb+YG_M?j_;wr| zP56__CcKv#tZ|TwT_Dhjb6#Sq5_nw!uXo4ejAN_puAFbp)crka(bSbO1C4~2J-C9V zz~m5|#w|S049?YsZrp$TVelf?vRa)|nq*~m)nNLOA#tcRhH9Lq<4_AV@{H#}>uyA- zQ06>Hnx@IKK^6P-&##=WboB!?(u9xTU@FFynIfve$|0og#^Dv?=}Fab9>JIEfL;*dod$t?n78CK=TvO9pKs z?hKt8?IpeS!Zi;*%<__KtFND;B*DM^3XTz)4g{Inwr=p=2F5$h2&6@8~`a<1EY=vofGNJ4&69rWV9qSIm=mrXv9LCM? zX#DAb9&t5A?)skU70T+@hFELyx(eTpq)iv5r0Pj~MmqG8Csj!UsW4t7NhcZ?FDH^! zVc3Z4Z#W~E6(>x~D|#rvfi{ejvwV(yVD-A8l~#rzx}bqra!OpOl$^#?tBcDGD#~PC zSY}&R3nLkZ>1BPK#8hH8^^qFSD|^EXQLEKTastH*U23PdfWrsLHgF@!hDZ~{(bWt~ zRRz_Wg2lRePr2t|o1vjBIXGlWSz|XPOe`l^MupXApWqD`Ym* zQY;8X$Iu7m67)WtAhgnnT|68>HiTwLB!BubPf?H7R+$hQs{0_l+M_zB1K|%QHuDQ| zTL0l0-5UYCb-ja1R;^$&CH~sCfs$jTgGiqDcu7R2$K!UGG=&CfOL3nYU8y>NV$^u& zgfSspgt82`^iCz*Ub7+t-6EI@8ov?b3#BG|jlD?Y6;VoFO%}rhn;h`zKZv#yFO%9s z91P)&M>|$kj-Gxax7Vf4&nOH$Jmh^wjaM?>346t~B{^j=ibhHB&Cb25jX0)!EB>*i z6KKpyp^gNj19cMHv-{(QQaxPwJ^CufFjI9B!;7=_b&r_#VsxYqN=Iv<#^De#e?U`s zeDW}pvHIWj>DM8y~ve9_*F(sZz4@2cb+_Fa|DReAw|^ z=M3|D5C%6W@Gc-HCOlFgcT~A#O_9J*RGr83R4R{AjaJurx_u6FQ~);-*+J0ihbF0| zf8W`zxM?Uk=MpRuo39beh$&wX)g%q-dT3@IXAAxLn3-VKmlt`I>qNM7%m2C#&6K<@#OxOH4i z*h7d&mE1$PSP|ZPSd1HRG(&l24okW5i$C$?sii*=2xA-nHds6~Yv^1{y;38<#*=wA zC2mQpr}pH^MsLWHXOA=1h`!Fs@2czpe%Fh*+y>I?11pSa|gbUEP*mMwY zug+7Brn;u4p3ba4FdeU}zSfARKND-<7B$SY!z#1|CTv#9kr4B|RFpA{=JdH>?{h{y z9*70>E+pm&n6FU!Y%TRt#u3If2o|=B=a5Wq;Jiuo2HtCO*=Lzm0QfUhYT7)t5U~_r zGEDZoROlNl|JRhdt@(e^NK=sgTW8rxwDU!-@njA~24#k&+qz2VG4KD(QM;T1w>N1N zlD>Q2QBE}n^?naApjmOG)2FgVKUVS8E~DOU#3kYyuE?vSAYo&X+4G6Pgjpjh=k34z zp>?m8vlgjgOIS=8Jsgq0ZIw1~>Uz#;u;IO_(2<2Ju~?_wf1NBi&d+Gvu!&Ujl$5BD z`j&DQJ1UGb6rNLNQJIX%oFhElS~?b+rpVt%o2};Risxy1LU3_0#TjFW3hiKOY-4yt z7xpw#w4iM0Oo&es9a~OT)O&>0F?qHq@#fc&MlPwxR${CgV2s{x#s(x&*|8Si44FgL z%%y$nbWxp0D&8Z8QK3?$+dYF>CW=%~Ut&&i6Vfzzg>H2EU|OoE=7DG~d(F!_8Vwwv z(j$^sBNJ6_#nP=+VSMNQqq@{L-;!)pN6JDAWX;hJZ>Vo=)EOn4RsvLo)hkN4;@gL8dd@&vKlrp7pUOMYn;Y6w&1!1}vmpQT zq8)K4RS$;5bYKauJ8`dG;DTn= z7?Cg5F>}~J#S^WHqI1**!@G|hCc(%!#V?~Y8F}QVid}v2&W5CZN8UR#NRW5l*N|K* zjYVi~_hgYHpUa}qcfDTUWjs<*7f5X9 zr#5XMh}w>nN5+HjHhQ9_y3(9aLM_LI2@t_i)b)MbXrC{HU~IM!G_h?$Gm@n)rzCW` z?Rwm-lwTwEaG76DGK50wjC|5l-Dxaq&SAEm=v{OS^(E)$CG>$N&N|Lh`SUj>1FNC~ z!k7f|Cn_Q5IC>`^rz3ELGPKn%G}XmY)%95My*cQ#oa*V9M$Ae)OQ{ia*2(wY|VM3p<6xF zL^Zx=Fx1a!`d)(|meTCeoXT@bYOhpk^-4~)7>48&hGtVnvkQ@~nX2*37S&o$Uz<-j zv&P#pm$PzM@%+^T=H_S}LcMzshH9?I$=)fYOz$y77{}OZ&pp$T9YxLnIodDlo$S`r zO6F-lxsr>%6pPdWUe}da_AutKdm@ll1u*(TRpAStcuE@*6Bpz3EbWo4k1SztNl}xl zCA_%GoKfHy#@p%pB~f`wt#+*a@C(CzxJboFjLX%Im41A0xqPNmX_pr}0WJkK3-+t} z0tfPI^qYCQto^NviJJYp;eJtnhbr_>;a6OM#p_Z#HXy%-zpEIX-^=B*uIh5RA-?Oc zVQ?!D5*7UPVs{Xs`5OH_`txEp{I$Bgf;Vr{8&zC-9(bw|`PN-s!SRk$R?_`@iu0wWF3=6^tuE`IZl0Sx zy1!U1SI?fUEYwf$-+u%CZeR_v+{4p7%XPena4#p_ZFk5|Mw>r$rI56?$sq^{JLt5GUkbD$FAI7#;6KS7OQ+v zQ;#z%mlsgG&X@M@K;%-bzcu@gK0>oiUJDup@`wNgIZy^(&^wuPdXy| zwJSw`8f7s6}MFbBJ-}mABgYlmYKhHnx=UUeUxIaGbhyQ-Y zd@VeZpV#A4{bs}O^vw_U^VOfDAU~82_uq5ziZ{MnD2`0eNCMTFvO z;bS8F$frzr`}vqVqY>Ktv-C3}{7b4PhQa66R?>XR_`d|Kbl#soY}{%7*to-d+I+HF zWc+`IA5{8{)rEo#zjcufX+DReUkl&H_e=1f4L^U6iFf`3=2OOJzuV7UgjHqKhF@m* zWrnx)1S0yM4QD_91Jt#Djp5fAe%C4%;rX?2Ap)zqW5wE^GIqD`Vmer`?C&;S>(@&W z-i?3z(Z2hHb*jfkH%yv3J`@I#+5;f6kBK6h-vP6$6N!WXtoxWXnsZA1y-iy7fR zDb(0>Tea6Cyh@Yx&(^&rz$bste6C$)CC%q>^lRbSjK3(j4bSdt@GHdiKl?d>KUDuI z|2_TVCVt!g+uucU3NL2-_ILYvnBislli^>M5PqKF*R4W+TDvV)-$fSdpLQ93?X;16 x0zMv(|7`iOcB%H!`rb&I|F@rR8txBnxzv+4#SkWMK(e&Vnok6xINk z+N{TYUuw1YzC~+W+uByGB4Aq+q9j~|0ABFY7Pb9)Vys262zWW)&ok%jB@0M@zxVgo z&z77sGtWHp%rnnC^E}VYnQgpynXB04a+UDUvRtl}JmtTT`g@2!ZkOwV*aa>gF4xye zT$_%FU3Kcc?T5`9_|12(YJKj$Pj@XoZp$&>DqA1B?vK6uPF%d=CtH7b+0={f{_926 z({FnC`2`(wb{={1n$`DRe%ZIa`~C?{Z%%%4{C8KLe8WE1vH84b4}Y`uJN<5(k1tL_Za^#)jVUopv znWw8CPV)aRzg$9bGoY$c_K|&YvZb!PHcu~hEqKf2>N~!~)g+mZmAJMK=ee-N_2?v? z@OQz9JiAINs@OG`WZ{3UM;E&)$*h`C?5eRzw?SHqTsxunVE>&Izj{fW!n3ag zNC=X?i%{hK7lh1{7F~5^{7RSW?|%R}I9*TjY#E9;mg@zN=g=qfbu< z_Qs1`=GQ%p91{N24E&XX|D#(Dg}*2Rf7>G9pF1S{ zR-{eQ|G2>4e@OUKmpJgz1n?!_I3zt8_?xA_FWz=2_^oin=BL0vd`S3*GVm#(KYmE~ zMH%>0grAW^!cWP-A0zN%4+%de10NRnztj6KmWQ|Cxa#i@g#ITF1&^RT#&oD&XQuK& zbMjU6H3#3f)%c@4zqtLaS32-pZl?YF4+;NJ2EJS1uR0|Bq73};LeFW3grAauAGitl zl0(9e$-r+B_!k!+s=seFXZQ^p&4&*O|3n5pCGhb>!rz^N-z)S)4hcUy1OK4#bL=7E zkIle;Tj2k`=n(xyUOE9pURD8Gy^iz}(t1Z%e3bc%{Mu&CLFT?Fu$+87bVWY@uFA(? ztHuI-wXW$2}2R?F0_}Llwwi|#SdnkCB{~%^O2NvdaH8(y*8GNq_z0c(X6^>6xpYJQK zD`9P&@M&#{%UIK(J>B1JdL!D3K4T4kUyG#XR@8LATOLVWT2U2A#VRTz#`+H;)rd3g}R=KFqiCYX(Gy?e(ny|~BfPQF>3^4%|)$>%~t z7DwpP4YzJJ2eTB~>GFcKzaZ@$n$A@hd@-YGUetQa22uEye;=yz(#o->oO)uYG z0IwI6?-*LXd}Tp;SuUM?uTp48&#kDkmMte=H58k%1ISPsA9 zU#OtGZ)o}Q&VqD*LHd!Q>E`AFe62&vmv;?KH!m!JHw(%a6qN57TE4u>J!E{Amk&)h z&nu|Ev!H(EQ26EIY1mcl@edWGgW0s2KUKO>6f?GB)yX&oqsDrxX~~D0-(@s;wH2YV z54F+7sX5-xsb#v+gA`Zgq!|1$V_7To*dk}Q=gs0*_N?JVHtQq`6*8-5<%O+G*Mg7kudwD_6%@OuizdEk0mz${Ir+f z{{^}FioPWU>4gR9P>x;&&k8-HKT8VI{(|~V1?kR$^p1kGS&*(Nz+Y964i=;zE2zJ+ zAic97-BeKDTaa!pNVgW$?=MJ8G$4Px=M|LexwJig&iYUwT(&$TzoO5x<43xj`gU6B zi;&V6YjXOc&sydML^N1P)J1Bl-_~6FufO^2H=%g4l9#2Q_WXh@PdPY?YnHyMEIms3 zpi~_FlUqn_bQ=rGmt`knUU|r_9}?Ia5Y?cKG8#W-xTDtcA?pY0eUr_MEIpzW^7wNA z{k}|=y#GMOA3%RE>!fN%h1>H>x{=r3f%La5iT|PgKCnKPG2c(W%P2dXvmc6QHvekm zy$b$SDW*Fk;u=~*DSzPi*Z($;RsWjwJPf(9mlo~vB?bK7k>!V7?k^};@|0Vq@=)6R zly2Eo?E2+H>o*05rgP{WdRV`5_8;=$Kd|s->G$T~3-ozY^Ux__(!~e=J9v@|Q=F=M-%3meX+45bQ-}k_L zTty2z*`e2JTA1A9)_&TBol%(|-=e%GVq9DjOV9R(lb@6%iW`kFFvS=X!-c36{~>58 z`-qw~J%X=>;s&E1ha$YPL|B8(rSH;4Us|E-QYUO|?ZvSurYOIr)8AZuyb|5U04jwf zzdAiUK=n-fjmDM+V>|T}l{S{Ju}#_)lJ;-cZXJF*?c-2GYeRI@N7tj z-3P{T@cJ+OFY9moU)Ep$zpTIRe_4OS|C9Q$bflswCYEYL^*Ur{Zbef@W)9AKHD9vh zC*EsQ4v$n9nJK8A8!u@sB=8~C?m>onD14taB9fa=>%tYObuq z1YBungI_04^W(5IH5pU6bNNl=2n1gI3({Z)n~vj%P$At_w(^dy1-uREJ%y}oNXWY#|qMqQz3erHTTl{DE+PmUx zIL#W~{Z@H+WdF!DJ^1UdMmnRz)`q>Vh_N+nbQ5Ij440@EZ)eyO?j9%yV0cS!*z0!p zx@C3I=Z|2038#vubw4hSZ1vG8m8B?sl_^HSb=@bcdVC~rP^d83S1=-}l)m-67j%b${R z*B6uveLF5bDE)tu`dtSvZxs5MAH4j|!zgb(czJ{1Z$5bW1i>FVc=`E4fA9e1$=m#{ z#Ber6wFkNgRn@u@rF!x@$B~I=x|@FlC!=cxCBHvYjjX9t29AApf;2q}_??7RA*Qa=XdLFwJFh``A?; z)xCr1E`Pei+6rA$Z#~s)BICctCKn~f#f-2wHhGFSQ5Iq68vkU9KQ_6mv7<}7AE?}a z`Sk4(rcBPK&r*@GtYyyDvl{Y%P28v)aoIiWAtI5#vF$WD@S7c$m1};Zjsfqz5UC03 z#$Mfc4L02@WITK!EF~Cfyy9@mV530Tbp`Aa-`c@Z-G$lSVhk7{$g3I(FZ5zK=a{Bq3Hg(UoaoKa`Qpj3n%vkZyE0N6~Qm*b&E0+3L58k znXBN5!w-ACjT!6o-Y$#(T9)ZnSL<0tuEfVWNET&~^twgmrFGW#Xi02-cQhUKnm6a; z$?%}pEy@!0Ife(54Y~G;@LN8Zr(s#^IFfvg44GOGn?tEBua9bX3iy zqh!-_pLy?WrDpOqjILB8YNbY|y68v8my>?#)`NMetbO%cA2JmJ*HCXLTmC004<5Yy z1u1tOy!>cp!h#4qrB@2%1bF^_q<-rcluP~QgO{Hu^o0&y{*;sl4_=-Y z{H}wSKQMyw9Zd)A|Ieko>)_?jOZ&?YP~P_7LWJD1G^@I+vu!c&c8<%nQaVg8eZOkee zsN7*$nQ4=?7s4w;f}yQjnlk^r9G@vU$SZ0lf4$M3E^0}+(EvQcMaGm8CO`(4$S3Pl z%3g^jwQ(cXifZ%8zu9I2$$apQtXo6UV%-!8kx9FPB=Z4Mvr0sJ$f7?&@~O`vu}Djl zDw=A;MT;EERW-k{B{e#~`bO#xNn;=k!LO%1BgU8Hw2s7X%438MgLBTmK&Eg_)Yz$~ zudi4l#*MMp+<=-Dd!jOCY!U-UA~=d|wxyF?S7ZiXl0|D3fR?6r!_S&4F2Bs#5-!jW z;npSNT~Uk?V;@#Y;!De7hHS-PG;K1sNK3|s)m1Vm#*yYr7zwSnk9WmiFVGnI9n6le zE<2#U&l-zdm?+A&NR8Cfx>p-rWFCG47}FmAc?5Aelc@Hr9UG&d{+|w5{}oma>YEf6 z)+hT-$yUlAC}fkYKPyOVp6+~FWyA{FYdm0k*GPN$RN7-Le|O{d(!kH{2DFYpo;!32 z(T%8A?^qWvRcsviHip3v5(@0s5xQ}aSD2hQsYR^^+40zHhCd%V9u45ijECloQpU&~ zsH9NngSOjZ!X^xuZtuW}^hC0=|LMkP+=ZBNu0Py*8)JKdZcOotP$Z62?dx?-{sguw zyzAGEkSdD5OFC+F^+b&kQYkUM1!CJp3SkrRuXVc7Cph9CG^V@)m~Af4K5;mAolTv{ z->Y$tc}LXc3hnN7$9=2)3f%_tGE5iYe%6A|L$^P^BZuNUA%`#M;Pd@w_~6_*GvFL< z1+Kws%Km!)>n_*s9wDw=A=_+r%@N{+?bz(HeC88Jw{7|2IPN4)yb}x74W`$c7))-c z(Z?ss!?p8*cMfYL>^V3LPBj`X$&Z^6DQ&5n7rf(2y>3nXQos@y>W1zQTit4)jwRt$ z+&@gO3;Pp3gjLR_Q8wL(sbz zr&KpQ?Kt{!ChvHO%FZ)3sBo^;j}dyf*<^!U>ws)DHdy_GYeAK_i(BCT=&3s@g5e9$ zYfZZGp>7PABNdYEyF*p(#1uUhs)?mfzYt!p$C_#JiSg2F7}~$YgjDdl55Z_ zVASy|v42fpdA|LhN3Wgkjh|{abUf%(3{f{Wn{!4<%}Xnq#jAtRc*R29xC1rML7DL7 zJ?7%kF4r>VulTn@yt3z->^0yO%yHe=**eqfPF+zZN8qHFv`;Ts4ysuo?x43btIU5i z9jdoW=a zE}guh$NX=mIDO3jFRPEu57pQQo*`DvDr5!j3Gy>Akc<0JKMzyuO@@8F&)0917oB|KA{k7FmYlhncgfnTas; znWynxM6P;H351jx-daD$uDe%jpNP!JfYz82wbxc$h|9fm_Zqj>F78L`Tb~PwRh^Q} z|6kROS>D!fpk$9SrkAxWtDSW1~i2MCNMHm|bG`x3)>~ zOVq0xV3(A~2O>2$k571waIkh-$(_Yi(^E73+QbkNHWLNS^dGO+ZI90|E-0y;6HH7s zU)&>v&tM}v?Cm!z+>tZ)B({ZDxFek+&Z1~9@LEmYWrKf~S=@w3-62yuUNMhXKn8!I zr>28>dQi0K2%M8|mEh3egnwuyu2o{lncHHZN%4Qt zZy|q6Z>Zc&@XFHzgeZ>s^}K{S>)+;)s(+KeLojmg`)SY80I1E{dipjUQLUZ^m>U>9r*!~MIXsF&TgV-E1_Qi|=Y+(^3 zB}GJnN@KOCdeM(cfS3`s0I@zV9)Ih)N<-rL*)liz4Qb z^X5ilEy^Qk1@_@WQz1T1#4>8U+-PWs8ju{p2wR+Sn95G781viI)SPjM`F$?RW_!$d zL75ciW24yfN7)wBhS(N^XDH3IT|sI}_R&&~!51}9iZ8?rmu+^GsdSg3;G?Lq&io@w zS9aDXF_xosmo3mb{-JDJS^i`plx{JXH<@p->2I%($>*wMnuIS4Uz$7ID*9YmJ$}xD z*k`gCU0Xj8BduSHm+uli5i#D186RlvcL~#z^(!P_YZp(rwSKiqbj(*{bt$Vb#={a( z(7KFTXszc50Ju}LjXLOd` z~YF-T1`Jy2Zge&W)xn_eaxr`9)7fY8M9+=ZNH-@X`KyO3u54ou=T*DfrWk4m;odjXVBO?9}V-F=E0&O0iqqQ>WH89b5X0*C@V5LhWAnl1%e~N+oiB4Rdk15x!-rn3ktdnbipGPsAFVrD>oA?6tFjjohRzv8 zS(`4;*)q00#4d(CN{aS0Cp>K1^Me?tm5dJ+brZ8*r6c8f5|5gtsmwn*jgbK^&YV{%GA=t>ZBnFY=?t<=#Yzp1Oj54vW<; z^d^SssU_aQQL)-;g8r$xK?-k&zrz}9s}g5qP;DDLUN_KNuc#I#$f`fYe}$NwI!pO= zV_d9ukw5WORe!Xex|pmDEa0=s2kwIs)^sv9OTQDN=ue+47K#hI^b~Hj01jTS6NnIb z#viV8K^*qfL_w$<^D=!%)#04!Dc!Fg#4QXua5jq<^)OwBK9yZpF7$bY zJ{`drucu}JCz!=P-KN?Z-TIAkcY>)MhE3(zy%J!~q!Nd(og&R|feJD=nM!GZL0d7k zoC(?)E{84v!VtA-EyjzPLdy=YP`DKN=iB6uw5bP50!-2p!}A5?A7+!!WS8#v2aD;2 zp1O$Y(t(jmR1U`$M{Xe=Qy7Nua)eDLuKOO{xWpT)3wgDU?=znU&&q&|E9lT`;@21> z3;RQt?Cu;vch*jqE}n*dfjw?VstEZg@15l3NWyD!||3V4~rgMMd z5&>b#DS=jcSjI`GrcJU@jJxekXffz-+n6dEtfW}4^Cm{cQm$MshdM+4T(X!r zhw5Kc40PQqgXO&14gR9W+mnp$H)SW8>NOF$d?B4> z_qbHrENoAgfk07U#H&YU8tmWEVbW2V#Y}AY>Y-1AC3Y{76PfK27nH#pua=AHQ5kMFw)Mmo3`I&;c@E9G_uE3oWjh(rUh zft|1dXI2%tl6x*poES+@#lPJ|EXCrx74wJB%5DMXX?2&XZ3X7#>eV|NyEDGkZa19l zEz#>f(>ktVbAY`UL?D!RECCHNFXPdMFJ)YNvK7VaRdJjTPZFwn1|9p)I_`Ly%W_b+ zj#~#ne)?Ah=?90TZT{x=JNJf-oz?4%*I<{fJ??whU4o|R6J8PQfD>oX?}_xALH0}c zFA-^mxLWduOTN)(1&)R!sYU)V`nNn+fFxq9mAJ)bTNR^zZ3i=Iz3H$BxHEt|5D=Yu z(ji)fGV(Tg)E-ZjgizA>pt8a9?mr+1TbK==4U8oI>w)%o&U!{hZSA)#jq%Uh>Z!^E z5*!X5y1j1NI9uW)s(m|E4`ZlU%}|DL1iE**;?;n6uBM zwILgES@^{T@Rbl|9=kuhLtu(awSJR0=OFdfg2x-)0>W>wz0<}`cH))n2X6kk?V#T zOI=kF3gb*qGylqpqWAX7&MY!tISCpG=}2e?pVza4devK34SWWWvE2$R_(~BxOvncG zB#8lq!mGv?6@j%&)lhNN7p%aw#|x)67^YcW3)BCpb?gD}s$*FLVyP!977Aro({Q@+ z*xvSv$4CaH$k=JlB$xJu6m}a}emo7rMIk4jjvX|RrGqd)NGIqacvMU(26tAf&SF#I z!b6r7_(x@irk~abO^F$XT}8UP#UzL&T&bzvDvv7FWuZd2+0u}zm?kaQEQb~=(4r_< z^$m#BBE;x6nL21CBoVR#H`7+kushm8q}m=N?f#xf&BF?b)^Rr#b;I}cV6h8kURBX# z1$I%3?`p&gB5u>9)cz~vS$*{!z;WF=TM?L{Ly@UI#>kLKM??-FPf-urhbt~XNUXpr zuxKl6k?H6PYmtqeb|Plja-tL7Iyq`=Zs@S$U)Iz2+dZB9vUsA}ijq@1hw15C+-$K8 z(~|@4#CvO}SrB>pF`pDeu_z3+dfU~^XO>EAMCww`r1=dY~I9oAhzeg0-J zo^sZu@lj`;951yti~rEEZt1%etSvbOOtv7i1EL#_wC5%O`hX*t&PrlG3hbFoG(HrY z{~@zAoc!Fa-77~(H~?=24*#-@Oc%?j)_$RQ3QxPZ;z(Z_8{t{$ zObE$CAYb|t@;%?AGyip$lC2laqO%TI8PhLQ*O|3@tiX4vZtR5i(~#u`#DQ)1SMK*U=AA;|0$1bw_F{MkPK@-{xM4c++dX z_UYC?elx0to@dsWrFGObUJxF^tNoP6&yLGe9w;$=KP3hWtiZz+w!+wB-gp{4!?2a9 z8DUy4r%}~%IDMWwuig95?NifbUuE}KqQJyw*ub>dxsTEUs}<`nb4?lNmbOij`Ki^>FnE?Xn4VagUosuI1+=C8EMW$ zsAbNz$=Ia>}0TK5EgHHar&Uyt9j< zJDj3aV;?yiBWxd}1HY6^2DjP#?N@|L5A?Vr)K+EwjE7sa)e+%) zjSQfsbe>ilgAx3D%<3W|T8zcfxH8n4=xSMZ>g(EyhA#FjI&Ktjqwx`z2d(`|650xM zkk)>Io#ITy+UyHR&9mWPVaG~ z;p>{#y_Y6NIGq{vwC+7NejKY2xlyg5A5QC&*VD&Ns^|6DJNPGk@|sX*)nJ(%9kS`} z9Isozu-+5*8-CWEi3{3aPh1?4wLvsR&{=QN4V;%(<^*yV6uFC+N7-Eoyg#wX71CDh z#*1AE=UiF?%6XHybgcx{Hk;4I-QyIq7)joZ!k8!WztH^%K~8MpCj|AfK&alzo-n;E=X zsh5$66G%9NcZ`jX!8_c31GA4ed+@GNgC|n+2X`zTCFF%@$lwtIA0eanIz|5pvTs{+Kjs1JPJJSafPvEdApO(U@@Ve{(UW!L*k6-Zo3)q2cd#?#ME@})=n~i%JAoF*E zgF^(vtGfn%f)W!ybsuHQ*Wp_J6?}d^|ikis}odDxXqcYi+XAp>s6e?haR002H}+KAk2K< z83ZeE!jZTJwP$M`vZ#P6G0t?4mEq(dW7_vss#pOpFrjuU!8sRSyUN*pM3HOY32G>p zJFjDo+(wP((ANyETL!l;y@ZdmyX_&)$>>GQ5E)c4SP?)a{EYArm-nAEq}(;yaIC?) zym9_2#m+cCdpvz+oDa{8v(0HM@Q{LGsL#%}A~GPboahJ?(;@The`XnSq;yT36yYn} z^OgQZaqmch8<&IIHJ%7S=h=gYgJZgs?Rzha;}yz~C^>I@DaGCj;o@vXheUzn)5YrW z{XkerDAVWvK(!!gJ>~iJd&NLk04*JTfgCU83h?8d0E#a3{gb!BuQJQ-IEG5;z-L(Z z_5@p;Bcmxt#xN^TL@Jg#TY|;5Fvi5^l(EC%?38EQb0W#%%P&Vk<>@Cv@jQ9=$nvq=FbQuw9-<=5HhpyyB zYd;sxiV_x+FmG_GWe_rh^CeZLtVubBVGqt|`wa#T=Z$fhDC>)iP0cUAaBQ{<=VcFC z#)gAZnX&1Td^I*J>^D%0Nho7;yo^nSod5WO;TgD{<#X5g4X9VfFMf3l z)Qhg*Z~5Yj)DXpQkl~3$tuAr#rpgw)09LcPda&{N@}b7()UJWc#aPH6458wkL5;7$ z@&_Z_T624%UdDn^WOO;d$^c`8#Ggr@k4d`0d>rF}!wx4 z&#s# zcjisrJVrtvJG#il*{!tia9*sy0O#jvYl^}i%V76%P%Q1MR}j5Gn5CGN8GL#3(9v;f zzdu?3={vLEjlRDsuID?gC1DX7dyVS6i`-b?%2(H2uNPG&b2mhyewvjPLw5*Gm3^Us`A&xM*=C6h%?*%p_7R@sq72Ie+d2jZxBk* zcB=B4_wpS>J#Y3b(i*m#+a41p@l9Ibbn?|V?S1CCXz$kg`A~!?)IZ0QvmMF$eVfUM zpUm4Mf;E1eEnRX!i7j1IY!qgx(*B#RKpSxT4V%&zj4F=J@nuCcwC`>q?;nx7R!NPOL3a{^&p@J-E+Sl zgqZ-9?74qbnhWfxm3hDIDmH1*dm|Kk*h7DIJgY1lao?i7^m$%(*jMt#(#w5b6NT}c zWye_3*a2q}(b#XtsV%=rc7B><(^-&YH=RO~vz#2UcsO7r(F)`d}!T#THMM{Jgo@QNQCpU4Calfje4m!Nc7NBsUMg{fd zttxYVfF{nmKupH#Wh^`FhL4gRz#G&Gr*;6p3;sxF{TVxmy3CcbS-G-rP$muj%?@W0 zl2u#&C+hQiwH0NRxS(+j*Vo?k?_+?MniNGk&#D}>58|Cdn=wKG3(z7l+^U5V7g(Z9 zCF!w5XLzpl^f4FZSd2XxGZWc)R<9el4Lc{B`?!1H+WfTWOU&W($zy7dM*W6gL;W4} z3Y!pL_)!Om_oZ3f%o8|mgcHW`YD8K-#%)fYH_iMmpOi$>=VcGkFrGgXv$Zgc_a%5_ z>$YkUsHy`@W)ywpFk7JZm>;71(R6pEXL=bYF`!-MS5#XwRa?$9UW`MqUt6oel4*;% zh~ET;tlTV-Wb=^u+wI9Td%Ie4sKY)unH)9ju+;oy;!;bzo72Ef?-{E-Vm`AgJy#M; z&M~Upc(9F;lv$bJgRsqia9e>{6v(-b2y0s&x&l#(lWc{70&RFWaS1xAZc~rsH77G<4riq#uG4X#& zM!^M)A|J{6`fUA2RsB(!`js3@@wl9p|0!-yNPRO|nW2E<@EZ_j_&&6ubgcnlU&r?a zY2QQ{C+oPb5>CF>ze>65PRU+iXa9=qUeB{+!!>28$W@-=!DN`hGg<$j)KmOeZr8h+ zdMxN#o$r;`i@D6Mdh5VfcV})mN!H&i@OMc&RpV6iVb#usWQ&_n{vqQ;RrqF@O7#dyc-9 z=nMxg%n>S9!#*>!0u$_Wpuz?owa<@fXwLbS=n!Sd&qmFq(+gn>AsyIjUc(NHd^&As zu2y+({~CCD6mW7woqxIs#OBX_&Np;>(D&xI!6@54UnhID3&k#*0R42usbMz1PRG;f zWrv|0I-j~}B)cQ_b`}#VQuCA$fLEIiw2ER`{GyD7D7tiDsbqYO45v2R2-)u$WIN$B z*-rR=%tjq6#%oi(0W4~~(#SGvb=8b@C-%hVtC)%?O8wbSCbCa*4rjH~6W+nQa^lxh zI2q;j5)K8G zPw9-5Vm7ZSO1`O)9zUhCq;wEdX^Pva0Yq6T!C>N%jdeYXzN@$G6HJRd4V|Y9mQ3qB ztyG4ALk{+akNH-W9j!?(gi+y@vQnCx$g^KL1R?n>IT!y`sqPxJjwvms*0ET+LjW?% z85=6Hm|>VW``3SPv!C%GoRCv9&42}E6A4BA6>&Awfok>ebz(@G7cy|H4meqWX7+G| z>Ceb%x4}B|<$p6aVu3sdLY1jN2~iQxHCN7)QKYJjI;_pdpif2wU%zXt61Tw1;#~@) zS@9iPNC1!2u&Trtsjg*q2PFby_1Zf}^Ec;ha2_>IrpZQDayF09&HF6_e{kiS5F5gO z7UjZz7uj+KeGF0J&j&U2#j$Suq;HbT6*|=l?_TSUdqv0>$`)F>Lu5E=d?fd&A-ZQ( ziFv=tCdcY#PfTl9hb3~GTJ35H*~6$v)%3Lewkq#NF_je)RGn4gmw@_$I_#bNDg(~u$h(KC_iJ!Y&kGluITD`T`b21RJP9O_A{)O$c zm6z#%l6XQJl~D2?bKYvp0(|&d&uFl_xjfObq%9XIxmQp%hLZ5?qJ{+auK_?DwIKUY zlUO}YCO>cWsI(E=tH&oCeax;Nx%+E~O#xN6Azoqo&GVSrw%@!6<;g@ex3X#&8&VTa z5^S%>&%o3>sh{2^PHe4zi8!JwQ*}bN>J_4Qe@H#^DRvJH-!8x)&rdZWk1H6}^`<-2 z(_ZnN^KFLbb@8h?ACAd2+PpISSk~VhI~#atpO_7qxbz)!fFye2^xX7cu!5(!CwIRDAm`UDfAkFzo^1z?Lw*f zaJDctp-a8{zAcZ`gkQ@;E!q+|UCERI{7$>d(432L=E5iQ82(;lJ$sS2igO z+j+!gBVgjQHY#Ea_X7uVpX3;xvjtwZ2|cITH7CeJxSFw_29A6tC*1I7V8+mNHvS;z zMdZ-IV~WsMQDb|RBs6k9&3M<;f0~hp)rhqDJmVc+c@D5^J==-(v2U}DOC>f-*e~r{ z$HOd|BQ@G9b_&8YUOQ9{gFK}$rC<*Q#$UNBbe^PeW8yx8=G1vcC+G`xnS&?UXm7xB; z72By;-KFeYdKyAsJwuQ&)vgLS^2M8}ns%PD;F^!%&>QzU1%V$@!Juvg=vW0me$hjj z;3b7q;=2z8L$ylL%%*P>+Z5>^E02`}CqWLJa6;y7xsSw_5NG^z-&c7q-bjd2zAN2F z{7KEeJ*Jv{VG78Im#Ss?6~ZLM^Cjw;zh#F|3S36~viGm?Rh{wbGD;1Z{B`~DsgQaO z@0^e1XKpE2Bn7ooy__*j&+uD^3Gn5TCwt&&-(d>&-+O?STQ8K1a}}-gmQml?_B?;s zKvlV3$2UTdbaXk6Lwq7k-&7=Oxm+5iLq05q@Bu68UDx~*CF;d9~~GuE#Oa~L)=Ph z2SwSTVfoq-&#qaQA4xVmN06IkAL&s;(hT4SqHc24mdLf(+5c>R^)G<9`N}s^afZ9? zs*2N?T@oL_dWJA2Ulm;!*VXqE8&~4!&RE46`H*5Fv@F;z{|oIKbnXm;q1-b@VDTF`1E!bauVtkR&%S@9wbe$cNS;Eqj1)Sf(1#e~zfj8wT&)3NjH19L)P=6swsMp&6N}75fuT@(z zP5VSqa-ZYuW;sc))jm4Tw!aOv#fB>7pfET@Upl}Xo#kg)RA5PKm;JEn*OeoXtp7Lq zt5}O{Pu36e=1vqRHw4w~Seq>bb*GSU3GiHOL9aLS?>XZot{>7_aaKxCWyz6MOO_i3 zg=R2E+r>YC>QjWC)al<4eVMAiorn4P3#vjtrgynlq)o>~%85rrTS%2LcrptO6R$F)5EQmqNyt?svx~-_p`ktmK{%w+8un zi#_EMT zSBHAJMaNm2x%FT%485I$q4ozL>J}Y6d8Hq@@l;AQI>o2@Y0p!#4yLfWiZ=20zZ!?Xl|)H%EbRaJ=~&iJ{S3%}fWT#RFnjsGyzXo4+|mauZT%j9Fx<#BlRhN}2&^~a}r_gfdE-6TO%m6Q{ zOHMz30Hq3SEY#ZafRFkcDtD}V#to;nNKdEdN2JY7UJ$hfvm6*}zW13*jDTOO$RB+7 z9+52DkFjyJ);Ckt#m^dClicgR_3P&8HieZPuXDM7+IOxX9sz|ACsfat^xjWYFHVBX z)h*gho)b$Dt7MRH!Me=XMu-s6*K%$h zS=o3oU&z#YUbw-sIPE##=ekje?Vm{N#p~N@n;P(puUJX+3$So`Vs32m8t$GZgg&cW zFYf1+k3h>>W9hQ>dTMM%v~CL@MxI+1tLt7=%553N-Lbl^MYt1dDhL0@_;qYmk}@{= zjSAPDA~U7PHmNoE+CZuf|K- zwGU=*&& z449H1+)PXVc>9rEA8)s~oZ|Zc2Ld{*px>X2%swvzGWPb#2Ok;(-UOWtE3w;QEa#1ik?-&CHn>b+O|~MH(GUK z{(C~V?^D}qlH&l6OOABh;U&&U+V^+Lt-zNkN=}kazo^nj+n~Wq2KvCB5C5bp-#zGd z8J?QoLnnM9%Lr_fg07Tj5}B!DrOoD1H@ph(gWz0AKj%Ftnwl!1qkpEp8l8a!1^DMG z{NJTK$-PyhlOwIc(^g#tka;(M2H#EAS@C~mu^YZifp>m3(HPrjcpm;8=~RhKxDgbo zM%d$M1fgSVdXABN*o$B1dr2%Hcml|MYD|3_$gu)H5dsb0pL50E5e!*5G(6iVQsJQZ z1pA}D9|~;Rm$a+M&*U7Z#xw0}CCdt|e^;Trj)Yhje6+4hBWd52c{P3lP)63%p3N?i ztK6Lg<-&(Q?-dEiXrqC}B%y2Qe|h(dCEw#pnz0oz(b0|7ug5S3%VU%K8?`Chu8tW; z9p?vUW8JpJ<66RPZ@G3hrhmlE{8kuSiQL4_m_c3SO=4s6OEWT;u%c^|@4Dla@@E`& zOT1J}K3llVmmz4-WB!tdwxUaz%e~$G@*I$@@J5Wd2ju?e%7Jn!IctaWJ*IOXAD;+w zeIn97ZJ$WL)-F$d$!BbxX{z?Wc?P2`Dh)_Q`GC}CLtu4*;)4tbXy7}QArIwF3p8_Y~LCRO#qwzB-zmf7@&-EZhUrN9nh4htA`1Xmf zL(Vbp7lf#UWDPM!tZDOBMP?jDymqxzzy#&2GsiK{1{nu-Ezc@YDIN_LF>&pMPR=8< z1Fpjhx%SSU$Z}tZTv43WO;w?L_8OkssLzhN&#oMz3ddfozLck#0`m_aJDdq7>sLPq z(TOw6UV3J(VWlvShuq|G50Ni=bw0FKZw=kE*}Rjt-P@0RJ^8lVTn99F}NVK`O%EwIf8A1yz;w$s>bPPjPR8w^ryJP?i5NKHAT^7mvjad~yx?Ioaoqq&&akb#OR&%XwKT%UC3> ze^R&ejxPzVv>q{p%O#n%wbgHrSX|mLvYWa$P^H)N%p>-!kEDDndDT|GGgeDVcz}$b zk?{DnM_v_tn1@)2r5+4dmz$18U&zU`m3k}bWKqvdWy`IZ> zVZb;te+h5l-95p0O|mc8>xqyQ6D;MK!rMxhtGGWhX?ujrO^FeJsba@jI}*O6ZxW@U z)P(UoWV9<~@^YvIaq+7&Yf2%YQ>(jbJ?{}Ek)Nnb9``^ujoNX6^L2L6L+Bl%eqFR^ zyt)eKgSF)Hf5-+{yu{e>!tsP)nB?*qC3ac5ac6jCKRU!*%(k2)t-#*DGaHPbn1Hd% zx}8F76_y|?@Q###=PW&WRYeV@=^d(60m*$-u%5H>vT<%o0m(|iDKA0U$@-l_-O(q7 z2yK&|>X2rH5S1BT`F&|M*@b)1(J6GkF-DrVYoG*Vqqd11zYx#4A}ChiYv5#8Nw!D9 zhql+#4LiAAMc#F3be1$qYauIeHm~S{GE7TvS}!*LtmZ!-rNS(tnZh%^@meGE21yEX z&G387h9DFD(O)nq&z$7DQbawmXVada z5M{AtN2wpq0{rY=(GT@M<{>wE6w7#H3_dJ9O!jaM-rp7t*t&?euNYeQR=e)Pg1QS1 zQa5LVx)6CWku1tb=uuww9K+({kn%FMtJwztoqTmJ+7e3b;~$;ah1Dn z_3ie|{v-Ae_XVN}p8x!&qURpG>!$0Ab_f8R*S`52I)*RpqsccXMw0`<=;U?L$y*wW zyR!g41HeJ2ed}yzv~uGG&Qku(4)W*X5P!bb{QPH2NAPb}kUtlN_;Y15`S-Mfi06IF z?dK1>?B_!}U&?=2-oesRY zqtSRZ+CAu*Zv6O%Atgqyg{`-x)AyFjwmxr5?;YX1kr&M7Id9~JvU$!MdCl28=k2Cg zX{#WMjgSYa<#tLQq`K^sdR*_`@%-mYUC-~iwWnnn9N3><@4&BjgkSFnzux&z`1Ov% zuXh}Nz2orfo&Wdzy6S-Zy7Qg=`6YQlnPToWmy@)uHy@Zt>$-W1Bfca7+^X2-YodC2VG1@oT`gJ5*L$dw(k}UwY+^oJw zRx?ezX||u2i-Y|6S{2vTjNsoaKYuO?^5@Db^3M(NXLgf-H4E563Zfx_YZAC-fm?{2 z3U1_79>}TvP!uh`9~p&|H+R}HS|T#qndP~Vk4(u_hevj5nVph{NSK|H2dT&GlsrV< z?36r6_1h_VklJCV)Z==$C=^#uR^Rgb(lK^LS(S?W_}Q4zJGt`AA5EJ6-QAk(KH-_Z zxl{8~Rx(-}?IpCAMJR42le<73YF%MjozWR4G0s+U#J}) zZI*&2Dc~o|*OA#oW=JxtB$MBO=jXGJj|L^juldVHCw?awS(BD(mECr`CGjJ2^1$!F z3dKU6tX?~T)Z%;!>@N*#qeoI2HPpShm^imRUF=#W+il8b7({-Zwf~VfaB*k8oAnbg z&NBBQapVH~sFfc`wr_Zdv$(pYT0~E7wmA9SdKSO5_c6a(g3ov@Bd;Q3@uRr&Tu)y} zyuQyo55&YL!fU5_-O&`DlElPQzz%Zrx|@54*G};&yT|ccCaNGB)*eCDA@TIY;Rr43$ zutVCeQKl)ka1>)73Kk5xg<{@|H^^@d;XB7ty7HZM$9bLup@6)aZ0@5`eFuKEX?BjH zbI>!-;1E#=19Kz+-8nsU^fq?itUu*Y3_E!dxqv zKK+FYF*M}XD*4f%-907o3J%Vca)k3-?a%F}?D%exYsx+Mp0blCF`%$9<y!DYc=o5^ zZSOXuX80QzkDyo|CESGPty>FftGl6Y{i4Ca5n;*)i&Inl@*`)*$p`H=srMIAUbeC~=fZB|!HSRjBXBMdri1_e3~@k=6IVm=e^0DMa{<(7)^QT!qm6}Tq53^^XbwLfYu9cT<_tl4oS zU(=3XXtL=c$*O8gq(@ITZPzLll^f8S3%RXkXZ3FLObnQaF%n0tK@LIxneoO_EW3bd zYD_IRHb?M;CvX}!$jj2P9qYoz9QHXD zk!f{h;PdP(`q@1NVR zn!mFZ%w#o_#k_1?W+xYFqm`|pr`jZkhOTETf_Ax)s$nM2pX&1)u85@?ejhU$ZX*3l z?w4B1vo~t!<@~ZnZXZbyoYC!zdxNCZ6>ks_z|>ui$|JE7*q7vtUUi?ix(ljUvP;6KeHvdAR(s2qNC+FN1>h`uyG zAqdAR)SS_WlJ}0Cx4_BE?gg2fq#!j$;PosZW%}SqMuMs zXrSwYC@3cD+ZlI$W6u7%kZncT+V0!+Yb>n{K&~8Zq^;N*UFCNEi)p`KkB{ruzWE^& zwq+tP-U@VtkG`2SfsJZjxjhqULULTz5E@b zh2LtXDi*02jE*7B>vH}*X4{%wy!P{NM+<*;CSDY)GGkI6mbu?Z?8$Dw*peS9TXNVG zE{=5X6f;uYk2Be=P0rz;4F1VB{>kAkUi5MTf(*8_=QqcejojjQHlT4 z60Td9s6a9EQ}o)6eS;+w`geu3n_Mm0ne+PsiGOkDZ!}#J>S+mUH?GT-gclE2C1XSJ zVNz$1i@VokZS`|1vL_M7m{U9|G2Fb2+`%DdF|H}0!yVb995RR5H2Vhu;q2iI$>S7$ zsW}xi<`JOC7TI%tGWkxHvv-mG_B(qws(c@545;gS8946$eAk=@rbgo$ej(+IsBuM2 z!-a#6I!c=d_~58$o0)n1rutOb^A3=Z(3aV0OF^F&bUb z;uoDodOEf~8pR5nu?eJK2ijUYa5@GKTU^gWU({F{#Bz&H{xn?oftLI&Sq(U(F9&l*LxO^ zrHa2cvcaDy<*kgjGWC#QK9+gh9ZpTx zIcc_y-@$vSvG_T4z0XK$p$m;}h`k&0(!SDD^j!#jSB1W-+5e7TITCqI=Unb?WJ9X0 zR&jhz^0xAlM57T7o+#!?a!=8%$VHed=DG;X&TlGV*%_@D%hAGUrTG=QKNuW5dbi6j z9}R>nIi@REI7jgtAwuP-g+yu@68$CMGUKMG|d|vq1PTEm%PrV>ej@E^7WZo zR;s%OcW>0^7wN^_$#0an?id%FkAE?LHr7Vjh}is+QVHo5bC6cav>gjqKTOni&g`jE zcf`lEozpmzXKC9x(zW=pgNK=)L2zn&W!ou{Zd=;P41c?|ZywmNA5U48iRC(2ipF;hEU$q!sx42Ko-)I5Ac( zUh@LZ@5mC*SjuOs%E^_vD;(p)9nX`$#QfzHwxJV&$d}>+m*)~#yc~l!_rQJQ7k19@} zn3=3!1aeDV{3uqk6_~M}ndZBUTs|;Zp*ASi%a_#X)@!7r9?owLUS{~}fG~eSIeF); zZl!Me+&1$zn&G<=n_vNFU%lU4YJ)NUJ-t(xU^n3!{Z;Vz_;%-B&h>6JFO`<)NF^PS z&yDYg44tK2MlQFziW~?p)4pGU#N6`(%Sw5k0D*iVcO09n7qIujAZT`bqk287$dL9p z)im71XtKX9mOU2m7`|JiF6=Hd{|K(6?`qP(+QWivUNk@z=#c$$9L|NTMN7*0h3R1O zHb3fv>od7x<^QtxF5ppB=fZ!kWVr62pg}}~M2!Y(1k{9yI-AVM9y8IPsL*PqjYh1t zMVNsgD1k|8CYx#Psh-nwPOa8zKW%GkFLJS7k{}@gxk**5rxkj!d)!{ow%ks{}9@9U=&2{i^`<1akPkWt*_nY=fYzcsp%+!B(b zbMU}&dvCy;jc{$V?!&huQo!%Xsc%Ost(!Vhe*PY_Cz#)DxY8_^aaapUiRRO_xWg&RWfOESlu)|(u#w^Mm@BuKws z-Ruc#^^MIQ>zg>-k|w{%CP~a|eu8)<<;-0ydO4zCr1f?HNCUNS5)xOz!}MlN1*L@g;;I@qioX5`bpr#Kv{seH5pRLkvxUZxJk;#9=TOfR9vd|4WxZ*wDpjDA@u)H*?8|5}H+)6Vyp{rV z^&c|3Qs5mf>KiXdPq4)r`4)aN(R)UDoD&`mvW>PrqR?POFn*oKsux3UY*0cJwR-hE zC1EYt_{9>-vGWhh$LG`BYQ9MaB2$9ZcbAjhE%gFSwiG zeP|cy<~-(VlGTgn1>#V+22*e3R&<99?80sS!)=d3~DhfrqVxbl*>*~*j*0EBBq8a8Zc$hcPzQm3i0$ZOb9=WLu!Cg5_inT*> z+?wAjdze<4vN`EI{gT~O^eA$Kb!sTM6NvE$txCIKwpsYty{r(WRhbtupWZ_o>R95( z6yl5L+03$Ld~AYn@v&PT4w@ezQ_W!>EbC-YFnW&xwGW?cbp!usbMqmsZ8A zSFM$st=1BZ_3qc?VCtOZUfIO8wioGtbV*4;`egDHpQ7FSgaXf!N<7BC&!tsNKa6N| zDJjtX|TYy#^cwCfj?M>Q(ot`G`o=$I|Q;XvML>KOU+zi}D{{9wf&&9Sdy`;&Ygf=qJ z?%kk$OXXTcF$ws;rKqTRIsTT{w!7Wt8y>5D2)ziDa>$3Ae{(Yp-GQLeikTLjU7$)U zt~Y3|_d{-GeaMxijjZ2kbr69J@KH^4!p+TqQgjO|KF_){^@4|fvj(Md@}8;I7kEnE z)XI>#OB`?J8I7J${%(egl=55ayg|yzkH&aapeN=0n`N8)Cf%V(sr8rPpkDGt)jz-Y z=CV7&vlC*y>u8f+0x)B#3t*?=1GVd)iaECOY&e7LBz%Og}0fnLe5 zkLUy76~wK_SSi4?r5;TMyvDg$4PDKefl^-S>N-{`m949)+hNtmrlXCTo>J9)TDg>_ z?x^K(ub)eMrO7U7*-_PToTPl@`ZDz4bG`MQ+KLk(P;7L*@C*=7sO--PauV#6jmWMM z^jdgP%X0!9meIfQ>p(!^77j#!icup_yatVe-QVPlKSDw4VcF7UWkkM=LUW!dG{-;6 zmdxQlz%WC!zHs9@G8)3rEfFdnuWz@u42Dw0N84@U$HILc+w4o$ysfa^))mq!E{c}U zMJUE%ou!`7m*2x_-yLPW5WXsh@cnc}itv>$uV&q~uvammYv?QDEwu1qt}rPn^po@R znzhylE6aL~ZPjl+_7t!E=5vorsc3U?PWbcUnsi#HaGyJ^U&`KFJ*znEUw)~QGisJ! zs^pF-3Qv{#i6#-!_$j)t=-t5o8wAea z=SBsV9!0h)V5=Nl_4irV5^2ZMx=)sPOV$#9T#No(EpgU&eE36`K$HXRGQ7F`o5ND~ zrI&h(E^Ga-GC*a7V6nrbGrZ}t*jwCAJtd3%KUrz4icUKD6bkP(RtKv;^}xK*dA5*e zQ9CzGmgoAXthrKx+sFseySL#&Xf1I9L&W66h4L696_-KL(@^Q23NeG5>IT(vjw zk}ncG9EuR@S$uE{e;Y6{vNlh>HxWQK|EX$Xoe0X9;gu--H^5E40bo2 z!P%Qm5S5Y+dyvr*AFEiFmUzHov{WkQ6lv8QqQ{If*6AEPqhdY}i*k;a14vfPRa^s% zDJ$mvht-M^6z;cTtQ9XQ9d1hA$hHq6u#`@I!-<+lb6GvRKx>tK-e|nDz#4@A7=C`C z{4LHg7BG*R(YUC<#1;J3KDsM_OG{7Otmp zdV!I@N4WLBxzFI>13c?BydQ+?s&6Ww@cjc6mS-VKiXtqO4;Ecs3?;-ENb;IdITafT z8%cH|{=W-9Reor1#(${?XGnY;Uq=jLKWzPXLBJ+ zI>AXL7N$dt*lZ1$CfD|yme-C6Kab1)H{R*7dLWP~`0b*77k(b=NeK@<41O;N{H7zI zxB&dQQ(>glB83$IVSHQwI?oORpa%e6GeBW^_E`XUi37kv^u`lDTR~8+bHig52<5sU ztO*PYTc-ok&j7?DbcoLRhb@d1(|6G1FumS-Q6YY4jV^|qwyTZ(*|JUwOx~-nIR{d# zuf-P5`an_L);*K@qZC2OjHZ~nDei@Vxg+|}8OTlkYFq`JesYi-|iS}oP&u%R5j7HP!f zR|;c}q-LE)n1tUXb`vi4e&?$gUHIasd}{v3JKpnqw`lR3sH7qV2w~k|{TqZmaW0Q! zs6>m_Rq1FLw3XF7I*wMwlJ6&xYkaVjH|xsn|D}TCUl9-a_ji$dY71ka`|Knt*6uY+ z&RnX-_8I|(KCSgep9hsx=x{Ppv_rgAU8@ZCKrLy>CefnGs%#SFPQpi+}u_R{pi~ZzcckVq2 zPf5yE);Q;`RFDjuTGSwkU4@x7%$t0$HHv3%2${>k(CLPhn#aXlxo9@0H@m4&%;{BJ z?NZe?Su>E%tQW-#8|9w$8+CzF_?L2FN*02vVb4~o#Fp0JKg!y%q&*{4ee1077?Os{ zR?Irgv6iVS#ynXtJ|}OT5*k^o*1dd1Jgul%PD-o1yU`lo@Zvl)x{jG$h%SgPBBo`k zvM`k*NmklR@g*u%A2^KYGnz$NAwY;etNMa9MfmS6)<|E_unioIP^gjN6v>sxaSyHR;;TCEDyB( zIWlSarOJqKZuA~R=rZ&2j0la}9jc#0d=OpT5a;ttxwCE;#sH1G^=082V$J2&Y%b{g zE~oF-72LYo1i`N5FAA=}8r`UKdeIlwSzlQvnjJ}SGwxTDl1Wy-3If;hSL;d<08A4e ze+N8mL$1G_U|4YkkRyQ4&aAjG?shRId%J*pJ5}+6d%N?(BOGS~mp?3}fr8^{0M7(i zKQe1yfuDk}T<_>EtE|VRoBE!xx28B!9BdpMjO(z7K3pq|%5^pDcE4hf!N%NRe1>8j zd50zpubf6a4Sb%&eY~b>WtL93-vB}ty*Pvzj*>UOTfQev&~)Smr583EFgYuau_Nn= zE?H`bQ#SoM<~RD6t#W+?LMFacJN0O3j&i0W{U3XbZsvNsA~)*F!gL)f>r%azCp5$k_t#lPIgL8)Xg9zd!q;P7L^90iz_*(!nzRD+2Hd@VYNdz^^}Iw zF|1^4Likj15SrD9)Gx;6BaiNIXv5EW}P{lyJpnURAgt(osfl_sqT`3`ex z^gYp}eaU@{Cha;d&3Vn%mHc&a-;>ij7j7G97^OVU**uSi=&UK2T&a6i4jkg5W0y*5OFH=q(Pp{N=NIIO8UjhfcYX@ zKZH+Owj|l=eVIC~WrCry>x;B)@cr-dS8NGY$~1gFGZ*rrhSt7O#-(^|AR*JLwF8id zn9D7EyjHvbhp%6kwNP6tr*F8QIccZeDx0iF1ctRp6MLL$aWw)cBFU=cl&puTs++%6 zERmHfs!LIr`SB6)Nzy(d^zmtP^ z6@`MvNTF(%2tl0-jQm2^*_coh3f(mlKSV&ZbaHsOw#$pSRJKb;A1!j@>VIK(F8{T& zYb}3K&^JqdTxZqde25Z1&AUScQ59y?#wuhArAR^Pgx+zr~HUL-u)d zThI(th8pJ#3r1fo2<89LdN%VIW)Z#Jlp!RLzGtS<*ki;CT4Xz$W&In4g655E$oqqh z1!YG5erukp2UdxFqp=>p(SrMAt>pJwW6;I2PDkj;E%FtbTlSotudmKc_IPn6TzRLA z9pq@xJCuhgAPOy7{6jjrfVBmb0U60Ldd~&@J?HT1gf9WsDi=Ze9 z(+K*e@>@tckH2CsWP>_kqELqIlC~@D2itlW0al)TJ1ivj7SWd#i8KyMal2gE)&$PS%^dIa60S@c29id#Jkl zp4x!fSgd0>T_=)2IT!ixy|Ij*K;H)i9qWus<8BE+Y zfE`QF0j$IlrV^Qi%W~i6oQJ&I77~0If=yy1<<=0T)PbAB!Kr$>XVIVSi?w2GW3L}pFIesKEF?*Ly5e@s zW^!Ae++~dKgQE(_;#M+W$>o5cRQW~vYN90%lNa&Cg_nZ zqQuW~FAi?#lI`C1IHj9Zd;-_$_zchu{=e`aKS5+*E%h*5JP0P-Vx+U<#ALTHyhic- zLHv3a7Oh2(pR@TKshRpO>-%E&rw`7?C4B%)I23eQUj~zs+bmO2j9$_8vo252RVFGc zAWx&pD_q({80P?c=Xz2PF>MBcLT zAzFtYUBM9CT0GE?VAS#~JS(_1c}V?t4~~4O0)o*Gll7Cg2iN8}9*@iG+qQ>^uj@ny zW74MEd~2rp=GH&AmkkL=+^^k+dYNN^T!x5P7@+KBqBHe zN^p*Zv9wAT{FNcem1q87DMGlY)adl^D0hBQJLfNU*?*HSxcf?oSyS$=r9shq?I-rp z%C8ef2;ET+>rNgGwLZRrE4C`pODblQIZN4Wh1tYnTl_Key!@R#uqUjHzuo3CoNXM^ z0F(0PRP&_FIDo}DQew5hS8d2S35Z)yT!>`Xn@lK^o#a0d{{UQQ9%zu(VAXuq_dQ}* z-zUoQYT`w2F1y6veskGniUkl|3)=FVC1B#MyvPuFQ$-VzLNx7FDn^?Smz#ynZR@2w z(&8N91r5x_?Xtno#i%k%z7-3!@a#T+?3VBHm0KR>-&6kfTYepiJtpZ(`O~TXi01*~ zY_mW17_rRw^htnBBT2RQhJ*Uv4Y}bnR9hJ14hwE-v)9pJk7b?Lx|hoBZu%Y&CKDnze9s`pf>Ro6Ba&3BbT4`h&-L`-unv zae=b%Pzz#8_b#)bSP2m@3Du@Ah0#q(`^nS?A`ktBMVJIq7boq1JN8_jJ@-@R^4F`< zbNTzp&Ri;u;J|bFGKQG{yK~7DJ}mS3$nnnSqRf2O9X6le5E_x1&vTg1b7elywC|Mp z#EC2!9>aVN;&W$7+9!8HZ{@!1^3;qz`t)(n=nw8a&Kdo_no&M;^F4>`e;t@_G}Z|D z=f7p}k8lACHS55yW$K18DDq<6fGb>l;t59eFFasSWU#UPlN>G<4mP|`$i<=p_G6@k z&DI%=zzwS--R8HXxV43*jMzhREhnbNelAsT^Zk zIU~EDH<4VKu6F$;?>M@;NV?@j0H(V-;V4}ldd#lA6fk@BC;|SH_{|NUgYJ89c!D0? zKRi4xy1PJM^L`Q`?}5&y=-Z7=dfPS@DvnzBcfvoDb~H_M<1=QFDmZAa5J>q6fveRdMmyc@CA3Tol zjvwf|O)>S|XinbYAAYLehuu%QGFfSlPMQ{oZ2V4f3J(`*#d#>LF1hV)cIwD*t+;Sb zqspK-$iC9*!f0*85Ek)gX;*5e7^|6=4dua$iDuxphLyS5Di0K4X&liK2i;ZUycuNx+k z##U?0CV3zkQQ;JQ9ea>ItdZ;>JS!C*svQ0>fJd;1D`Y3W1{Dx^bWJhp0wWfe9x4V- zG+PbwGgp4*`T1#PuZrOQ5V_Ca9=Sghdz#ouIIO21R)0hUf-{3f3!!2`G@Ij(J^d7~ ztQR1ZY@|{QCzjb*1l>wpSnFfLR+Qh!cy~?q@-_i zMWJh*vLh!39)B}D)Gfj5_qjxy&}Ah1IJrb#5UI{(_#gVzE)rZO4K1+Ujdwz~0@$w3 zZ6~JL$Co4T31(;h1IF}9!AyEfP4O+^Q-Wr0DqjmPRmZ07E$|gYKA<^{Lo9BN z6v~Udph>z0nU@)P@=ewjZM8$;r_n$QZ*mwhb+-B2DdfwgQWr@t-*#v+ctlLIn`5oi zFlRH>9}1=;ZIc)Cr!qMhfuX0+-I!U;=2@*6;W_CUY+)K8_L2d7b^I{c(=7!=b?!ehi(Hti` ze+Ji;4c5sOaCkTKv%1M4XhcENO??gSKIwiEiN{uNeL%^6o$UQdrjw8{#oy6MWU=Wb za7UyTb$Yp%UQU)?vW!HLmQ5Qo{X2o{F|7Q zxSD4iWM2|pQjTtNw%lpYUjr%JRrC2poY*yO7x8ks8R{uOf7oZ%PK*!hjqc`2!IoI< zMA9Z{tye-2#4%wX@RRV-)>l#&;(sR|)VIpdZ22jdpOer+Yi(yMIpx5-+8N-6QM6p7ZBda&%5n#O*ZD`RV{uF9^AHdf^%Vs0gp zJX|xy?#nJ2tDDl+w|H>0uCid?>)bF=*L8Ha94rl&*GveXR#OokRdZf=2n%yMWjwYn zC2cRix%P$pmfGj@n`fUBZJYwsD*0DKss~?AY)Ms5?aUk-BE89a5+I8w;#ErLQCrgC zX|K~;`?R(PKO1d}d2H5}C&u$PrPpQ`b8tc-#Op?Mt>8qC*1Af81dR9)(&<%-)d7E6W=6bC=*Bv}!uJ_;jpx|nUAj{@(4<3t>?-{Sbb*+ z2r)U+&=99AMzal?3%1;&zh{)H0XD<5D z#1Qu;jD{fb3Dw|-vxvZW;w}Fr-(p;J)R*|CHhkJ96HdGU0BfpE%q=)WY!fe zNJunillPVI`Fw3cxQyS5a0$QXg@^H58CgP6j~$T;_SgB%wQn1Id!>CdmwEOVjo5-p zU;6?dlpwaChO3$O6_5n@FjG-;j0;A!j*;9~b!SPqFj-q}m&KNp+oyAdGzQ^_I^{e~ zXzbh@xXq`8&{Y1=h|VKP~Vk&?=OD3-R~WNkW@_E;L(s?Jczw%zqP z!cy|O^;@987yWJ*WDCfM2C$<}FO_(3qPfv9zazc2R&)YMBm_0Cf@od21f!i`{qWKB zw_Y0WD(_LkXMazEH~1ccmV%SNqgEF~jDpq37U-M{D{0d{Ee$Les3ZL_EG~qTxgfLwC8o&f4xVaNhHVqv!kOaIzq5h;FDIvCUl=bZ&rXli7sZzEu!@{>n(j3vh z)5D++f9Uk^z!5z(`t|URpxNFJy-rjh8>AsL7*h08-VtP&D0shiXA%7I>;FO=6DNhe`>{pbj#oj_mvEbNW=0b*K&o;~% zB^`M+lOrd$+?6{?&d{8e$4kXS@;SSz<|6fKO8V7M>Pb)RaT90NkWkQgCbxKnY>bk5 zwtGPlwNbgJ0^MQv1uu>T@0;oTfbgF1gY+bZPW%7i{aN76Ap_Ic_EIP=`%^ZG&j#;! zokH^KoYxA;e+KrtEc;N z)*dcuYQyK%__c3}d{Hx9`{pm@?=|7;eN)$mZ>{-!_|%#&X{`^+!$v3Sj(BY!kW~?| z){lW}qE+7Bde4%Y+rl^3d@Ve?=Ih~$YvzT=)+`7Y*F?fIeN%fPw?0!$DZAb(7|EY& zH|SMz?+!W8vB7zLff=miI324{Z`}|s(w<2a#p)`b5um~Y*~QjnRI*E=!OG%i1d!Oq ztukP57g{4Jgd(5_(kX0v%MxFX)@E>JUPJJI6RMJi$;ozq+0|w@XX8rqpOdRW>O&H7 za-MyP-uha&NUkgF5oY&xOWsLZng*>6^M4<=xaRk$hjrKYZ=sni(AEmuKESrQ-I6iud<+4~vWr z#p}xb=6WNpw(eh+TG!#?8jAl)wxpnBOLLZvrH)fF zoGQuCw4$)CIIGGSmxO59R1o6 zzHL6#v=3h4s%bH6%Qp3VvmzX?$G^7AAHOFCIlXz&En2HENL=e$>s!uMZMd3yzxItc zo%`w9H(qkCt_gp=$-6$hjMtwJm+}iObhjp`;ZqcX8XgoH_%PR2F}zbkwhT(%s(s^L z=jyib*SP;$_)GkLJ$wnj^TMa|yC8f5ztG1fZx(%A3T^$Tar1OGki&4Zm30DcI=Yvm z(MN~}c@Bk2PEqvHAOCu((2YrH8aX6B{XBVJX^&N8a!mz;v)@gT$-yYA?~oXgq_(5B)h-LFe=}acl3d zu6`+cVCbUHkxB|@sqOjbyjHH5^)Xvs{>J2P<@~%(aT@CshrVu&Y=@H9s_DMzl=AXV z=bv2HANKDW_dfN`i0Yyo)JsFAZ+|_mjnnHnr%W;&Tx!m!iN2i|E|t?@PL3WN96kZY zY^^Q6FrVn9hS|Jd90Dd1$AJ}SFA-ve&0Q63oDYK`r* zM05t*6Y{Cy^?Zu7dd6D$oTwLLb$rTIA7!jMF1-+Ls6ki;*db*mUVM@aN-ukp{286m zT?e9_!!Z}zr$$a`S*+h0_7J7ISf3sl29E;mcHGa6_~KcGH*#wJmfm+fNL= zI2fbgF2q+;8v2gY(A)BCa42rh%45}?5;9HoTrzX0PX)IpLiv5c?H01NM|-|bCtyUF zAHXD50t|%wtukw?WCdd}BA!Kjd@vg^39ng8f6Ah$t%Rw0oI_BfO9=2L!4pO;v(B)7 z4sxS>m^dBI=j2qWtoQE|O(q39a%;}R)>c=EdlzvT@?c`H=FCMU#xvvTiTqPyoCp2z zzMfirfq^*_&#so5kt8FIUE@ySTqI>u?<%~3)=MfHNoSljgF3($UE1PP9a+TEoAe1i zO>!()W;RcYoZ1v!Jnh!-V7NanK4m`N`~CmZ^EvIa&F4>le2n>g6W4NWbuB`iH67a9 zGvK72LZM?#>bS!ub+}CGngz^pQ>=DcznK-w&b+5`nP6F=YI-F(tkQKsl3zE2HR3aJ zs<@1d6OR!rEl{2>3eG9Y(eCdP%6oV8rVh z6mM*O^c_K(@aHf~-V7zU{2v zms!8t2@_=fzF3*gVGW+9T+f9f3bDwhb6HePTRm^T^$M{7u2T@y#&F^UgzJH!K1=_0S{q8o-_fS8! zB^M7i_PrVH^C2+`1-pCjhFV3x?GuTmsfzC2#|yAwmX#;o{>@61BaDG=bXi4wW?XbhLyjCWTxYaYSMt@Q0J+ZS$*oM*X11V# z)XT>S{|`9?Ot}Le9sakX{Bz-dAM76RANv0-_@DcFfB5GP0RNu>b_M@CnXEMYCkXu4 z2-E0HLQ}P5qKMWShh!F-V!59>3l2}<5uTtldx;9RTJJtC>kMUdK|BrO^APRu{3DdR z#=3Sd9T)pdh59{}-#q#L{(=Z;3^7;4itN!CEcr763R~X9MPWy6d3%wp+!Tl6u181L zpWO#P`?KiY$sD|291)k9*PxhzFtL<{H#=2$vvILkAL;_N^_#(n1z>Bmim(TMBLrk?3^WsYavDC zjwyXPIAd-85xjh3FB!MTUM6Q>j~g|c0}{*(sEaIPUP%V;gj+5GrIN<(kV)@b#)XEQB>bM?UGy~{5m_*IFvv@lv7o~ z04Gi$)aK&0-2b!TErg z2z*8)EAqlT-DST~1X&}%U{HC699Uf`$^WK@;>siwlrW__SED3eZ5mG;jZ$GCH`hK% z1g9VdEcu4Bagt<*gX+CY8X_xf%>P(UbZ`K*!M7b16%7W7!#cC#HsIUBnjz= zwi>S@maA(jq)FGTdZz0Cr`c75`ForJuRN{+Kc~8guaS&eT#cAS_yrZdB)*1P>%XfK zM={!ySuohRAJT#2Wv0)Lc~j4=wgY-}aFP?-C405GPh9A{8w_pQ9uo3dP4`PQ$RHyZ zwFAHNK-)nr%@{@ta=kWLb@y>lBqz`2Vkp%9%kz4KB@>eI%=szon-7hU6EL; z`Wr$yb_Qdd(KrDoE+hXvdjWvbo~R7;ZxA&JXGo~?{^pR1s?vxBaQCh!3u}PIdM$i6NBAKCjJ9UbKbI>~3HEUA zxfa$Hdt$sq;M(_Gagt&m;Usi}B5oFdsC*hFLi5B9~&d?P6gPr*3}8$uXpy zEwo;bD-9C_e4cZ@qIK^NoG10;^f7K1(&(9v;}KOa`fvwXU4)8rA~vyJye3re5sQlC7Y%r{3|s8jY^D@ zG9sdJgj_=T|AM7Pyi5gJM|hbUvEdJ1l{yDed@AXvyBSmxW2u6%@T*K%r^CU>h&eW} zMU2;Sjc6nuv2gUF;!rtsU_eMEK7WC;+4%acGGZ-5=IAd@$tT=k6Dhr^5Sl#tG(I2Q zeOYh^-W;#i`Nu~oGF?7G9exp!_I+2 zRj!YKdjRN-cdAV1MQa&9v5&ga^*wWPK~AS0H*&4--G4O+{pOlNpV+N)C?{+10DFVSC%&Ks<%Zj!2ArYjRuM_Cj>};=W;cB%34CN%3!Zp6hx>~Y zl}~4UhikAwz9ZsOwbq~0V~Nk?Kv48I%g_tTVWA)12VXBir|BS6&dqcqP$eX@e-!Et zln)+i>*7BC=kQcfE~-G$@cGUYRz&K_IOhq8xUnGm?Yt=68w3e@jnzV+s63akGS!&3*FnSc-cnxksB{N)4k1#mu2WGq}Pp? zo#ftT;$=(SCvLnfVXRW#iI+`}iWze7R~Ae@)BzkGB6y4?27L9$gM!9>ZF--B!yt1u z8V{8;860x~@#v9#*}>dvQfP44*kIMTFW#I~6iZjt4nGNst^| zbk6qm2#HrQ`|M{s4U>2nmaE|R{7lpTnQGc5Pd4~%EN9Q9-8Y2ck<5ZuiwvhQzMXBl zKgJJ&6>%}%MSd6KhtL#jpInx|J1XA0tgVCmd%Fto@+)|tBRZA1ojuG(f>jMWvtnr)l#+iru?ZNFCnG!>OEYHh;7$ERFPwD&i`pwSxRohw) zIGx-Y?dsP_?)!C;d)3L!I(EiMx_8Th^K|c?`NJax-i;(907yCZP+j}^-uw^NioGox zHbWEk1Kb-)c*=ZVYcf3AS5*}*wzk}r?(;s>c)q>sJ&}nDnL;tI&{2O`Y4l+U`S}EO z6$UTXle81dQk~S z!Cnzo_>Tc`7EOWL(M(P#^O!yMO}cmQqE6M+)_s4lHxVgv_J9?!{HWz16**FHgMH@l z6jKqbbA+*)E8@#MQT#W{X5nOQ5#A>yVo7{VN_g=R{toJX0RFS$L)B-1{?Wy0Q2rF9 zTVgRB(oxX=(LO={%XIS{$*|tcettR&AWw{EMcV2Taq@B;h+^Dsvikl*;r}1ap0%G~ zMT&00OY0*R!Jc^SD@|Ccv3sSeq0U?L)?oN{>{M%uDg4YF437G5DgnE`W)IgKb>EU+ zC0}BHZqmJPXe(B;SZQ_Yf}z^#b$Z3R#1G_w_jN5wure3F(^g2X$+cgh2_7#PLa~bO z#1eU_dv|Ip7RuX$?pvpk&cqGQ>({jv*URg9^h_!{ue%bL$ZM%w&BdBM>g~=2!)f|F z8AM{7RBA$-DIHlcio3WzgbL2ryOcZm+P6~g|IQel_w#qfjfJYtuFSKS);6(3)YFZb zr>jyWS7n|(l6v+?=GiybHegj(wMDVzd`DXm2KSxP^P5x8B$apExSX={Z%#c6@ocRR zDj-jf*}V|Ya;ZZm@2 zVH4Y7e8xj;eG7*~4~|)gNw?b`##NjnvZi-MJNq(p?N*RQU*q^X2zkSuP=mZ-RqG7b z^doT5u0y-v82~NIr7)rnVx=)fxx+b_)JbaBBjW1<*#} z97r^WySM>nP-otp6wAl zP~V$>-x>$EXz_RHr>qI)ELFNY>-iV6p8q)O`QU>jMQKFuK zXFKQ5O+71+792d=IsfX^)AzuZH4dKboL`fAx|65iS$xJ>O%>e=%ff6Ag-LqABohY$dALR!^X1W-Znuc(R0>d{TrC2UsX!)fHZf^=NIy*+GJ zq1a`TK|=+JW^Yzkr8cXgtj+2V?q>C;^k&r~o7Ee#S@o#R>U}n=xALr2n1wgwsLkq) zNEM7l`Wr|k=|EIc?mByp#-3xYm(A)u*{o=4>%QN!S?vv<{pl6eW_3KpdQ$#5C=bOy z$MMY}ABf^@Q~Q*wb2uuS_Qw1)6br~CoFD<}(e|m2yR+D|qp)adqk>7hgRKN6?afbP z(*8RP8Wb->$UZxRw)@WX$cMls9)m%vZ`^__6`u?4`+%%tp?`u;b5@MYr$sk=g0N~M zB1IhY>wS4)QFL>GJ(z3l=N}mPsDv_H?7)25S(&b1#knh=jZY)52h8BBr1 z*m`wd3cqP8mlSBxElK-!#b$jj%{OH)r+&O&MsToiIG)lFCHZfe{WYKq}>{JM>JqF=xv~@8CKDX!~ZS|?m6aIiAMrn*P3izn#j+6|kbY(@zRp}$kGq`f$oWpOOD^C>9K41z3RSXPN}lMHJXJ~-g35NpFl{4#;TKB9j(A}W ze}eHVE780L9Q|v8wv{U3i9kgK( zI|&N&^}%S*EF-$kL+U~PzMmWUK5C%DzzBmajwmXh1d;Rc;lDbaFWufbi_;c_jyd`= z>j?xnEA+1nh}06puNO0#Ic%u)A3Ua$)1CW#`cRo1{ef7l)Mq~`iEO8MIj^k~zedsM znPa);uL{bN;^-oOL3wlm>iS7_oz{tqLJ?IhT>bA<{Ci*I4C}{VO_L4ICgUtd8B*HX zfgTniKKy#i)SU2dTd=kcw-%=>*e{E`>$4`%?T{D`B~R9HYnCbidzWMX!qL0pzN`+I z+G>?PEF&b^p4BJX0b~Mcp;`@Gt~jfRZE{^gmSWx(t+!~+ix(YDxe1Qgd~C}&5UKwLj#WG zt2E$@Y7vMdWBIx>7JZEmm?X}TJNNoal*rQcVsDi3jJ8InyJcO9!?xh}3{2+))OK0J zaeJ9+Jp}S#z~@lFTZmYfkb&hmH>2hK3J{j`T$IJn#>9k*R6E4_)9sXsHnOfMx{)e) zk7ty|DL9A9Tt^J2Bd1v_spcw>p2iuRjz}#?gpiArF+W^nHPfQK+gc2hbDw;noIy$^ zwoTT29wD#xx^pS1_p)V8Hha?ekYsH)mUNj*SdO!;(V5R)%4Z#bfSGT8 zO(v9MKlg@FzHG8SB1MS$`oFxXHiBT#{JW%4D|cdUiSL|3QMRqK-5(t~fpi8X{xsMLSXg5qGs zd!gLP3`E9UU-4c-WK-xT9QN&o_iexV544~k3L=Ygsb}8@{)!Lz8}zQ%?q#{r12$K4 zQ?#)dLs`DHuvH$>w#lK6J(l^HED5buN^0$!boloCO7LUDTv;l2P|C(*5|(}iC&;&?`mzmtmV{P;I@(|lhrSCMT#uuN)EKU zs0|oY&lMO5^PM=z+Sn7V8&zx<${&wW@d*=XZV2w%m-q|soc&m#7Y9s-FmoZ1s&g~J z7cpIB_TbD}m`<=6XUZuQJcvDCQ1E;~!E8aU- ztEq#RK2aO787w6gj;m{B1%jQMtZ$+OMJ<1W81uHM!znnkZ;NHCRiajYGbI>|y~?HI zq-|XOj!*V0v#_;VuJwfIVr#h%O|99NXz zgX9b1<=7KNms)DQ%eB}PF?%uR))zJTNS(M>WvYnLZl(H~SUNw#6Hyk~TCdTTw4P9on`vEiNO z2@gxDpr;k1MD9rH&x-n$<4LxX;@j80v}`U0Y+uJd%iypu=IBa*Kq5DE*8LhBW1zb< zaT@XJS#9c0o6;Jsq#Cq7;h--V$ERsF1j^3ywH!K+h?M%c>0zSu_T2Cw9#urV|ID|f z%QPInd^O>Lt?N**#4rD)T>gm5K*#&ET&cDFFEv#-Ns&+Ju%UdxNeAYD=gJNVNH@q8 z-EyEgT-b78R)nKhYK!%!NwoBtfDkk-dy3w%)fp42?0)b&_H)Yg@(6l z(FC!FoQ7Q~87={DLoDl%H!nO4ye623H7+>fwY0WpQE>!X-_=@oVZBq~O^MY)9BBxQ z+auO3n!^`VE~U{kRHK~vkza2b#r0lKn8l(e$;&{a3-~8F*e+L#M<@}!>0VTwy>up~ zmyX`i({dmX9@TQ7Av~nzz%BAKFLDBSXbw1^H5JpDIj zf2AS)6@N#UzvK0m0~NAN&x`m5t}IYW6ThM9%0m0L(1JZ!y3x@S==ejR0MYTbJRfHc$^fZ7{9++8KaTZ`ohDba=~w>SYRHF(+o@Z|4*J@V65?9fCk~ z|Bwa$+MQqnYGVZgStP*J|iU2=^^Rw4(m%S0~<%j%o9v5EDYiF5Yrq}pB&sHi2~F1KJ=&r za``(38}akr-*_k~iiiV&j_v;V7xMj6S60k@IxmN#;JCVjD@~o?saA#1@4JM4PgC@J znr9IN3Nb#lv-@2%mz_N@sShU>f4fG*LGK%p9~tHq^pr%JVqU(YSQtK_wH?RpptpC? zdCp~^wJ&^z-hxvev<3H%*~LNc7&3guYoRUT(s?zsWg15HE1)e`NG3j^Exn7H$lyqZ z7)tv*;n}JiLW}GMr{BGY_xn`+P`BQ4Q0=Mi05W@OPZ5WuFvKD%mk7J57$&6LoJoC* zXL28lSf9G`oOudPa@DNh)TUq$Sv?X?B$~|h&mkL zp=!opm>%H(`hqc|n9RB|;T|#d0vu+Rd+EeLgYIN89b?c}7l)scLF>PfAv1LQ2mSFT z6<7tUiO2pgk$XBuPEPteHsTxNH(#rrdjBac3Mz8ds-W^~1(h^`GBSK7>oUU@4#g!w zR<7WVuj4hmUfNHh-C*=!zCfjpxkt_u+^KVL2X8|ccPgAq+!e;RAlY!Z16v))<{jef zO3qdXtE;^A>%pA54CatMDMRvSrRlU_PMRKM^%=zJ0zU_4#!wi9yzn+4PHl;=y~;tH z9#|Flj`k6fX;k{qa-cliV8C^7z@3Z1+ZLr}I1Q5wKGLk=y}ekXY*e!VXmze{@0$`~ z`AM|1C~>neLuyH64eY2?&+W?&39S}M3L1Nf47P&^DnMuBxt#ftZIQ!pV8E$ZiNu36p#S2Wj*X-%)ml5a^LKRX9ou9VxC>VscFKh(@fYT5 z;Rx~f{n ztg|Y|b6sh|9J;Eg>9`-PKO_!ZIW0+h`w^{7=(V3!qQ3~lr9rcg3^C<7R?7w8riz9T zwLt%OqfX@A59o%|*Z|+m-#DRLzM0E6qkXxF>*;AUF(}+5L%L3WrZBEZ!%-V>I<1m- zIX!2X_5}lUUh&%Ua+Dq|b@dv9^$m2cQ`9g@(}UtZ4e#>+m6NPc;QfLFZ>{w|oQrU? z^z9-A^H>_Fdv|86TE^HuV?uAn8IA*>g3Q3YbYTN<$bWm7h9j%F?>L@mBh6riGA48z zLwoJtq=t4x73SyrN6b&Iwd;K5fP4s!sVRGYa;@-4-tFR@)9A5#M92AW&*42XWZKSf zrcG-_C8+Q+Ea169rtWLU)3$2*$hOtA0yX=xV~ko&tQ0yWn4VZI1)cpcGdZd0&FJy6 z(w}F$W8vZafn&JA9ZQrtUoOE+@ZL3PrOwIQF?RRC3ZjTjyCiZViR+SB{HQ1=mk^Ni zjY)0Z)5mI=a-sv|1d=rw~q=LhUfzN~-_izg0$_WtP<-tKbaY(6F-h zZ%*2Ga-A2x;JDgNEyMc4s|7kqBD8VEh`w$7*+p4c81d2jgTIxxSMpdnUi} zH%VABi?ib_s~yW(71047$m8$3B16?5UBPjOcO~F6wW{oJB^SHzF5U_h!EN6rpDh zBz#%%N(fwd1f*l+_U7i22b>TsBc@vNn++sL2%@>cLMFLJ3or(f^bl3JtN~1_wp;@I8tmA(sTbvx~zE zwN;Z@8Hn7$*z6Lib)jr_6QT!-!UcpopMxRF+!Ebg6zsC{IU_tLXU89FTW5M^+9Tta z?;~h_;l5S{UBt*eEpR$mw_9WBrixSz^RJR(dtHW<_s zPR{<+CWt4;pP4L4jbZ-XU-hM_5pvMqZN_;4R(n#hH9mbFlQKuyylA#<@W#3`CsjhjWMGDn6(3Ig=4qzAg5 zgO-VVW81CAX`DHGA8-J8qK8XDUd~fg31r#biB8&r79bR>QxjJ$6L+Re90d49dt$x0 zPRG94Z|d~cnI$uAf}U#C+;Ph9N2$r1;je0(8mSb6EQbYP<@UF8jI^8~OCKSzsrb81 z*2xzs;cu!KI_Kczl9JW|~lArL`s<>NqaJhOZUuhKe&bY27JPzE_$!8_d(K3!Xd!h}Es z0?`!1VQ|iZ^=Fb#h`z^ujXnwetV+2uvHYM*u6`@krb<6veP?mxT-KW>cIT+3*qsRF zcb;Oz?ktVnStCK(HBxI^MGXm-sA9s!C6-1o%jc-d zz|hk|5bsci>T6#@*OV>WN>n;(?mJ{cFt{#9oRLqE)oBraGb;3=)?*KvSo`?+vN=8N$Zco3%^pT{f2WbZ0iP!6to0f$4ce-@lBVh@h88bW z{jK$M2u#nCPn=5wzK%mj`N}JNg`v0CaYi80c8`M_$U7~IIoJoNN{)sqqV;YrD;3|Z zF8rePwGTnQ$Z=No70kHt;dk@Ln$n|OG9(U4)r}lkw@uZ3%&9xqsoN!W_pf*BLVHSB zv4@?wDk&Z2E8X? zx2^}-I=X|;is;LtM0g(Cw|sXC+T>{03hx08jSpsdb-~6JUIxXyl3JL>JVy zj(ADzaQXg@PuO?3!SplR`Y0wo72z{45mr4s#?R`W=SMZtd`S*Hj#P2biEq++dlG-` z(XwBiNq#LX60R!)jx9$4hZ?0wIv~jDIj(fCG%GURg)@!qc1L$| zgNr#JSF~%H_rN_h^=1(eM0?+3tfe;p?OCQ^IkXGW)E=(DIUK}8L6f5dUi;R21_^_) z<4^UbCZDYz+gU&1#o+Qz0d8o*I@&uU{;ee6pw;EOlW34>aVY8mqigq+;7}9|gYDAz zRm*65_sQV8)Kl7#WlY`BUY-AAd*P8 z73kQH_N^n}5hFIn@#r7NMY~Vt1ms2M_^U@QEHw2335V;BR_87(@S8I{IQi=>ALP=< zQj{Ty-_i!zRlD9Vj4wLmH*fw$0mdEkhUJvqt9QLsNP?NuiLmVYQ>mW+YB2x5q^@or z1a(@!LsfHY!0d)hbOowsngx}4hH!f9;6Yk*gl=Z-&8(8&LtxozLe!3n=LcD&6EwvkBDQ@Q@OzlO0#cGGe8*0FxG zD`0M9flRNS8I>>gVo~bo@@|Rz#oy7Z%PC!($crpIRYO{T2?L97Pue_`$i?id;M>oo zB>vN~`|#_)b7$UAnbpFLf6sF~ciQG4XLn%BiI4l)Q<8*e9{itn>-%sVF*d2IRKk?vu@Jd-tal*M)bC3(rk@OE1~@T{@AnX z(ybBfdcRcnz8ZN=YJCoRvtYq#s8c9)l+WT-m-;Ng0#cw^UTu8X3p(z}$!>qV#t@LS zy~`#V5WcLU5YyFW9I*6#f04|w`9Q78Z|>qxjkv`9S%zi4==c6ro4$cV8c8>s>^1jm zt5!z4e~I01)vu%L_{&b$^`|^?r^E|LaTI?Mh{xJG`L!Cq2y(=S|MX{vl1A5CdD`mM zwVhNj;@^Ji3HhVS3LlrhiL^~CtXfW9@|zDxUCzYI)$aX;te7@w5hQ_a(lSF#oJ#MI z3$@l0xe(IaIz%qiAeRVMec)Z~_Z%H$>tZmdmeO^X*4=|}^kR*1Dz`}wOz{x-8!mEua? z5=V~l1Q+6FoNy7Ym*oX0Mx=HWG$j567=`?IO2(e*adaZsibvYQNFr~q&r)O2L# zi*})^!~|A~QRQXb10hfA`tU_pMhJqfL&RQ9VKe(c?*6%RTS-+o}UM_=>*7_6Cw^BrO zAMGemyeAj`1QFdM%UX!2Lc@T0C=`E8sCr1N-5rWQsVJ*9?Vw9qd76NfD*8D9=|s~q zh<-pib#qEq(V#FVBJX$U*>i`{GpJ*}T0}G3sF0za zs+>bT^A+_}5BgKj$Rk-~bEWF({~X!8OxhD-xm9aB_!)`i{y>IU{@1n4{+m_l+4ro< zo_#+GT9WCqHbu3mwe|@ND?V`vCGWJh^BqDtMJRwGmKD8P>)G@UZd-RWl@~;JSiG__NUO$1QNLLOe(RK(t_g{)X^dqc7N69 zJ>jXyzOErRrzK_Lz(^|A-;Jp^NPPXxxMHq6WA9F>*n;I9BmG9MTb8BF;i8AYR;|Rr z#NV*erq&pUOR)4N&hSDq_M=8IwC5bY7cEGq$Xn~Wb{FRNvK#qeE^54?vIf#G(1L71 z%lRvkNi%G%N>3dy|HN96gwJrfoL8^44o8@D*!)Z7sVJ5oNP{}f>d)le0Ia?|c3Azb z4y%7%^f_r;9e}|ANo{{wQUe*pL^_bklI_pzdPdj` z??!DpAtb!1iZ>$&6#ftmKAYbfS9(};Z0X_5tJT)`H7Xs~)>k;~u&wVk`rt?pN_ZIS z%#({all$BWOT5R1qgLrBTqx8*iUlg`{70-(nGhnQGJ=G9prpjD5fLOfZ$w&i(FbAT znCO!*<%^U1`XP|hzkc}h^HPLqx}%y+%%YOZjkAeJ4?qVbjvg8AgK}R_TF9>5d8!R$ z4(Wq+J8eu(q8u9;drr{RLEyySnO3)9B|{vbtb)NFF3@xM&8;D`pA2yvFnE%>E)294 z`vq|rqxz}~bLZpz#<=Np(F1QtoP?q(;C;;%g>>^xgpcjIHtlyJa3EmJb;k@5sYLSV zZS6$Pcx^H}XrChrh)7gK2S@}u&79H!=_&Pl$*%QRz%y<9uj7h$M?vnCPHf{bgfg{j z%6VFuUGqA2O~1EOB#xu*nxpPDTb-@a5j$iHRU${$rs>KWrjk`6kIJ5DD3N9wYv29A z5n_fTRw&9qizzX~X(J*=y-dn1*Jqe zp~a#!8&GUG4rCHanOaace9D5dzZQK=kUK?;qmK#hi;OKF0!7rwt((TBhs=P50WA`& zJ04PAMpf#hXr{=v{YkkakGyE!uRiI>Bl2;#h9i&s@&5oR?>II65jv>$d|fG~MH=)O z8x#%*G}O^Q{)$q6b&1mc_+w9sJnYx%DW~uIb4sYG$`JKx-XB5K4Tq?2`QE1zb=enV zZ89-dLr4+zf2Y#GL|w{ese~$P6j7JDj!H@hAK~`&yVd(g6s*S~uAQ-&sha7Tl}|e)vbx=LC9+S5RPt zSCB&P2iZkk_}Wp`RT)W&3Mx%Wzpk!7C0!GOFP%*^fSPUv@K~CL(Sh9OG6eHMxrf}V z0m%Y4UxnaHKdifGN)i0Ahml@+OIH$Ssa-Au7DfLj%8n%4`~>xO0Q#>Wa4e9(1JJb; zSl9!P36{ju6c)(z-yft7i96g8TThNj!-Y-40gU8D3V2^(rx2Xc&EF5OQ%Gm~?G&%L zJB473>=cqWH)JOI?GzS^M0N_1+Q=WzP9de$>8z}*)J`Ejt(l$T`mCKo>bbfpsd!ZR zO3L-yD&#?WtB|UZ9}c`#9QFk?Qu0J?6|P1~?oHM20k;a-_4;iUAEmbnrxUyL6iPXp zWY$EKm82&^%|d$15TU@`GDJVfmaz$&L6&-HSNIAY9|Pe7{}YdZ%zn|wnxAG@)I z6wj@$7~F4NJW1F}_7awPU}I^!km*CAo(SE%m&-=X3{&XJLB5Z;WU7G8) z2Ng@B^y6@b8*pYTLoB=S>?7w%YEQ$69I!?-UtmPh$SiOCO?m@p(1GGMPmk+~H9p%G zpKUuN=?Z^|@Y1B<&3Og_YD8dB+{t{^AcTd1 z9P8P!kR0Bq)`-TVLTKyTEv3dRi`%BhDg)F(xvn5oL5@44%s)Y|bWnw&A z!iKy&;mDV+$J@lWaYG}!lh7Wq=~mGtR_&UBFYrsE{iCU zDa^HNORe2d{qN?|Wt1{}wQ`O9E7$6u&`!^N-NuB&=u5jl^=Eg^h&HN~^P^E6ymHSA zaL97W{1Yr3BVAYI8C8`9m8xf5c%QjaoJi{Uip2y*jg9;<30OApmXw{yqr|V+BIY6M z1wXr9Tc^N%z&vDq`W`)Ojc3!sQ;Ro=>s!9k3~Swcl=a0L_9!c>T|F>BbSP1vF}5VI|bS;P88C!+dDyE=A;a~ajD^(}GXshKFz}zuK;EThYz4{a(D1oi z5lh2^Vx_iEX|DECmp)Z^pO2;1zaZXpjG%@2N6;PP%CHbc(6+{Y4+5Xz4aE)`5SbNx$6gbv?E=dS?7q zY5H|0g^`exAQTzXk(u&oX0D$ms`lJb45Y-hRcs!7_CG|z+bm{#EQFeM$ipCo3S=T;lg<-(FB1!ek}A(< zi!v+q%l>LlWMHt4PAK|l;bMxZ(Z#ATEV<@>yU|r&wqH@8 z+W>!HXq4UR!!9Ip$Tq)#_iidYWOmY@y(8fe+k+wrGSjLZ_}Z_pA-6=p(+ToX$V9*+ zU~8+^H={QNBeYg~Iv1TmlQL~ApX&v(EQyJZJCMBrFaL$Z0<2H*nxL(nk_$g3c8qDa z7}VeP>+f?O5#)o|DSS<2GP`TMg`ml?tL;(|_=eNG6sVvJXy&Aoq-w%C!RH~X-}G%P zvoF~m^*?0ydv?!PA&SfNMm047&zc35e*JA4z}v}e0LSJ4ZBXaggpZKP!;av7A3+Cb zkfA6ukxIJ=cm1C2^KlwoR~4I`477dX_7Sv4r$c*ukuR*S=&jJdLET&{U+faAzTPux z!H7E7ct4ZCnH&Y2Jj3SyGdhY@7?xaLe*5G0@DRBRe-+T*z_70s6eoBYMtU5izB_Z(8V5Ry=qG$ zpa*b|bKE;QbUtUzIb!uYR{pr;&-d_B3Vl!Vj-I-TRxOFK%4j<=QvA?Vi7e6pCbq9q zw6MOWy~Q_44Bmi&9avNfLOaSHR#}}(Y%fWxfc5#p0^^TtnN) zaTqz4ewbtur_)m+3uRze0{$LstD7FPoH=g&V$7X(Mh@bn_yC;=-2=0{4MPyJwiU~t zQ{_)CW+>r%V^=uiqFPf~?0st2#M`(p-Imy=X5BcQ(Z+=%1i875On^z{oXaQMoHaZi z2$0ZC8;tXC#7gA$-`>5Mc$O`_VhenisylI8!sz{@@FE=E9?BLOOxKRJEw*m z5-gtmb#Z}Ke6iCQHk5@wU$yzOL#Z}TkTyg1(q!UUIs5^*L}U7*#Xv}e_1-FQI4oDI zu~fROX2~>>9ypUh>=!Q#Ymp$O8B+tpOKQ2;%58$W?xH>@i{Xm zJrD!%Oo4@&&cPkvu|D0a{0M=j#r~E}f@h1!`K(TJF>|jW@)6HKz)K2Ca00Ed8c$=4 z?JXmIRaiVAhzozW=>>z&)fVaP3u2yG$~a^X3_K;ldqH^J>L?;2rmH) z=_L}&^nz4Ey1`2|s1I~N4@{A?pSK8ZC>9S!g2Zkxa#@jHSXqb#ife^uEwz(<`ra~zgd2#hc2q{ zFU+eT-~1f8=lx86LHwP*l%>3{pxK9^yw4r=6*c=NMSUf4(MMLyaW$@%O0T90a?+2M zXSLzAd|K{vWBAyaBU%UJ2qZztToZCPyH>l)`7>=ew*8VqgMLF<7UTu(5$b zR0K_JNX2EYMllrky?nE8>YC846}N^if9dwAZJ|jOp-@4^U9KjHjUYqrtSpjAmg#Cw z-OFsKdtZwe1phL)2$0gQ!&fQrveI!4$+TEHyDgv zJf(Cmmbl^+H&Hg2I8@a|Z^0Gi7IxSz?&vMJqTDhmdP@l@btT0DUXDK3a8f-DCv*aU zf~&u;rJS=BMEb*UuFO_cGoM;3t_|m7I*j9_*9kT3tJluwyz6RVk9<>ct!W2GS{gA2 z5iud8Bi#jLbC&&t)ZIwyR6rSX5rAI#pYY5&a#sKb6F)^_Oh$2t{FQHxAVyF znz0iLL)WrybUrqI+ar52NoEtwnBw|VTV*r9zQgVQsk_3v5@TJ zO5z`EgO0Ao?cu;yU*tn~(6eRH$o(V8Km>LO6t52;#pp@3_#)FIZ4QKvA-Id!U%2J` zQ)|Y_J+DAfQ?AvxITBaW+q@WxYiEb|`xfK9b1W9GYuR>=#Tv`fvFVd0Y=0@47@7*C zGQFTVSa>BKZ=5BU70y|4zJL>A;*vVylhSE3>y#XHi1ekbE?OqcPmLBCk|9U3S4)B< z2GbMie8CO%RK@@}pVg&?)26tGj5M~5?5AtLeMFo96Qs+AaJ!Jug6cqVr^z=`24{1) z*u%TDC}}GBNBS`j0altlN1ST?w6uHn6~4>0*zJYukPGaQ(ddr9t;O0|0$Nr;uknS+9zkz}`b}~(NPvpi8 z{8m3v7rAk>>;Nrt<8}n$yZQGiSnEb)Bhjj7h5RYvf{b`o_wsvUFdBYBi!S{Q|6bsF zxfTt-?iyZ|=&0Ii;rH8)s*f$k`$SByU;5Kya*9=dPlP+${~>y;CkthWWCH}U7L-X` z{vTc^p`?!HEyx!dkl1hs;TBcar_05wINt~$<4Y%j`^m!``t6Ad89bE7xATdO3ZMIq zG0DwIj=>Z*=tf=%D@1{{6j+dB43+Osr3;DGD}WIu>L`j|i{?(;onaX`LkuY`C?ARG zhGc%8%}na2hA3C>SkLYaWD0EKEm54! z6JSN{f;;v=-buDDy(=!qUV0t|=R;%>zta1RQ7}&;$RXgh*o_FCpmHd;1!E1Zd`i%( zP;R*-cZjqSf}l10HGyMQQ3;0<)V-2Q&omDVM0y?J+#tIZq4sz2Z{%xXJe&egpK?bW zPYA9B&d6fB)72y@0l*#izegVCGJyw00OzYVNbB|CuN%r@7E3GPAVPOFls&{{=(dJ3 z0=Kk=u4yQvd~2w>p$wgOYv{a&vj3Fdvl_~NCco~6vR5+r9o|qzOqo`Fd)T{*x-5xv zR#A^7an`CP`6*h}B0s0BYLuTbs~Y6zgjM&;PtK}{{5V$K#}CnuRJIHR7s>pa(04}f z&P(L!nq5Y!9$vyjEqWUfVnB%E8KhPpQ$7|MRA_X#Q}Ge&dn0%f*H8GJRai^FALvE0x2 zz?hv&uVVSb%tw;q(WsP11d5X4!#W&S(VmJj*ujs2U*io*Jj^~UZEffiu;l2+j}KJt z2d|n=jAo+N8bT4_Susoc=Q&KZY>jCvtFPj;Eq8`XwOXwL>1jiGO%)7#;fieLp|0Fl z5FQ;_DeE_EVP0fqFBcY-GW54x5HPl&qpC}!?Uqz*eE6)}Ee;1Qkk`m%h9vWC^3uc`;G4DYf^THO{jo}G`~gNq zo4^39ct(6Nx4Pq(QY3YTvN4Yre?St!%&pcxBm}w=EB6H=-?Icd0RrjBHW=?Hkz+cX z-m~}CpMPrD&EB*3w`2-cBnCYd?vTA=s4$bAEt!dZ z^7u6#OSJFIK=fqkTp(#LR00xGO_0^GGuhhWs_(ld^w0XfQ^O^VYr+N6YF61r*08)z z3@zQp0|wEt)kx%tgXyl&kPh25615}TvIuJ&xc8giNiUuT|Cm6$NCoS7 zh0pJTY3VrYJ%nUma2AqNTea(jF7X6DuqA*1^`wApohdfD{7m>qIRcEi(6<~UgHE8- z87gW9Q8K8k$|Z!-)AxzM^-|yfDHuCp;V8q#$Fb{1A^Vyf1-!nEi(1_H)sLt$Htxb5 z8RS1R!{K&wWSz&>*7P4FKLkfb$L)|G2JP^cofo!g`q%7G4YI(?&h?7C#0}(fuZaL_ zlMz6k5Y(4?eF_UgmT1JaG`s!yPp@#Lmmjg%;Qm+>*|Xz4n}ss*Cw2O^zLvkd+R$anZtNiHhqMQz3i>X=SAkb z8Rk3vsCTj4al9~Ob8`@nmTRPifVuhn8uNN6&!xsVW^KuMH@hQE`yPkh@LrIjHy|@C zWEami?&HZ3%Wu+)$NAQt{urLVkaFA==)jWK(S~&Gf_I)=o4*k6d)mL*_wdf_qFyXKAOcxX7-c- z5@c}>!;yQ>`NdrUeJ0&vbgf$V`T)AH=y2$?u@qP1Ep8)>BCyoGNh?Jf$&vhhKBh$9 za!RDD;QeN;6mF79zKiPecctH=;N){(OP9A>o)H-J@gDCF04#uCedX>5IbE z$rODqS_n=m9thc54ct~02-yk=Yp7;9m7ry--O;Kds9Yn$%_zCAk10Ng0FCIq3&!-ecS%SSdko|Ajh0ev9{+zc0w? z&i$oKk^Dw*#5cttO7Dn$XLthGC|ytP?wuJH=Ap;cW);?OJc#T~9zh`Ff(JCuhER?n zz8f6CukbuwmFl~m`xEK!-{rNQdk5EKm%5f8i}9TK?6*=?Wa7o@*`wyO$INF<{19(b zo<%0s%T527WBK1F4hd6p#`mn2`15mxUzU0J)@$61~oMvLx$LT)*Sh0Ivur~}yGXEMrN>~(M^st(n1&mML zWvvt$i17-y`yJQ9i)Vz+Sc%c2C7J(Jk0OXOSAu3O8&5asw^jKF=zJtXH9lNII#!m* zVqA*UhjvZ4!Hygho1^#*qz&P*&7+OSIe^k4o7MM-$VNtbbvxl5g2ipqTkK9!a3F1k zz96^WXI4&UWVk!|Qc~iY!kRbUj9)!LtqDz~{;NaTE*vF8Dc&lc(6=6Flg@_5xt3X{ z+OxvfiWD^26}p6)FVxJJ;g18a)~EgptV$j>|BKnPCK3+EGOd?Ci&{DVG!GRl=lF02 zB8GFJi<2cosPPGHfu{D*W-x$g#v@&7zk(WLxla)B1re|x0+KM%#UeJ}66u{h|Erj9 zZ=`qlq9H3-6ia(Uo1a~*WKWU4VLsOk61D68HhaGeOszfQp6|&TGKENt9h(V)Ai>01 z3!`PmFnJLhw_E}vFlsCq=D5M6i>NjV1vOZ2CVOMIFbb)99_X)UV4u7`=zVSaz1w`x zTAq8UKskbSq33h6-pK$qzWYJ};fV%LAs03xev68)=#f6vXT*H|PS0ktl4aMnkQFNqo+Lsuw5Yj@#)nyHf zb0leV_*7u)gg0eQAcmonHW=gPGA*3CgeYC-(z+6(weshr3F49o`|DbkSr@fQ#3I_d zy-<^|Mu^5!fiTG>^)hc<%Um+>GNbW2wF;Lg=>_^kZ0Gu{_nhVP>|1h%9>_r--n0uz z>MDq33;31evPj4fw{bPRmHG(Sa%r`kjAZRR&s+9mYp9HB5)sn2t4^2|q`vh@%oQ1) zszldvj1#Cw4BZG;kThBa9t?ll;P>1628N=ysB(vlFK?c{26joj1ayW88-R7q@K%1Ga*+>cVsf6 z1@%jNm8h$FPb#Ud2)mywtmh?@F;`emsRagSe`ry&lIG0lL|P5*KTq{diPD&QdP~^O z8_;Z;3j{BPwU=qd_~GRW9EL}hVzASY8b;}^#C^&9VV{X04b|gKvMk5;_YVd1_jxE1 zg|cDgw=Pb_bkrTI{wx5z4zaWA!JpA1@W~1{zY_c?04;og;7{9v6Sg3+Cg*F=RkUDCQR`eHI2e})@~9M^+Qa`B-{1=!V! zk-zLbx?dN>aeuKx*N@xkTp??nYdhv$s#8emyTYHUPC4mR z$errY*NIW+YK9{XV*ET;5($caEgX&B)~-8l{gccm$#&b+l)fobxxdvKI-T#Z;~=>X z4(GpR?Mv6ym1nscU`xx31k^*%V4nKePF?OBE1$JM+}Ni(ic~w3^g^VHZIe> z`a&5~4O+VY*Fl9t3`UKqxSbVbOg*VFCGyo0`ds1uHKgMtg;*c{A2pg`G8$LoxonU0 zi1w-kow6uq|3i7I@5jt0;wO`75{r_zev)mK-1}AU%EXs202_ma9gKR_}n-+L270y(;fQv`Ul+H1^>eGzLc&y;wM|ZVYWe@6_kyZlZLAyXeCv(<0BCrPr}p zq+Q@HHoK@U+APWhx#e<=Y_nK(fqENx>M2DapT^MwPq>ACi5i1es|$=rL;|W57SKsD z3u6?W1;t>%w@}{(>8l} zx{NcWKAn&Cz&zuWftxwT$bp-=M&`iH&Bj5A)@FXA8G8qAYK%PtH;aw#{+nn$y*Jti z?pv7y<8RzE1@6=oFx5=KJ0cq9P~~AS^YV#$Sk6P{1;$wBMfO9A7lOe;OcF((sDNgY zk$sI!(@)g{Pwr^6t)UZH&=;RIp(i_XC%GLR#wS-&EczSsyIr7CX4Wsy1`*08ovP>F z&bMNtc!qJ+7%3ql+R!*X_cETNN{*?A8;m<-?WpE`rTjNZQM0cDHfqWI-5)^`=Z@x6 zeTP&!-f*bzm3%Y@!Aml{pfp*!KQT|w{hB?gvT>e45!(E8RTtBys3)Tq7ERCHDkTu( zaB}3)l?=*3-(F1*600B!jeVo#!|({`XG&6H4-cRs5;*p5Ha0*-P<)bf7<*YXQo}#y z^?}{O#7yWQZn2sT=qd;U(SaIoBQ;3T?~I=+We8)9N>!_x7sM-M(OR0gL(iR(?*476 z7onR6b^jSyMm=|g>b`uZx<8$A^yf)fM@S-^M$T3rO7{)orP2LUR6`r2`x}fLv-@{Y zTqqKv|E5?-<4o!1v2$Php)~p{s!Gp&v_PigcyqsqQflu1N|B@H{!^?i^xWA}Vqos? zryQN%P4aSc?!)Rs>3p%-`5RPYsky%hiiqMbO0IKr?jNIf=7gsgKq%VO%nN@G?uql2 z;fEEEI^KDP*#}yb$*Ga2h%e{|>J)A$5l>4;mE2ElMmjs;-6u+x}xB^^8tALrQ6dF(gmo8Po*M}A|*bVv4fTV8;G zz*qFg){J~U4+H%!x=td-rdEIR+4MpQn;$LQ!&8KED||X4+_)>y3juHZ&X-KaW@c-( zU7Ba#qI_cpYwnBPb1JOxkN*P-6Kf6FC)X+!A5=xpogzs@@F;0&pgtNi_zCJ8;axPS zYaZ<20bnO7xV`A3c!3C}v9OiL{^n3&2^PO_IexetglS060Sl+UxilW)Zq<$I;|<1` zPAUo)f#X^+QK}TTL_N=h@<3UagocA2hAI)2Z~0bhV{X71m<6S*KWi zf0qcSSb`b8l_iw)AwpWgFaP!2OMFnI6zV^hCDH*=-xVe=9a$za1B_9~=1+VLpB$!! zgssE&{nA~*SlCLyaXazrkywk%Rezex7E5y#eCUa;`?1C#VCM3+v@56!Kd*bvN=c+A ztdXj_jXIyLtuE4AzR*@@>+m%XNp21xxN7^%UgwLnes|}_I?MnT&%&)y&9A8Sz8gaK z)gX|#5Nh*6a(TrnS3S2o%xnxL)mGw5&9gCdLjA#VF<0)Gny_IF=WYIe!gkn~qzm~3 z5b=@SP2Z&NHZITMe6W38x~uG3VyIMo;cNYe!&kqy#@~F0g>!@KXNdNVDHCPgg)cxo zzV*%_cq?heT`P)Zgaqtv+E%T%p+|RVYnrR9z7A`N*3f4#T}#f=R%||S>BqnSuQ%L; z5w&Uu;#%85NRu^GdOlv(vC0zUQrazR3cxGu{jV{WdJA768c~_q)p>U3-tb4FmfR+)#tNijXggD{d^T=TfBM zqSew5`eU;*aI1;UwqoKJa8<5BYTjkscZE!2D=|R`wd}=&`yIB)b9{(JsgebV7d9++ z(_RS&?{)pA4OyHp2*mA2#X~$>g3fM6$73~iVfn;9auAw7L3Xgl!l$SPdwV}yN1e`? zU7H(mvzVWGvA8b*3n6cy?9VBy_9Lf>!ku%EmIP?W+JvG6XVB;(YIgvtRV|PZpP>sc9l+`7PLU5xrN7?6YdNom%m?oRM1O5NgMfXEEt- z8}mLBX_~7^*jnt~GbG41*r7_(XAm5|By@#epJDe!4C|elz5_ctrduZy59Wp?V^@rC zN?hzaV6+`{NKB9Mwk?)RGXP>g>R}ReQb53070Bo>V7qP2ehT<4)9sPJ7uM5cMtEd> zc?m+H*u_?#itT0mlAu2IOA0k*8#o}0LJIScB7|=!bOUGWlt!0>ZTZbgrhL3LB~b&a z7CdVgi*zlf?Woozmyxl=!(uEUu6m&MLVIWorVd1KJVBgUWi@cT8k$<~4PfzWPES}&zloKtY71p4u{4-EF<$UBqA+tcZK>77LP6hXtU7Qw z*vNhj`65uhw&OeL_6)LRzIt! zumsp5wY}+M8c~206sGVc833q402Wm(;e#7hbpp6T>%DTZLK9BVL|i0AOp`y^73uwM z$qbKKa_Ka`r)OzSi%?IH>i0$9=xVgj<_{_bl~slqg`X=$ooUk@9;SD*U@tInf zsd(PRMGHvnu|1)wI}Q~bz7dN}Xbm*I5iZ4CrX(~Pr$&1^Pa9J;VQrAt-3DYrKe-WX z!X*B&R^O>xTGk|r%&_t~b&%Xo2g}c2^6SLURKR&U$fJjOv{X;jpAB67d$T7F=40&j z6^vo7##!I1=s(FXk1^0KpALv$AHi-Je`Yvz$fslo$I)$}4}#Dea$H~+9NNmYoD7jZ zUnn=`do|LVv5@ovyNE#WH+E|3q}tz#3G2rc3+SU(n?#}jB&txagd@OT}5ODnnKKPSD|c4w9dA{{umCa?!yLm_dGB#uVp!|y~}1buYA7OTV& zxp#qsBs_^5B~uTP?1!5eherX=LG%>K&0^_~Y=3i()e`y2YTHI_;#F%$XSHT}eFfa> ze$Fn>LD%E0{+Jz7VSCVq@y>>eC}PAuF;Too9N2f*Da4=T5-F3fevezIw8%%+$odQ) zQS0|lc%@L5I1k`AUsHi{Bm5nRal6T2kNxGYc{Ps!(OD1?N=z!|EH{x+oqd(;^2 ziv)Z43oirxgFR~GOca5%xU5j8J5apUZ^LPKJ!yds24Xp*K@{6%pVt`C>J+N5S>j^C z(BD~kt9Guq^Xc1V_w#at@#j7e0Ov;km}p3bBxSwnasA6NAr}#5_5bPPFW~-!Vw(%S zmIu970KMi8=yw+h&GtjM*`p4?W!EddR(L^W_I?S=o${-_n^({j37h16iYVKp$gO!+ zSMy!h%M@fZBQ4G#Ux3IH_1s$?Q@Rt97i#6XZ@rXZ5xd@gz1&K+mc{sO7@S5>lhaIr z!+)B$m=!Z+cd{>#9lb-{AR%EvGZeq6{FesHXLcHYps3h%{+2q;nzU3+s4%C^rR~Hh zv%!+o^SgLn!}Fx$k1xQgPapYws~y!yZBN4mBkz-44~0*w;oxH2p9>e(P&@Koj*dtzP!y>J4MZmJ?-;pHzu*>vybo*f`zDIi zH8$>^7h#%XKRA(%|$q>+WsF`fEfA1~m3A6evn(k%IX)h4`21m+yqMOU z%>UgM#WvOX%R_}fGP8gRp>nh0b03sqwFO#ip8a);n`_$#vFq8%{LM54KR_;}wL;SZ08lVm zTi+1uuL62b7u0O4*gZu3TZ>wTH2b%fVQ(m=6U>hm`O+GGTD(c5>&M?yB0u~W=Z?E{ zzwm~&n}ENbze>YxVjR`hJ4B0}godS}lR@t;Lf@W&kjuI-t3J59tUmrz+UoG{E^E*? zL6eOVyqN`-T!~aInaElBjr=%Pf+mv5{P<>BJcC)qz@FsJn5bhpwTq`>vCFvKu4P@A za_v?Qk(ttfH}U;QpDjE|uAPaA>XFt-KcJ8ScgH;BQ^zes;Y53N{7M{OT0GGIVQbE7n8veVYWGqn9`-ac! znNEh)`7pI9U#A}I3TrVF+kzyx%lOk-BKEPC?ou?zB7ml93knd|a{HZ$dr`=4A*V{O z)29d?nM*yRuKXlzX^R;UdSzJgkk>+dL;A1(lN=(m!*1Blb=QOlM zugoEa3)=;;n@HFFNPWi7Rr4~k?AkrzTu{}OBBZh59fjW3n9z}WQ{9zf#}ShEh?mp8 z7v;1wwX!B$E&$ix4X*zf^(>p!=#I0RsNP`~ngx2rfPw2S<416!ve~#1^ddH1po=^u z|Hi5m_51R7>(Glpu31HE+_eBTjAQYCvgeksV_HbOlsL0ASx0?dIUw|7}D2rm1dk6r6R(1Y#f^qCns0RTo zC$QLWRGzPDgTCHiY+A4EH%OG8`zgC0`weA9oXp>ezD|I4Ya-W(y??9;M_P6}V85Ks z!GqO_U+)XXe(>vAhm*>$!3gS~U7$aBc#yvu=@J<< zgn@4bR38Q}SFaaPRpRT5lKGAeP)EI2gezlnUZfE5$vfVk6H56Haem&U3{Pn*QnzavyT6kIzp*B z&~743;c%y(Lh4Ch{~R{K91}5mV!6MNYQS_dq$abP`=y%6R6_-H1{IH4%LlR!sp|}C zNzaX{=X$BfVb)V^)^nlM^ACiMN7eKApnCp@?3pQ32z>-AsDDLkO{$SN%`K3t zSbm6~m8fDY#wKCE8NwyP-95zM3*vVMCNt0JyeJnqM@TsKo1)CjtIu&|zM z({N1Rn9QFhy**o$;~R}>ywSU;mHOoiYLlZuD741HdLF0)BAGv$g_Mb#dyE{ie@*b( zjpc3?S)B-~@;kXw=9Y}>Z(JK0N|c~rB}?C)%wI;835Rjr+#{AmPE03j+)D%R{G(~;l@%&!TjB=dmnQQslWys`kEKg5 z*)CA)Xp;L#VXr0Go_N6Q$X#@Vqmn8H2V7J8p`Q%r%!QcPp+SnDbqj_B#2?`>z(P`C z#jA8}q~2zB@j|_=&{jcQiv4Y^;Vjpx53}dnlNl1bWO^Rc+QTBkd4c+2FsAmXIUE@K@M%p*3|K;Oha3y7bKXSYCnmT~21VHEUnzxo~HtaUuZ zPcSw`S^Jj-JRi6k1rSYASVnxJY=Bb+bw7V__lK$OJCjhZ@DZF6eirQ zwur#6Ch!lJj1RpK9d3D6B!5bTb^z6~l}s`vX6_wB0{SPO*M}L03Zy#&mS-5@|F9je z{C-RLw6$5}MmHm=-lLf$mjQtV1*ohb(Do}QPc;A41i4sYOfCC!t#0U@Yqq78cDCe3nst z6O2l4D61!O!N@#eqld$Gwiy{LtD`4UJMxfuu$2tb$foy`Y-3uLrm+|eK4FmO(^`H+A>3^1=cg*H12sT znH1jhb&As~VP&H>$vLko>OWt#$SJAaIUIE_3@4PIafyN$Ci?`Tfks7s#H>VqgneS< zRJm|kBC`sT=2kk3ulg10Z@w7`6sXpevje^k_&dNx`-&d$jXcKx1Skd`r~TtMvh(+8 zwUfK$D$vbI8^BsOrmjp-rZ#i2^Hx5%X*?etsiKNOLb3$y2?{wJM(5 z<9g|Wk>RhIkNsOH1Y~Y7?xhfQIukFinnO*oSWVoabE<4hzznm%3x3KqAFbAvHK>WiK%=G3S0Kru{M)`|f>^6?Zke9SqBi8t~& zTtM`lQJ+(HOd}s%)MszWNDM(AAkl!_q5Z2f=wQY5p>Y-SLwOa8TurlifCnxJGvVs= ziA$v=F|^YvX08&b=ctv-;AtFel=KG$kzr0-JKPyfItuEr>T zBSigdlw_ijcy=ft%x6U;UO>dJcoyQjK065ucuA&JSJOYFcy?amde^GrOf%|_?K!Wj zX`?*v^2X&C38~86>TM({@+&;hZQkWpZzBRxX*06tu(xG6H=yZGh z30|ot?vpavZXca)*%q&tyZB|HnIZ@lo*3wpn{#YyKmNEq|v1zIrcLidLtpQxk5(r#7>(6(GdU+Tg;V&ESY7&uX z$8@JP{yGjW`SgOe+~6!ficvON1V%ZrR_EXxzKiF zYn-elNyE3LUm$p^vPl*L5>t0Mh?e`8@{c2+*|*wF7j(xh80i4huH_jCydGMKjI-QQ z=fnByD>Mz)hfd+|{Ll#g0@>l?Kz6PP*#Nri1F3;k+3RoF=xsUVZ$aDgek?2&pGB@l zO~Lqb1>+C=p3>$a=PTYc0OQ2-w1zwa&nk^H9_8*fDXw%0O(JEg#=t* z!0=uG{b^qE^$&A)IJN}nhc(rYrq=lP1@dPS zrY$83pEpaeyYin>T6=YBgT@D>Uf}T~_JE2PSflB2aYl@3hG>pQ9ON|F^_2^n!|VJ? zv=XAsD4W_InkkA2Bq%DK^rlXzqdy^bUE zIw+~@IIhQQ`tIn=f)tOS`Z!mPn#G*qcrX0#eG}g5|F&=E%}e*~vi~oA6F%l>S{hg*$QUn3Em*jxAex0G2!Lk&4<@c0~NJiI}cZ78>q z4M_;(x2Wf6d<8{{X!r9nt#)hV;9;Wuc=#yd#>cGx<3y-w-Qqc({PJIASQfC5q#1Qg zmZl|+3TizO;pVI}d(cG>Fs*=)h03iFN>}NPgsPpR*f>QI%kpYQE})edYph3(B@KSW zui0B8TjLQS*9LE!T=8wtz$GOsX@NTGjhw{te zp$(aAcy0vd=A7qrbs!@rM@1+HD(9tjs@Ez!#F6|xauiuOrB?cuQmMUEIMe)KT;!HK z>z!Ep;|AHAC7rPnj#3;jq8S+1@wolX_;O}T@tI~!Ycpv!3m2@76RRkI)(wBSC+Opz z^Q-kkfwtrj->cNcL0Qo{OSkt0^~-wE7MjL1;+B&O&KeU&4BlI2PdMst=q>wpR%}vb zQ2e+*&^+I`BCFYXt8%G>ASt<=OhZ5mt**vK9%E>hQAcQ8S6!48O8a1ZGU5bWVNN$L zm`KH=!-Uja-mb(o2-i!W zyEKFTw}*$|u{1Vu-bxu*UDOmg)J3yJ_B!rz^W%0Rtar6Ejua-a**|=$mkiHcwOIY1gnpZ@UM)Z!Oa))cIwXE+7EKV2+wlh!T(b}WwD|kVp7VyCIT_7d z5&q0?&NSfww5{L9?6mtvS-+@I!X7I$lZuZ%7?zNq{C3LWB=8i__cFg)+ac`iz7i-3 z=#B9g=ZYLS1xd0V0!cabQ?rf)qzhve873eFJO^BnOIbgnSyZ6!KEnhaYejA-w^@Ns zvOO;A8zAieDfQ)9;j9i@mRkP=!D392VzNDG>xq9ta|2K#Ya@_17;2~nsOkG}*~YHM z-*5-iNI{_H5&kkN#@AMgA($w<-WDUmV9YKf+bSaiW?F%n63pZ3DK1MOW32CEc+DWC z_C#%mEmw?~(jPPjTc;*2#3D_~(|1{DY!I>%XPMr+V&%*-8~?bCYEq@>!g+KdR8{q(sRbO}j+XVULIImEsX- zRF0?Y7P>hing8y~Tz450-e4O-)F+Ek736OGzL{VxyFzK35bu~dUlg(j*QksUeyt+UIV>t3Gnj5VB5D&R3-5_tm;qYkVOOid5?vIuj zL;4$tAA~c<2%X8<$(7$T@uNp&cHZQM814Spg;w^aU?;5_GBTFij;>OWyeVjoO<}Hb z+A3T8`^tQcZAd!!&ewnj08i2rjeBU2=APikoUr))yjQuhZsJ1t5Q%v%R!!YAT&u6y zSI${COZV?9B6*3>CuOe#{TQS_S@;yczT&N+X+F=^@M%7M?SU;c7(U4uW42M+--dC` z`GDGgvEO|9UMTe8^tf6-7;lssQs0G$a!{^3d)TNWl_*5DouZ{kuge)^2MW`k@0}vSIKlRfL(tD)3tVkT@Xw9ss ze_x4Q03G{aR1XIcYuETx4D--kGJ{hk&h0#MDc$aC&P;9ulGaq)FpcY*(R^9YRG692 zrShz@CD`L38f{3@F8O@TE??}f-c;e_Mk(v5nz}Rmy>!_MDLb07`DWR^Bg$4V0^{I$ zCW@9R5>Y6l$bDCIi`ZFJmW6uCmfoTKoRu^bI^abq9=Z|?`V?PegfCNXg?NBMux!Qm zd%FrFPK2Ul{!mpqJRW<8zbaHv&w(?e+5WC>#5mR)jEx(`7$AhZ%`rTb?r*TRGc+-% zKd1~Qg;d*WP%jgqNMd7NFP-X*Ujq;awi;dx&$ZA3PFbT0r@cS?1D+&M5ZFa8C+l`AXX zm^M!iZ9Wg%e#`taHd=DLT_grM2;DqnTItA#w4!$pX7Ue=E%sV&1po7p*2nNuNwwJyqY zEh|xMLS`cWzz*U|?{P+2ox&DJZpyRJRV8Z`5t$NI;ZC3sxfssE^A#DJYh{1%tSG*M zOH;lr_9pqcoGc>Pjph6o|JEz%u5sDNBDM$(PdZNi85U&ik#C31@5bg!{Gtk(9t^xV zC$4VSYTNjzo&%~w3{7Gf79}U{g7WCslj-B$!-gV(C7I)CiX;h)q#A+=Yo}Z?_cdOF z3wQTx#|G-0KTzjS2iIA;3--CBKjNgOYwq$ZolD43A=3FhdzgJ}GtfD_3UCM?Z1&Z& zeu0WT#9;Ape}XVWN69ZZ=@z>cES5+LFhgR`Kz9O@rxg_Q$q;7E-F+Fek!jeuK& zX*?BmYc7WKqL#V9d<9wton223sJOB}+^yIjxiqcY9>liVrNaWz6ya#(3BNKW};z@>)kDxi%ZHbqI z`i#n9RO-xFv%Y!er{n5JUVReQeETr)Rof*MiI zb0%ZY)d33im7$9!EF;pEHO`=|3q4T~qx0RtXmyRrl@KRE42%B!CxLr0OA0NV6&u-8 ztsl^$vv-IfEINA*^)pThgU5kVy`@?o64bv4B-c(at0=;eq<9P0nr(JXHG74fnR%n4 zE1Dt-D5~aQ(Y6IMyB)Kl_U{M80=t7HsG`ozen4F;@wOO zV@ue~vF#R}-$?Afjh88e%A`8BkT5adhCdf?!C&)CjsufcWaAxi0!w5bXhgSK2AX+h ziV``c&*gsGkwJc?C_XmQmA=ybM6$zHEU15o$*8w%%IV<)ip>nrob5BhIB6bZp*vQI zY2VQn94f@5X~R^F?kH$GBwgLTLrt^~w>ynE9D`i93SD=cXMvlJ^QU1wB2BFcfoL5Sw(4CPww0##Q;eDt=5237sB9D`q?Qy7`*BWZGU(0Z>ku|cgwI@~4&#*`}C zbipu#w`1(jgI0LTQYq0y;H*{Yq1#w<=PU;uhu0tvRLbEGQOn2 z_~q2@t2fRfTVs9i%=Osg6fiexV$^Y7 zi77&C_ER!|lcBcRXfJ|bkQe~(MGELOSJh^wU1DJEcv>cNhY~TLIC$(|f5$@h#%F?{ zszI5~>f$Md0u#&(^~{qY*2vXj23XI(X8t|QD1UBTY&XX^XJB;7xfFMdl%1Bn613Dj8zx3x8v*IyQu~d2smDVrqvWT5!^@~OZ&&6OQ zCZH>Jy`woR1ywZ*Y&X0iAbupj5Wk{NA*j?@)sLT`Z6O(sAk{_-`DJUqinYE93hgS0 zi>us9884JViJ}w~IVWX|D_$nj-l=SwsM93s3iO?EkpvQKGY={Nus$voAQTW=)Ntie zv=-RFgLb0 zC!BBkO3qXUP$|$)lpBw{t>Au48ED{0N6IwVAIHa1F(@$J(wBM0elY{}i*;a3sSJUj z?e{?+#PC>P*jR=SScRu}&Ex3Wyk9G`HQPYkA6ya&u#hXgpE=`v_^}-Q*P6U_6~Sv` zHTR&#bPcz<^0YkH@Iam`&+B##_qn+*;l6}>FZW*VYq+oBelGWOHQRa~&Et`Zc7*0V zyo_)G*2MQ9CszmuYjnPOIBBeZp`Y*ldXnra24j(CO_R7(bRx|$T9!}G{aKUQ&mT%Z zga7|@&7fksco1j&KOE^W0ZcO@1TY<&ej0NGZ=hS{Rx#Wg zYMjWos`s2dsn70f7SIx7pFvYEA(vc+^(!eYMniCH{dUe?eW$2wfI{P+hY{#ce7=#1 ziF|uFJfhR_ORniD%796~2K(ekzC9Ek+3AS%KhEVbI?@v(-+mW{1Vp;r>9{fVR-cp8 z4{TIF5c!S0rQ)^+VlO8of)J*J>HNx^2iT}G zq1hEXZ~A($*U9`dTVt=+gB989_2~QA>$`{SmvUkD9fgE?s@Kn8x=t%ips7;wrA6VoDP4^ zJh!A+?poL*5yDzBa1?$|> z!uJKC>~Bcy;*Ru=UWD&bc0nGQoDw7O;ZNN|`|c4J_vXfIsR+`DS$$`|9C&JBKvzZg znCMDY&O}$@u!s{&+J0N%D-nbjKn12kAbVTB5;Ja?lP=>4Dn`ip9tS!{YKh((G((s9 zJYKs}6^X!eWf5aG-s9))!1GhPK9E*>ZHr)reh~^oZRFg2+8^E#a}J^2j`I z%srY4uEkdF(;ZdlSU>IJds&}w5Gnl|*V95YcQOxXZ=tjo%l*wueE%M*5P2j=j1sX3qz~dZygTqiP zJqSni5uXWi1frAquczvIgFP{t4`)1^bt@lNy%Z%~(>JsP#KjLBJ+6}(mrw7gj#VSanf4Um;9D(9h+ym_ zZ=mgCd$nx?D9TqhI&@yyRb&(9=)hl^WPG01MTJ$d8mq72pq!iwPV}{8;(P9M`p+)# z+ulx3% zk3rHwFFD>o9{3hsJfS1yx9U)*)p2MN*~v9R2#5^Tt9*%QLK)qtz%KEHChlPmC-cvK zOxD)%r@N82=R1h=!Ob7INo_>Vn1;R|0I`eQv4*!iqRtq)D95*oV0xHFKfPu?sf5o6 zpG7+5;)cGTzXN!=8~UFAz+zcAwxRED^7{!KVw!jcO{M7VD`E`= zNI|MdCEIJoPsHbXd`)RG)$B=wn_ZD=wsn3f%_dGuHOa4PvLMyus8o}~%q9uMyy~k= zAQwRq{0JtcL6*x40t&jSI{CL*LiM&1Cb&#aQ4OC-Ia0kkoYnyDWI$I8Q@&gdobN837tfEa)fTJ+)SAf-mk@`%+MkoV#0Y& z_!9|zR;CqyEx*vR9U0oz!}I{E$j=jm$nwICv}cPzT&+PzFVGMOpZ<&hpo*h+rmBN4 zR2_T_5z2>YDoT`Zdx0lBgR{JbwwuN8`1{_uwxQy{Mlnxl4@JUeQEMxPP(o!#=OmwOj-e)Jt zdH;=is^r3>Bun6M4MAosr;H&%H*d!OLM5V+v+gKjSJpR6|8P>eV85 z69k%_smePg{n)+IbOfA&^FN+p;rCK88kkV}E32$H6*YgURIwxI` zXLl+$fvNCH`EMV=pJE95^e2iC7?;U0m(Fu&W3*%zflu1W+hgp})A0&yKvNPnr⩔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