From 6d410cc1b79c8ddf886d1d0919a39524629243d6 Mon Sep 17 00:00:00 2001 From: Markus Isberg <3e849f2e5c@pm.me> Date: Thu, 17 Mar 2022 01:25:04 +0900 Subject: [PATCH] Unstable 0.17.1.0 --- .../Characters/AI/Wreck/WreckAI.cs | 2 +- .../Characters/CharacterNetworking.cs | 750 +++++++++--------- .../Characters/Health/AfflictionHusk.cs | 37 - .../Characters/Health/CharacterHealth.cs | 8 +- .../ClientSource/Characters/Limb.cs | 2 +- .../ClientSource/DebugConsole.cs | 144 ++-- .../ClientSource/GUI/ChatBox.cs | 2 +- .../ClientSource/GUI/CrewManagement.cs | 6 +- .../BarotraumaClient/ClientSource/GUI/GUI.cs | 19 +- .../ClientSource/GUI/GUIColorPicker.cs | 11 +- .../ClientSource/GUI/GUIDropDown.cs | 7 +- .../ClientSource/GUI/GUILayoutGroup.cs | 4 + .../ClientSource/GUI/GUINumberInput.cs | 9 +- .../ClientSource/GUI/GUIScrollBar.cs | 1 + .../ClientSource/GUI/MedicalClinicUI.cs | 6 +- .../ClientSource/GUI/Store.cs | 59 +- .../ClientSource/GUI/SubmarineSelection.cs | 4 +- .../ClientSource/GUI/TabMenu.cs | 347 +++++++- .../ClientSource/GUI/UpgradeStore.cs | 65 +- .../ClientSource/GameSession/CargoManager.cs | 2 +- .../ClientSource/GameSession/CrewManager.cs | 148 ++-- .../{GameModes => }/Data/CampaignMetadata.cs | 0 .../ClientSource/GameSession/Data/Wallet.cs | 26 + .../GameSession/GameModes/CampaignMode.cs | 4 +- .../GameModes/MultiPlayerCampaign.cs | 61 +- .../GameModes/SinglePlayerCampaign.cs | 18 +- .../GameSession/GameModes/TestGameMode.cs | 2 +- .../ClientSource/GameSession/GameSession.cs | 2 +- .../ClientSource/GameSession/MedicalClinic.cs | 6 + .../ClientSource/GameSession/RoundSummary.cs | 21 +- .../ClientSource/Items/Components/Door.cs | 4 +- .../Items/Components/ElectricalDischarger.cs | 2 +- .../Items/Components/GeneticMaterial.cs | 2 +- .../ClientSource/Items/Components/Growable.cs | 2 +- .../Items/Components/Holdable/Holdable.cs | 14 +- .../Items/Components/Holdable/IdCard.cs | 2 +- .../Items/Components/ItemComponent.cs | 10 +- .../Items/Components/ItemLabel.cs | 2 +- .../Items/Components/LevelResource.cs | 2 +- .../Items/Components/LightComponent.cs | 7 +- .../Items/Components/Machines/Controller.cs | 2 +- .../Components/Machines/Deconstructor.cs | 4 +- .../Items/Components/Machines/Engine.cs | 6 +- .../Items/Components/Machines/Fabricator.cs | 4 +- .../Items/Components/Machines/Pump.cs | 6 +- .../Items/Components/Machines/Reactor.cs | 10 +- .../Items/Components/Machines/Sonar.cs | 12 +- .../Items/Components/Machines/Steering.cs | 8 +- .../Items/Components/Power/PowerContainer.cs | 6 +- .../Items/Components/Projectile.cs | 2 +- .../Items/Components/Repairable.cs | 4 +- .../ClientSource/Items/Components/Rope.cs | 2 +- .../ClientSource/Items/Components/Scanner.cs | 2 +- .../Items/Components/Signal/ButtonTerminal.cs | 9 +- .../Components/Signal/ConnectionPanel.cs | 4 +- .../Components/Signal/CustomInterface.cs | 11 +- .../Components/Signal/MemoryComponent.cs | 2 +- .../Items/Components/Signal/Terminal.cs | 22 +- .../Items/Components/Signal/WifiComponent.cs | 2 +- .../Items/Components/Signal/Wire.cs | 20 +- .../Items/Components/TriggerComponent.cs | 2 +- .../ClientSource/Items/Components/Turret.cs | 6 +- .../ClientSource/Items/DockingPort.cs | 2 +- .../ClientSource/Items/Inventory.cs | 20 +- .../ClientSource/Items/Item.cs | 173 ++-- .../ClientSource/Items/ItemEventData.cs | 35 + .../BarotraumaClient/ClientSource/Map/Hull.cs | 212 +++-- .../ClientSource/Map/Levels/Level.cs | 48 +- .../Levels/LevelObjects/LevelObjectManager.cs | 2 +- .../ClientSource/Map/Levels/WaterRenderer.cs | 3 +- .../ClientSource/Map/Lights/LightManager.cs | 6 +- .../ClientSource/Map/Map/Map.cs | 17 +- .../ClientSource/Map/Structure.cs | 11 +- .../ClientSource/Map/Submarine.cs | 79 +- .../ClientSource/Networking/ChatMessage.cs | 5 +- .../Networking/ChildServerRelay.cs | 23 +- .../ClientSource/Networking/EntitySpawner.cs | 2 +- .../ClientSource/Networking/GameClient.cs | 31 +- .../ClientEntityEventManager.cs | 26 +- .../NetEntityEvent/NetEntityEvent.cs | 11 +- .../Primitives/Peers/LidgrenClientPeer.cs | 2 +- .../Primitives/Peers/SteamP2POwnerPeer.cs | 2 +- .../ClientSource/Networking/RespawnManager.cs | 2 +- .../ClientSource/Physics/PhysicsBody.cs | 2 +- .../SinglePlayerCampaignSetupUI.cs | 11 + .../ClientSource/Screens/CampaignUI.cs | 27 +- .../CharacterEditor/CharacterEditorScreen.cs | 14 +- .../Screens/CharacterEditor/Wizard.cs | 11 +- .../ClientSource/Screens/NetLobbyScreen.cs | 5 + .../Screens/SpriteEditorScreen.cs | 81 +- .../ClientSource/Screens/SubEditorScreen.cs | 51 +- .../ClientSource/Screens/TestScreen.cs | 29 +- .../Serialization/SerializableEntityEditor.cs | 19 +- .../DeformAnimations/SpriteDeformation.cs | 18 +- .../ClientSource/Steam/ItemList.cs | 6 +- .../ClientSource/Steam/Lobby.cs | 3 +- .../ClientSource/Steam/PublishTab.cs | 88 +- .../ClientSource/Steam/UiUtil.cs | 2 +- .../ClientSource/Steam/Workshop.cs | 9 +- .../ClientSource/Steam/WorkshopMenu.cs | 40 +- .../ClientSource/Utils/ToolBox.cs | 32 +- .../BarotraumaClient/LinuxClient.csproj | 2 +- Barotrauma/BarotraumaClient/MacClient.csproj | 2 +- .../BarotraumaClient/WindowsClient.csproj | 2 +- .../BarotraumaServer/LinuxServer.csproj | 2 +- Barotrauma/BarotraumaServer/MacServer.csproj | 2 +- .../ServerSource/Characters/Character.cs | 2 +- .../ServerSource/Characters/CharacterInfo.cs | 6 +- .../Characters/CharacterNetworking.cs | 730 +++++++++-------- .../ServerSource/DebugConsole.cs | 133 +++- .../BarotraumaServer/ServerSource/GameMain.cs | 15 - .../ServerSource/GameSession/CargoManager.cs | 13 +- .../ServerSource/GameSession/Data/Wallet.cs | 43 + .../GameModes/CharacterCampaignData.cs | 45 +- .../GameModes/MultiPlayerCampaign.cs | 226 ++++-- .../ServerSource/GameSession/MedicalClinic.cs | 2 +- .../Items/Components/DockingPort.cs | 2 +- .../ServerSource/Items/Components/Door.cs | 19 +- .../Items/Components/GeneticMaterial.cs | 2 +- .../ServerSource/Items/Components/Growable.cs | 15 +- .../Items/Components/Holdable/Holdable.cs | 6 +- .../Components/Holdable/LevelResource.cs | 2 +- .../Items/Components/ItemComponent.cs | 6 +- .../Items/Components/ItemLabel.cs | 2 +- .../Items/Components/LightComponent.cs | 2 +- .../Items/Components/Machines/Controller.cs | 2 +- .../Components/Machines/Deconstructor.cs | 4 +- .../Items/Components/Machines/Engine.cs | 4 +- .../Items/Components/Machines/Fabricator.cs | 40 +- .../Components/Machines/OutpostTerminal.cs | 4 +- .../Items/Components/Machines/Pump.cs | 4 +- .../Items/Components/Machines/Reactor.cs | 4 +- .../Items/Components/Machines/Steering.cs | 18 +- .../Items/Components/Power/PowerContainer.cs | 4 +- .../Items/Components/Projectile.cs | 19 +- .../Items/Components/Repairable.cs | 6 +- .../ServerSource/Items/Components/Rope.cs | 2 +- .../ServerSource/Items/Components/Scanner.cs | 2 +- .../Items/Components/Signal/ButtonTerminal.cs | 6 +- .../Components/Signal/ConnectionPanel.cs | 8 +- .../Components/Signal/CustomInterface.cs | 13 +- .../Components/Signal/MemoryComponent.cs | 2 +- .../Items/Components/Signal/Terminal.cs | 26 +- .../Items/Components/Signal/WifiComponent.cs | 2 +- .../Items/Components/Signal/Wire.cs | 22 +- .../Items/Components/TriggerComponent.cs | 2 +- .../ServerSource/Items/Inventory.cs | 16 +- .../ServerSource/Items/Item.cs | 226 +++--- .../ServerSource/Levels/Level.cs | 58 ++ .../Map/Creatures/BallastFloraBehavior.cs | 55 +- .../BarotraumaServer/ServerSource/Map/Hull.cs | 320 +++----- .../ServerSource/Map/Structure.cs | 2 +- .../ServerSource/Map/Submarine.cs | 12 +- .../ServerSource/Networking/ChatMessage.cs | 10 +- .../Networking/ChildServerRelay.cs | 11 +- .../ServerSource/Networking/EntitySpawner.cs | 59 +- .../Networking/FileTransfer/ModSender.cs | 2 +- .../ServerSource/Networking/GameServer.cs | 72 +- .../ServerEntityEventManager.cs | 27 +- .../Primitives/Peers/Server/ServerPeer.cs | 2 +- .../ServerSource/Networking/RespawnManager.cs | 10 +- .../ServerSource/Networking/Voting.cs | 2 +- .../ServerSource/Physics/PhysicsBody.cs | 2 +- .../BarotraumaServer/ServerSource/Program.cs | 55 +- .../ServerSource/Steam/SteamManager.cs | 2 +- .../ServerSource/Traitors/Traitor.cs | 2 +- .../BarotraumaServer/WindowsServer.csproj | 2 +- .../Characters/AI/EnemyAIController.cs | 16 +- .../Characters/AI/HumanAIController.cs | 40 +- .../Objectives/AIObjectiveChargeBatteries.cs | 1 + .../AI/Objectives/AIObjectiveCleanupItems.cs | 4 +- .../AI/Objectives/AIObjectiveCombat.cs | 2 +- .../AI/Objectives/AIObjectiveFixLeak.cs | 1 - .../AI/Objectives/AIObjectiveGoTo.cs | 19 +- .../AI/Objectives/AIObjectiveLoadItems.cs | 1 + .../AI/Objectives/AIObjectiveManager.cs | 42 +- .../AI/Objectives/AIObjectiveOperateItem.cs | 5 + .../AI/Objectives/AIObjectivePumpWater.cs | 1 + .../AI/Objectives/AIObjectiveRepairItem.cs | 6 +- .../AI/Objectives/AIObjectiveRepairItems.cs | 1 + .../AI/Objectives/AIObjectiveRescue.cs | 21 +- .../SharedSource/Characters/AI/Order.cs | 60 +- .../Characters/AI/ShipCommandManager.cs | 7 +- .../Characters/AI/Wreck/WreckAI.cs | 2 +- .../Animation/FishAnimController.cs | 29 +- .../Animation/HumanoidAnimController.cs | 6 +- .../Characters/Animation/Ragdoll.cs | 13 +- .../SharedSource/Characters/Attack.cs | 8 + .../SharedSource/Characters/Character.cs | 122 ++- .../Characters/CharacterEventData.cs | 172 ++++ .../SharedSource/Characters/CharacterInfo.cs | 26 +- .../Health/Afflictions/AfflictionHusk.cs | 66 +- .../Characters/Health/CharacterHealth.cs | 81 +- .../SharedSource/Characters/HumanPrefab.cs | 2 +- .../SharedSource/Characters/Jobs/Job.cs | 2 +- .../SharedSource/Characters/Limb.cs | 17 +- .../Characters/Params/CharacterParams.cs | 7 +- .../Params/Ragdoll/RagdollParams.cs | 4 +- .../AbilityConditionNoCrewDied.cs | 22 +- .../ContentFile/ContentFile.cs | 8 +- .../ContentFile/GenericPrefabFile.cs | 1 - .../ContentPackage/ContentPackage.cs | 8 +- .../ContentPackageManager.cs | 4 + .../ContentManagement/ContentPath.cs | 18 +- .../SharedSource/DebugConsole.cs | 69 +- .../SharedSource/Events/ArtifactEvent.cs | 2 +- .../Events/EventActions/CheckMoneyAction.cs | 26 +- .../Events/EventActions/ConversationAction.cs | 6 +- .../Events/EventActions/MoneyAction.cs | 44 +- .../EventActions/NPCChangeTeamAction.cs | 2 +- .../Events/EventActions/TriggerAction.cs | 4 +- .../SharedSource/Events/EventManager.cs | 30 +- .../SharedSource/Events/EventSet.cs | 2 +- .../SharedSource/Events/Missions/Mission.cs | 63 +- .../GameAnalytics/GameAnalyticsManager.cs | 2 +- .../GameSession/AutoItemPlacer.cs | 2 +- .../SharedSource/GameSession/CargoManager.cs | 57 +- .../SharedSource/GameSession/CrewManager.cs | 10 + .../SharedSource/GameSession/Data/Factions.cs | 7 +- .../GameSession/Data/Reputation.cs | 2 +- .../SharedSource/GameSession/Data/Wallet.cs | 193 +++++ .../GameSession/GameModes/CampaignMode.cs | 48 +- .../GameModes/CharacterCampaignData.cs | 29 +- .../GameModes/MultiPlayerCampaign.cs | 13 +- .../SharedSource/GameSession/GameSession.cs | 57 +- .../SharedSource/GameSession/MedicalClinic.cs | 27 +- .../GameSession/UpgradeManager.cs | 15 +- .../SharedSource/Items/CharacterInventory.cs | 6 +- .../Items/Components/ElectricalDischarger.cs | 2 +- .../Components/EntitySpawnerComponent.cs | 1 - .../SharedSource/Items/Components/Growable.cs | 2 +- .../Items/Components/Holdable/Holdable.cs | 34 +- .../Items/Components/Holdable/IdCard.cs | 1 - .../Items/Components/Holdable/MeleeWeapon.cs | 9 +- .../Items/Components/Holdable/Pickable.cs | 6 +- .../Items/Components/Holdable/Throwable.cs | 6 +- .../Items/Components/ItemComponent.cs | 25 + .../Items/Components/Machines/Controller.cs | 16 +- .../Components/Machines/Deconstructor.cs | 2 +- .../Items/Components/Machines/Fabricator.cs | 9 +- .../Items/Components/Machines/Pump.cs | 2 +- .../Items/Components/Machines/Reactor.cs | 25 +- .../Items/Components/Machines/Sonar.cs | 4 +- .../Items/Components/Machines/Steering.cs | 8 +- .../Items/Components/Power/PowerContainer.cs | 28 +- .../Items/Components/Power/PowerTransfer.cs | 45 +- .../Items/Components/Power/Powered.cs | 26 +- .../Items/Components/Projectile.cs | 29 +- .../Items/Components/Repairable.cs | 6 +- .../Components/Signal/ArithmeticComponent.cs | 8 +- .../Items/Components/Signal/ButtonTerminal.cs | 19 +- .../Components/Signal/ConnectionPanel.cs | 2 +- .../Components/Signal/CustomInterface.cs | 10 + .../Items/Components/Signal/MotionSensor.cs | 1 - .../Items/Components/Signal/NotComponent.cs | 6 +- .../Items/Components/Signal/RelayComponent.cs | 4 +- .../Items/Components/Signal/SmokeDetector.cs | 2 +- .../Components/Signal/StringComponent.cs | 5 +- .../Signal/TrigonometricFunctionComponent.cs | 5 +- .../Items/Components/Signal/Wire.cs | 14 +- .../SharedSource/Items/Components/Turret.cs | 31 +- .../SharedSource/Items/Components/Wearable.cs | 8 +- .../SharedSource/Items/Inventory.cs | 66 +- .../SharedSource/Items/Item.cs | 123 +-- .../SharedSource/Items/ItemEventData.cs | 114 +++ .../SharedSource/Items/ItemInventory.cs | 11 +- .../SharedSource/Items/ItemPrefab.cs | 4 - .../Map/Creatures/BallastFloraBehavior.cs | 32 +- .../Map/Creatures/BallastFloraEventData.cs | 74 ++ .../State/BallastFloraStateMachine.cs | 4 +- .../SharedSource/Map/Entity.cs | 1 + .../SharedSource/Map/Explosion.cs | 4 +- .../SharedSource/Map/FireSource.cs | 4 +- .../BarotraumaShared/SharedSource/Map/Gap.cs | 9 +- .../BarotraumaShared/SharedSource/Map/Hull.cs | 97 ++- .../SharedSource/Map/HullEventData.cs | 70 ++ .../SharedSource/Map/IDamageable.cs | 37 +- .../SharedSource/Map/Levels/Level.cs | 39 +- .../Levels/LevelObjects/LevelObjectManager.cs | 19 +- .../SharedSource/Map/Map/Location.cs | 4 +- .../SharedSource/Map/StructurePrefab.cs | 41 +- .../SharedSource/Map/Submarine.cs | 2 +- .../SharedSource/Networking/ChatMessage.cs | 8 +- .../Networking/ChildServerRelay.cs | 57 +- .../SharedSource/Networking/EntitySpawner.cs | 83 +- .../Networking/INetSerializable.cs | 25 +- .../Networking/INetSerializableStruct.cs | 211 +++-- .../NetEntityEvent/NetEntityEvent.cs | 68 +- .../NetEntityEvent/NetEntityEventManager.cs | 3 +- .../SharedSource/Networking/NetworkMember.cs | 10 +- .../Networking/Primitives/Message/Message.cs | 4 +- .../SharedSource/PerformanceCounter.cs | 94 ++- .../SharedSource/Screens/GameScreen.cs | 9 +- .../Serialization/SerializableProperty.cs | 5 +- .../Serialization/XMLExtensions.cs | 163 ++-- .../StatusEffects/StatusEffect.cs | 15 +- .../SharedSource/SteamAchievementManager.cs | 10 +- .../SharedSource/Text/RichString.cs | 2 +- .../SharedSource/Utils/NamedEvent.cs | 61 ++ .../SharedSource/Utils/Option/Option.cs | 12 + .../SharedSource/Utils/RichTextData.cs | 6 +- Barotrauma/BarotraumaShared/changelog.txt | 84 ++ 302 files changed, 5878 insertions(+), 3317 deletions(-) delete mode 100644 Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs rename Barotrauma/BarotraumaClient/ClientSource/GameSession/{GameModes => }/Data/CampaignMetadata.cs (100%) create mode 100644 Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/Wallet.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Items/ItemEventData.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/GameSession/Data/Wallet.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Levels/Level.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraEventData.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Map/HullEventData.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Utils/NamedEvent.cs diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs index 6904554bd..cf8c7c628 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs @@ -42,7 +42,7 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { IsAlive = msg.ReadBoolean(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index a3dba23b3..22d741e27 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -112,395 +112,395 @@ namespace Barotrauma } } - public virtual void ClientWrite(IWriteMessage msg, object[] extraData = null) + public void ClientWriteInput(IWriteMessage msg) { - if (extraData != null) + msg.Write((byte)ClientNetObject.CHARACTER_INPUT); + + if (memInput.Count > 60) { - switch ((NetEntityEvent.Type)extraData[0]) + memInput.RemoveRange(60, memInput.Count - 60); + } + + msg.Write(LastNetworkUpdateID); + byte inputCount = Math.Min((byte)memInput.Count, (byte)60); + msg.Write(inputCount); + for (int i = 0; i < inputCount; i++) + { + msg.WriteRangedInteger((int)memInput[i].states, 0, (int)InputNetFlags.MaxVal); + msg.Write(memInput[i].intAim); + if (memInput[i].states.HasFlag(InputNetFlags.Select) || + memInput[i].states.HasFlag(InputNetFlags.Deselect) || + memInput[i].states.HasFlag(InputNetFlags.Use) || + memInput[i].states.HasFlag(InputNetFlags.Health) || + memInput[i].states.HasFlag(InputNetFlags.Grab)) { - case NetEntityEvent.Type.InventoryState: - msg.WriteRangedInteger(0, 0, 4); - Inventory.ClientWrite(msg, extraData); - break; - case NetEntityEvent.Type.Treatment: - msg.WriteRangedInteger(1, 0, 4); - msg.Write(AnimController.Anim == AnimController.Animation.CPR); - break; - case NetEntityEvent.Type.Status: - msg.WriteRangedInteger(2, 0, 4); - break; - case NetEntityEvent.Type.UpdateTalents: - msg.WriteRangedInteger(3, 0, 4); - msg.Write((ushort)characterTalents.Count); - foreach (var unlockedTalent in characterTalents) - { - msg.Write(unlockedTalent.Prefab.UintIdentifier); - } - break; + msg.Write(memInput[i].interact); } } + } + + public virtual void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) + { + if (!(extraData is IEventData eventData)) { throw new Exception($"Malformed character event: expected {nameof(Character)}.{nameof(IEventData)}"); } + + msg.WriteRangedInteger((int)eventData.EventType, (int)EventType.MinValue, (int)EventType.MaxValue); + switch (eventData) + { + case InventoryStateEventData inventoryStateEventData: + Inventory.ClientEventWrite(msg, inventoryStateEventData); + break; + case TreatmentEventData _: + msg.Write(AnimController.Anim == AnimController.Animation.CPR); + break; + case StatusEventData _: + //do nothing + break; + case UpdateTalentsEventData _: + msg.Write((ushort)characterTalents.Count); + foreach (var unlockedTalent in characterTalents) + { + msg.Write(unlockedTalent.Prefab.UintIdentifier); + } + break; + default: + throw new Exception($"Malformed character event: did not expect {eventData.GetType().Name}"); + } + } + + public void ClientReadPosition(IReadMessage msg, float sendingTime) + { + bool facingRight = AnimController.Dir > 0.0f; + + lastRecvPositionUpdateTime = (float)Lidgren.Network.NetTime.Now; + + AnimController.Frozen = false; + Enabled = true; + + UInt16 networkUpdateID = 0; + if (msg.ReadBoolean()) + { + networkUpdateID = msg.ReadUInt16(); + } else { - msg.Write((byte)ClientNetObject.CHARACTER_INPUT); + bool aimInput = msg.ReadBoolean(); + keys[(int)InputType.Aim].Held = aimInput; + keys[(int)InputType.Aim].SetState(false, aimInput); - if (memInput.Count > 60) + bool shootInput = msg.ReadBoolean(); + keys[(int)InputType.Shoot].Held = shootInput; + keys[(int)InputType.Shoot].SetState(false, shootInput); + + bool useInput = msg.ReadBoolean(); + keys[(int)InputType.Use].Held = useInput; + keys[(int)InputType.Use].SetState(false, useInput); + + if (AnimController is HumanoidAnimController) { - memInput.RemoveRange(60, memInput.Count - 60); + bool crouching = msg.ReadBoolean(); + keys[(int)InputType.Crouch].Held = crouching; + keys[(int)InputType.Crouch].SetState(false, crouching); } - msg.Write(LastNetworkUpdateID); - byte inputCount = Math.Min((byte)memInput.Count, (byte)60); - msg.Write(inputCount); - for (int i = 0; i < inputCount; i++) - { - msg.WriteRangedInteger((int)memInput[i].states, 0, (int)InputNetFlags.MaxVal); - msg.Write(memInput[i].intAim); - if (memInput[i].states.HasFlag(InputNetFlags.Select) || - memInput[i].states.HasFlag(InputNetFlags.Deselect) || - memInput[i].states.HasFlag(InputNetFlags.Use) || - memInput[i].states.HasFlag(InputNetFlags.Health) || - memInput[i].states.HasFlag(InputNetFlags.Grab)) - { - msg.Write(memInput[i].interact); - } - } + bool attackInput = msg.ReadBoolean(); + keys[(int)InputType.Attack].Held = attackInput; + keys[(int)InputType.Attack].SetState(false, attackInput); + + double aimAngle = msg.ReadUInt16() / 65535.0 * 2.0 * Math.PI; + cursorPosition = AimRefPosition + new Vector2((float)Math.Cos(aimAngle), (float)Math.Sin(aimAngle)) * 500.0f; + TransformCursorPos(); + + bool ragdollInput = msg.ReadBoolean(); + keys[(int)InputType.Ragdoll].Held = ragdollInput; + keys[(int)InputType.Ragdoll].SetState(false, ragdollInput); + + facingRight = msg.ReadBoolean(); } - msg.WritePadBits(); - } - public virtual void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) - { - switch (type) + bool entitySelected = msg.ReadBoolean(); + Character selectedCharacter = null; + Item selectedItem = null; + + AnimController.Animation animation = AnimController.Animation.None; + if (entitySelected) { - case ServerNetObject.ENTITY_POSITION: - bool facingRight = AnimController.Dir > 0.0f; - - lastRecvPositionUpdateTime = (float)Lidgren.Network.NetTime.Now; - - AnimController.Frozen = false; - Enabled = true; - - UInt16 networkUpdateID = 0; - if (msg.ReadBoolean()) + ushort characterID = msg.ReadUInt16(); + ushort itemID = msg.ReadUInt16(); + selectedCharacter = FindEntityByID(characterID) as Character; + selectedItem = FindEntityByID(itemID) as Item; + if (characterID != NullEntityID) + { + bool doingCpr = msg.ReadBoolean(); + if (doingCpr && SelectedCharacter != null) { - networkUpdateID = msg.ReadUInt16(); + animation = AnimController.Animation.CPR; } - else - { - bool aimInput = msg.ReadBoolean(); - keys[(int)InputType.Aim].Held = aimInput; - keys[(int)InputType.Aim].SetState(false, aimInput); - - bool shootInput = msg.ReadBoolean(); - keys[(int)InputType.Shoot].Held = shootInput; - keys[(int)InputType.Shoot].SetState(false, shootInput); - - bool useInput = msg.ReadBoolean(); - keys[(int)InputType.Use].Held = useInput; - keys[(int)InputType.Use].SetState(false, useInput); - - if (AnimController is HumanoidAnimController) - { - bool crouching = msg.ReadBoolean(); - keys[(int)InputType.Crouch].Held = crouching; - keys[(int)InputType.Crouch].SetState(false, crouching); - } - - bool attackInput = msg.ReadBoolean(); - keys[(int)InputType.Attack].Held = attackInput; - keys[(int)InputType.Attack].SetState(false, attackInput); - - double aimAngle = msg.ReadUInt16() / 65535.0 * 2.0 * Math.PI; - cursorPosition = AimRefPosition + new Vector2((float)Math.Cos(aimAngle), (float)Math.Sin(aimAngle)) * 500.0f; - TransformCursorPos(); - - bool ragdollInput = msg.ReadBoolean(); - keys[(int)InputType.Ragdoll].Held = ragdollInput; - keys[(int)InputType.Ragdoll].SetState(false, ragdollInput); - - facingRight = msg.ReadBoolean(); - } - - bool entitySelected = msg.ReadBoolean(); - Character selectedCharacter = null; - Item selectedItem = null; - - AnimController.Animation animation = AnimController.Animation.None; - if (entitySelected) - { - ushort characterID = msg.ReadUInt16(); - ushort itemID = msg.ReadUInt16(); - selectedCharacter = FindEntityByID(characterID) as Character; - selectedItem = FindEntityByID(itemID) as Item; - if (characterID != NullEntityID) - { - bool doingCpr = msg.ReadBoolean(); - if (doingCpr && SelectedCharacter != null) - { - animation = AnimController.Animation.CPR; - } - } - } - - Vector2 pos = new Vector2( - msg.ReadSingle(), - msg.ReadSingle()); - float MaxVel = NetConfig.MaxPhysicsBodyVelocity; - Vector2 linearVelocity = new Vector2( - msg.ReadRangedSingle(-MaxVel, MaxVel, 12), - msg.ReadRangedSingle(-MaxVel, MaxVel, 12)); - linearVelocity = NetConfig.Quantize(linearVelocity, -MaxVel, MaxVel, 12); - - bool fixedRotation = msg.ReadBoolean(); - float? rotation = null; - float? angularVelocity = null; - if (!fixedRotation) - { - rotation = msg.ReadSingle(); - float MaxAngularVel = NetConfig.MaxPhysicsBodyAngularVelocity; - angularVelocity = msg.ReadRangedSingle(-MaxAngularVel, MaxAngularVel, 8); - angularVelocity = NetConfig.Quantize(angularVelocity.Value, -MaxAngularVel, MaxAngularVel, 8); - } - - bool readStatus = msg.ReadBoolean(); - if (readStatus) - { - ReadStatus(msg); - AIController?.ClientRead(msg); - } - - msg.ReadPadBits(); - - int index = 0; - if (GameMain.Client.Character == this && CanMove) - { - var posInfo = new CharacterStateInfo( - pos, rotation, - networkUpdateID, - facingRight ? Direction.Right : Direction.Left, - selectedCharacter, selectedItem, animation); - - while (index < memState.Count && NetIdUtils.IdMoreRecent(posInfo.ID, memState[index].ID)) - index++; - memState.Insert(index, posInfo); - } - else - { - var posInfo = new CharacterStateInfo( - pos, rotation, - linearVelocity, angularVelocity, - sendingTime, facingRight ? Direction.Right : Direction.Left, - selectedCharacter, selectedItem, animation); - - while (index < memState.Count && posInfo.Timestamp > memState[index].Timestamp) - index++; - memState.Insert(index, posInfo); - } - - break; - case ServerNetObject.ENTITY_EVENT: - int eventType = msg.ReadRangedInteger(0, 13); - switch (eventType) - { - case 0: //NetEntityEvent.Type.InventoryState - if (Inventory == null) - { - string errorMsg = "Received an inventory update message for an entity with no inventory ([name], removed: " + Removed + ")"; - DebugConsole.ThrowError(errorMsg.Replace("[name]", Name)); - GameAnalyticsManager.AddErrorEventOnce("CharacterNetworking.ClientRead:NoInventory" + ID, GameAnalyticsManager.ErrorSeverity.Error, errorMsg.Replace("[name]", SpeciesName.Value)); - - //read anyway to prevent messing up reading the rest of the message - _ = msg.ReadUInt16(); - byte inventoryItemCount = msg.ReadByte(); - for (int i = 0; i < inventoryItemCount; i++) - { - msg.ReadUInt16(); - } - } - else - { - Inventory.ClientRead(type, msg, sendingTime); - } - break; - case 1: //NetEntityEvent.Type.Control - bool myCharacter = msg.ReadBoolean(); - byte ownerID = msg.ReadByte(); - ResetNetState(); - if (myCharacter) - { - if (controlled != null) - { - LastNetworkUpdateID = controlled.LastNetworkUpdateID; - } - - if (!IsDead) { Controlled = this; } - IsRemotePlayer = false; - GameMain.Client.HasSpawned = true; - GameMain.Client.Character = this; - GameMain.LightManager.LosEnabled = true; - GameMain.LightManager.LosAlpha = 1f; - GameMain.Client.WaitForNextRoundRespawn = null; - } - else - { - if (controlled == this) - { - Controlled = null; - IsRemotePlayer = ownerID > 0; - } - } - break; - case 2: //NetEntityEvent.Type.Status - ReadStatus(msg); - break; - case 3: //NetEntityEvent.Type.UpdateSkills - int skillCount = msg.ReadByte(); - for (int i = 0; i < skillCount; i++) - { - Identifier skillIdentifier = msg.ReadIdentifier(); - float skillLevel = msg.ReadSingle(); - info?.SetSkillLevel(skillIdentifier, skillLevel); - } - break; - case 4: // NetEntityEvent.Type.SetAttackTarget - case 5: //NetEntityEvent.Type.ExecuteAttack - int attackLimbIndex = msg.ReadByte(); - UInt16 targetEntityID = msg.ReadUInt16(); - int targetLimbIndex = msg.ReadByte(); - Vector2 targetSimPos = new Vector2(msg.ReadSingle(), msg.ReadSingle()); - //255 = entity already removed, no need to do anything - if (attackLimbIndex == 255 || Removed) { break; } - if (attackLimbIndex >= AnimController.Limbs.Length) - { - DebugConsole.ThrowError($"Received invalid {(eventType == 4 ? "SetAttackTarget" : "ExecuteAttack")} message. Limb index out of bounds (character: {Name}, limb index: {attackLimbIndex}, limb count: {AnimController.Limbs.Length})"); - break; - } - Limb attackLimb = AnimController.Limbs[attackLimbIndex]; - Limb targetLimb = null; - IDamageable targetEntity = FindEntityByID(targetEntityID) as IDamageable; - if (targetEntity == null && eventType == 4) - { - DebugConsole.ThrowError($"Received invalid SetAttackTarget message. Target entity not found (ID {targetEntityID})"); - break; - } - if (targetEntity is Character targetCharacter) - { - if (targetLimbIndex >= targetCharacter.AnimController.Limbs.Length) - { - DebugConsole.ThrowError($"Received invalid {(eventType == 4 ? "SetAttackTarget" : "ExecuteAttack")} message. Target limb index out of bounds (target character: {targetCharacter.Name}, limb index: {targetLimbIndex}, limb count: {targetCharacter.AnimController.Limbs.Length})"); - break; - } - targetLimb = targetCharacter.AnimController.Limbs[targetLimbIndex]; - } - if (attackLimb?.attack != null && Controlled != this) - { - if (eventType == 4) - { - SetAttackTarget(attackLimb, targetEntity, targetSimPos); - PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3); - } - else - { - attackLimb.ExecuteAttack(targetEntity, targetLimb, out _); - } - } - break; - case 6: //NetEntityEvent.Type.AssignCampaignInteraction - byte campaignInteractionType = msg.ReadByte(); - bool requireConsciousness = msg.ReadBoolean(); - (GameMain.GameSession?.GameMode as CampaignMode)?.AssignNPCMenuInteraction(this, (CampaignMode.InteractionType)campaignInteractionType); - RequireConsciousnessForCustomInteract = requireConsciousness; - break; - case 7: //NetEntityEvent.Type.ObjectiveManagerState - // 1 = order, 2 = objective - int msgType = msg.ReadRangedInteger(0, 2); - if (msgType == 0) { break; } - bool validData = msg.ReadBoolean(); - if (!validData) { break; } - if (msgType == 1) - { - UInt32 orderPrefabUintIdentifier = msg.ReadUInt32(); - var orderPrefab = OrderPrefab.Prefabs.Find(p => p.UintIdentifier == orderPrefabUintIdentifier); - Identifier option = Identifier.Empty; - if (orderPrefab.HasOptions) - { - int optionIndex = msg.ReadRangedInteger(-1, orderPrefab.AllOptions.Length); - if (optionIndex > -1) - { - option = orderPrefab.AllOptions[optionIndex]; - } - } - GameMain.GameSession?.CrewManager?.SetOrderHighlight(this, orderPrefab.Identifier, option); - } - else if (msgType == 2) - { - Identifier identifier = msg.ReadIdentifier(); - Identifier option = msg.ReadIdentifier(); - ushort objectiveTargetEntityId = msg.ReadUInt16(); - var objectiveTargetEntity = FindEntityByID(objectiveTargetEntityId); - GameMain.GameSession?.CrewManager?.CreateObjectiveIcon(this, identifier, option, objectiveTargetEntity); - } - break; - case 8: //NetEntityEvent.Type.TeamChange - byte newTeamId = msg.ReadByte(); - ChangeTeam((CharacterTeamType)newTeamId); - break; - case 9: //NetEntityEvent.Type.AddToCrew - GameMain.GameSession.CrewManager.AddCharacter(this); - CharacterTeamType teamID = (CharacterTeamType)msg.ReadByte(); - ushort itemCount = msg.ReadUInt16(); - for (int i = 0; i < itemCount; i++) - { - ushort itemID = msg.ReadUInt16(); - if (!(Entity.FindEntityByID(itemID) is Item item)) { continue; } - item.AllowStealing = true; - var wifiComponent = item.GetComponent(); - if (wifiComponent != null) - { - wifiComponent.TeamID = teamID; - } - var idCard = item.GetComponent(); - if (idCard != null) - { - idCard.TeamID = teamID; - idCard.SubmarineSpecificID = 0; - } - } - break; - case 10: //NetEntityEvent.Type.UpdateExperience - int experienceAmount = msg.ReadInt32(); - info?.SetExperience(experienceAmount); - break; - case 11: //NetEntityEvent.Type.UpdateTalents: - ushort talentCount = msg.ReadUInt16(); - for (int i = 0; i < talentCount; i++) - { - bool addedThisRound = msg.ReadBoolean(); - UInt32 talentIdentifier = msg.ReadUInt32(); - GiveTalent(talentIdentifier, addedThisRound); - } - break; - case 12: //NetEntityEvent.Type.UpdateMoney: - int moneyAmount = msg.ReadInt32(); - SetMoney(moneyAmount); - break; - case 13: //NetEntityEvent.Type.UpdatePermanentStats: - byte savedStatValueCount = msg.ReadByte(); - StatTypes statType = (StatTypes)msg.ReadByte(); - info?.ClearSavedStatValues(statType); - for (int i = 0; i < savedStatValueCount; i++) - { - string statIdentifier = msg.ReadString(); - float statValue = msg.ReadSingle(); - bool removeOnDeath = msg.ReadBoolean(); - info?.ChangeSavedStatValue(statType, statValue, statIdentifier, removeOnDeath, setValue: true); - } - break; - - } - msg.ReadPadBits(); - break; + } } + + Vector2 pos = new Vector2( + msg.ReadSingle(), + msg.ReadSingle()); + float MaxVel = NetConfig.MaxPhysicsBodyVelocity; + Vector2 linearVelocity = new Vector2( + msg.ReadRangedSingle(-MaxVel, MaxVel, 12), + msg.ReadRangedSingle(-MaxVel, MaxVel, 12)); + linearVelocity = NetConfig.Quantize(linearVelocity, -MaxVel, MaxVel, 12); + + bool fixedRotation = msg.ReadBoolean(); + float? rotation = null; + float? angularVelocity = null; + if (!fixedRotation) + { + rotation = msg.ReadSingle(); + float MaxAngularVel = NetConfig.MaxPhysicsBodyAngularVelocity; + angularVelocity = msg.ReadRangedSingle(-MaxAngularVel, MaxAngularVel, 8); + angularVelocity = NetConfig.Quantize(angularVelocity.Value, -MaxAngularVel, MaxAngularVel, 8); + } + + bool readStatus = msg.ReadBoolean(); + if (readStatus) + { + ReadStatus(msg); + AIController?.ClientRead(msg); + } + + msg.ReadPadBits(); + + int index = 0; + if (GameMain.Client.Character == this && CanMove) + { + var posInfo = new CharacterStateInfo( + pos, rotation, + networkUpdateID, + facingRight ? Direction.Right : Direction.Left, + selectedCharacter, selectedItem, animation); + + while (index < memState.Count && NetIdUtils.IdMoreRecent(posInfo.ID, memState[index].ID)) + index++; + memState.Insert(index, posInfo); + } + else + { + var posInfo = new CharacterStateInfo( + pos, rotation, + linearVelocity, angularVelocity, + sendingTime, facingRight ? Direction.Right : Direction.Left, + selectedCharacter, selectedItem, animation); + + while (index < memState.Count && posInfo.Timestamp > memState[index].Timestamp) + index++; + memState.Insert(index, posInfo); + } + } + + public virtual void ClientEventRead(IReadMessage msg, float sendingTime) + { + EventType eventType = (EventType)msg.ReadRangedInteger((int)EventType.MinValue, (int)EventType.MaxValue); + switch (eventType) + { + case EventType.InventoryState: + if (Inventory == null) + { + string errorMsg = "Received an inventory update message for an entity with no inventory ([name], removed: " + Removed + ")"; + DebugConsole.ThrowError(errorMsg.Replace("[name]", Name)); + GameAnalyticsManager.AddErrorEventOnce("CharacterNetworking.ClientRead:NoInventory" + ID, GameAnalyticsManager.ErrorSeverity.Error, errorMsg.Replace("[name]", SpeciesName.Value)); + + //read anyway to prevent messing up reading the rest of the message + _ = msg.ReadUInt16(); + byte inventoryItemCount = msg.ReadByte(); + for (int i = 0; i < inventoryItemCount; i++) + { + msg.ReadUInt16(); + } + } + else + { + Inventory.ClientEventRead(msg, sendingTime); + } + break; + case EventType.Control: + bool myCharacter = msg.ReadBoolean(); + byte ownerID = msg.ReadByte(); + ResetNetState(); + if (myCharacter) + { + if (controlled != null) + { + LastNetworkUpdateID = controlled.LastNetworkUpdateID; + } + + if (!IsDead) { Controlled = this; } + IsRemotePlayer = false; + GameMain.Client.HasSpawned = true; + GameMain.Client.Character = this; + GameMain.LightManager.LosEnabled = true; + GameMain.LightManager.LosAlpha = 1f; + GameMain.Client.WaitForNextRoundRespawn = null; + } + else + { + if (controlled == this) + { + Controlled = null; + IsRemotePlayer = ownerID > 0; + } + } + break; + case EventType.Status: + ReadStatus(msg); + break; + case EventType.UpdateSkills: + int skillCount = msg.ReadByte(); + for (int i = 0; i < skillCount; i++) + { + Identifier skillIdentifier = msg.ReadIdentifier(); + float skillLevel = msg.ReadSingle(); + info?.SetSkillLevel(skillIdentifier, skillLevel); + } + break; + case EventType.SetAttackTarget: + case EventType.ExecuteAttack: + int attackLimbIndex = msg.ReadByte(); + UInt16 targetEntityID = msg.ReadUInt16(); + int targetLimbIndex = msg.ReadByte(); + float targetX = msg.ReadSingle(); + float targetY = msg.ReadSingle(); + Vector2 targetSimPos = new Vector2(targetX, targetY); + //255 = entity already removed, no need to do anything + if (attackLimbIndex == 255 || Removed) { break; } + if (attackLimbIndex >= AnimController.Limbs.Length) + { + DebugConsole.ThrowError($"Received invalid {(eventType == EventType.SetAttackTarget ? "SetAttackTarget" : "ExecuteAttack")} message. Limb index out of bounds (character: {Name}, limb index: {attackLimbIndex}, limb count: {AnimController.Limbs.Length})"); + break; + } + Limb attackLimb = AnimController.Limbs[attackLimbIndex]; + Limb targetLimb = null; + IDamageable targetEntity = FindEntityByID(targetEntityID) as IDamageable; + if (targetEntity == null && eventType == EventType.SetAttackTarget) + { + DebugConsole.ThrowError($"Received invalid SetAttackTarget message. Target entity not found (ID {targetEntityID})"); + break; + } + if (targetEntity is Character targetCharacter) + { + if (targetLimbIndex >= targetCharacter.AnimController.Limbs.Length) + { + DebugConsole.ThrowError($"Received invalid {(eventType == EventType.SetAttackTarget ? "SetAttackTarget" : "ExecuteAttack")} message. Target limb index out of bounds (target character: {targetCharacter.Name}, limb index: {targetLimbIndex}, limb count: {targetCharacter.AnimController.Limbs.Length})"); + break; + } + targetLimb = targetCharacter.AnimController.Limbs[targetLimbIndex]; + } + if (attackLimb?.attack != null && Controlled != this) + { + if (eventType == EventType.SetAttackTarget) + { + SetAttackTarget(attackLimb, targetEntity, targetSimPos); + PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3); + } + else + { + attackLimb.ExecuteAttack(targetEntity, targetLimb, out _); + } + } + break; + case EventType.AssignCampaignInteraction: + byte campaignInteractionType = msg.ReadByte(); + bool requireConsciousness = msg.ReadBoolean(); + (GameMain.GameSession?.GameMode as CampaignMode)?.AssignNPCMenuInteraction(this, (CampaignMode.InteractionType)campaignInteractionType); + RequireConsciousnessForCustomInteract = requireConsciousness; + break; + case EventType.ObjectiveManagerState: + // 1 = order, 2 = objective + AIObjectiveManager.ObjectiveType msgType + = (AIObjectiveManager.ObjectiveType)msg.ReadRangedInteger( + (int)AIObjectiveManager.ObjectiveType.MinValue, + (int)AIObjectiveManager.ObjectiveType.MaxValue); + if (msgType == 0) { break; } + bool validData = msg.ReadBoolean(); + if (!validData) { break; } + if (msgType == AIObjectiveManager.ObjectiveType.Order) + { + UInt32 orderPrefabUintIdentifier = msg.ReadUInt32(); + var orderPrefab = OrderPrefab.Prefabs.Find(p => p.UintIdentifier == orderPrefabUintIdentifier); + Identifier option = Identifier.Empty; + if (orderPrefab.HasOptions) + { + int optionIndex = msg.ReadRangedInteger(-1, orderPrefab.AllOptions.Length); + if (optionIndex > -1) + { + option = orderPrefab.AllOptions[optionIndex]; + } + } + GameMain.GameSession?.CrewManager?.SetOrderHighlight(this, orderPrefab.Identifier, option); + } + else if (msgType == AIObjectiveManager.ObjectiveType.Objective) + { + Identifier identifier = msg.ReadIdentifier(); + Identifier option = msg.ReadIdentifier(); + ushort objectiveTargetEntityId = msg.ReadUInt16(); + var objectiveTargetEntity = FindEntityByID(objectiveTargetEntityId); + GameMain.GameSession?.CrewManager?.CreateObjectiveIcon(this, identifier, option, objectiveTargetEntity); + } + break; + case EventType.TeamChange: + byte newTeamId = msg.ReadByte(); + ChangeTeam((CharacterTeamType)newTeamId); + break; + case EventType.AddToCrew: + GameMain.GameSession.CrewManager.AddCharacter(this); + CharacterTeamType teamID = (CharacterTeamType)msg.ReadByte(); + ushort itemCount = msg.ReadUInt16(); + for (int i = 0; i < itemCount; i++) + { + ushort itemID = msg.ReadUInt16(); + if (!(Entity.FindEntityByID(itemID) is Item item)) { continue; } + item.AllowStealing = true; + var wifiComponent = item.GetComponent(); + if (wifiComponent != null) + { + wifiComponent.TeamID = teamID; + } + var idCard = item.GetComponent(); + if (idCard != null) + { + idCard.TeamID = teamID; + idCard.SubmarineSpecificID = 0; + } + } + break; + case EventType.UpdateExperience: + int experienceAmount = msg.ReadInt32(); + info?.SetExperience(experienceAmount); + break; + case EventType.UpdateTalents: + ushort talentCount = msg.ReadUInt16(); + for (int i = 0; i < talentCount; i++) + { + bool addedThisRound = msg.ReadBoolean(); + UInt32 talentIdentifier = msg.ReadUInt32(); + GiveTalent(talentIdentifier, addedThisRound); + } + break; + case EventType.UpdateMoney: + int moneyAmount = msg.ReadInt32(); + SetMoney(moneyAmount); + break; + case EventType.UpdatePermanentStats: + byte savedStatValueCount = msg.ReadByte(); + StatTypes statType = (StatTypes)msg.ReadByte(); + info?.ClearSavedStatValues(statType); + for (int i = 0; i < savedStatValueCount; i++) + { + string statIdentifier = msg.ReadString(); + float statValue = msg.ReadSingle(); + bool removeOnDeath = msg.ReadBoolean(); + info?.ChangeSavedStatValue(statType, statValue, statIdentifier, removeOnDeath, setValue: true); + } + break; + + } + msg.ReadPadBits(); } public static Character ReadSpawnData(IReadMessage inc) @@ -542,6 +542,8 @@ namespace Barotrauma { bool hasOwner = inc.ReadBoolean(); int ownerId = hasOwner ? inc.ReadByte() : -1; + int balance = hasOwner ? inc.ReadInt32() : -1; + int rewardDistribution = hasOwner ? inc.ReadRangedInteger(0, 100) : -1; byte teamID = inc.ReadByte(); bool hasAi = inc.ReadBoolean(); Identifier infoSpeciesName = inc.ReadIdentifier(); @@ -558,6 +560,8 @@ namespace Barotrauma } character.TeamID = (CharacterTeamType)teamID; character.CampaignInteractionType = (CampaignMode.InteractionType)inc.ReadByte(); + character.Wallet.Balance = balance; + character.Wallet.RewardDistribution = rewardDistribution; if (character.CampaignInteractionType != CampaignMode.InteractionType.None) { (GameMain.GameSession.GameMode as CampaignMode)?.AssignNPCMenuInteraction(character, character.CampaignInteractionType); @@ -597,7 +601,7 @@ namespace Barotrauma : Identifier.Empty) .WithManualPriority(orderPriority) .WithOrderGiver(orderGiver); - character.SetOrder(order, speak: false, force: true); + character.SetOrder(order, isNewOrder: true, speak: false, force: true); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs deleted file mode 100644 index f89b83bdf..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace Barotrauma -{ - partial class AfflictionHusk : Affliction - { - private InfectionState? prevDisplayedMessage; - partial void UpdateMessages() - { - if (character != Character.Controlled) { return; } - if (Prefab is AfflictionPrefabHusk { SendMessages: false }) { return; } - if (prevDisplayedMessage.HasValue && prevDisplayedMessage.Value == State) { return; } - - switch (State) - { - case InfectionState.Dormant: - if (Strength < DormantThreshold * 0.5f) - { - return; - } - GUI.AddMessage(TextManager.Get("HuskDormant"), GUIStyle.Red); - break; - case InfectionState.Transition: - GUI.AddMessage(TextManager.Get("HuskCantSpeak"), GUIStyle.Red); - break; - case InfectionState.Active: - if (character.Params.UseHuskAppendage) - { - GUI.AddMessage(TextManager.GetWithVariable("HuskActivate", "[Attack]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Attack)), GUIStyle.Red); - } - break; - case InfectionState.Final: - default: - break; - } - prevDisplayedMessage = State; - } - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index c84fe8eab..b61c76669 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -300,7 +300,7 @@ namespace Barotrauma if (GameMain.Client != null) { - GameMain.Client.CreateEntityEvent(Character.Controlled, new object[] { NetEntityEvent.Type.Treatment }); + GameMain.Client.CreateEntityEvent(Character.Controlled, new Character.TreatmentEventData()); } return true; @@ -400,7 +400,7 @@ namespace Barotrauma { if (GameMain.Client != null) { - GameMain.Client.CreateEntityEvent(Character.Controlled, new object[] { NetEntityEvent.Type.Status }); + GameMain.Client.CreateEntityEvent(Character.Controlled, new Character.StatusEventData()); } else { @@ -573,10 +573,10 @@ namespace Barotrauma inWater ? Character.Params.BleedParticleWater : Character.Params.BleedParticleAir, limb.WorldPosition, velocity, 0.0f, Character.AnimController.CurrentHull); - if (blood != null && !inWater) + if (blood != null) { blood.Size *= bloodParticleSize; - if (!string.IsNullOrEmpty(Character.BloodDecalName) && Rand.Range(0.0f, 1.0f) < 0.05f) + if (!inWater && !string.IsNullOrEmpty(Character.BloodDecalName) && Rand.Range(0.0f, 1.0f) < 0.05f) { blood.OnCollision += (Vector2 pos, Hull hull) => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index 4d924f605..41d2d470e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -523,7 +523,7 @@ namespace Barotrauma private string GetSpritePath(ContentPath texturePath) { if (!character.IsHumanoid) { return texturePath.Value; } - return GetSpritePath(texturePath, character?.Info); + return GetSpritePath(texturePath, character.Info); } partial void LoadParamsProjSpecific() diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 22c5061a7..4f4d110e3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -559,7 +559,7 @@ namespace Barotrauma GameMain.MainMenuScreen.QuickStart(fixedSeed: false, subName, difficulty, levelGenerationParams); - }, getValidArgs: () => new[] { SubmarineInfo.SavedSubmarines.Select(s => s.Name).Distinct().ToArray() })); + }, getValidArgs: () => new[] { SubmarineInfo.SavedSubmarines.Select(s => s.Name).Distinct().OrderBy(s => s).ToArray() })); commands.Add(new Command("steamnetdebug", "steamnetdebug: Toggles Steamworks networking debug logging.", (string[] args) => { @@ -628,7 +628,7 @@ namespace Barotrauma DebugConsoleMapping.Instance.Remove(key); NewMessage("Keybind unbound.", GUIStyle.Green); return; - }, isCheat: false, getValidArgs: () => new[] { DebugConsoleMapping.Instance.Bindings.Keys.Select(keys => keys.ToString()).Distinct().ToArray() })); + }, isCheat: false, getValidArgs: () => new[] { DebugConsoleMapping.Instance.Bindings.Keys.Select(keys => keys.ToString()).Distinct().OrderBy(k => k).ToArray() })); commands.Add(new Command("savebinds", "savebinds: Writes current keybinds into the config file.", (string[] args) => { @@ -710,6 +710,7 @@ namespace Barotrauma commands.Add(new Command("traitorlist", "", (string[] args) => { })); AssignRelayToServer("traitorlist", true); AssignRelayToServer("money", true); + AssignRelayToServer("showmoney", true); AssignRelayToServer("setskill", true); AssignRelayToServer("readycheck", true); @@ -1734,7 +1735,7 @@ namespace Barotrauma return new string[][] { - propertyList.Distinct().Select(i => i.Value).ToArray(), + propertyList.Distinct().Select(i => i.Value).OrderBy(n => n).ToArray(), Array.Empty() }; })); @@ -1763,17 +1764,26 @@ namespace Barotrauma foreach (var missionPrefab in MissionPrefab.Prefabs) { Identifier missionId = (missionPrefab.ConfigElement.Attribute("textidentifier") == null ? missionPrefab.Identifier : missionPrefab.ConfigElement.GetAttributeIdentifier("textidentifier", Identifier.Empty)); - Identifier nameIdentifier = $"missionname.{missionId}".ToIdentifier(); - if (!tags[language].Contains(nameIdentifier)) + addIfMissing($"missionname.{missionId}".ToIdentifier(), language); + addIfMissing($"missiondescription.{missionId}".ToIdentifier(), language); + } + + foreach (Type itemComponentType in typeof(ItemComponent).Assembly.GetTypes().Where(type => type.IsSubclassOf(typeof(ItemComponent)))) + { + foreach (var property in itemComponentType.GetProperties()) { - if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } - missingTags[nameIdentifier].Add(language); - } - Identifier descriptionIdentifier = $"missiondescription.{missionId}".ToIdentifier(); - if (!tags[language].Contains(descriptionIdentifier)) - { - if (!missingTags.ContainsKey(descriptionIdentifier)) { missingTags[descriptionIdentifier] = new HashSet(); } - missingTags[descriptionIdentifier].Add(language); + if (!property.IsDefined(typeof(InGameEditable), false)) { continue; } + + string propertyTag = $"{property.DeclaringType.Name}.{property.Name}"; + + addIfMissingAll(language, + propertyTag.ToIdentifier(), + property.Name.ToIdentifier(), + $"sp.{propertyTag}.name".ToIdentifier()); + + addIfMissingAll(language, + $"sp.{propertyTag}.description".ToIdentifier(), + $"{property.Name.ToIdentifier()}.description".ToIdentifier()); } } @@ -1781,18 +1791,8 @@ namespace Barotrauma { if (sub.Type != SubmarineType.Player || !sub.IsVanillaSubmarine()) { continue; } - Identifier nameIdentifier = $"submarine.name.{sub.Name}".ToIdentifier(); - if (!tags[language].Contains(nameIdentifier)) - { - if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } - missingTags[nameIdentifier].Add(language); - } - Identifier descriptionIdentifier = ("submarine.description." + sub.Name).ToIdentifier(); - if (!tags[language].Contains(descriptionIdentifier)) - { - if (!missingTags.ContainsKey(descriptionIdentifier)) { missingTags[descriptionIdentifier] = new HashSet(); } - missingTags[descriptionIdentifier].Add(language); - } + addIfMissing($"submarine.name.{sub.Name}".ToIdentifier(), language); + addIfMissing(("submarine.description." + sub.Name).ToIdentifier(), language); } foreach (AfflictionPrefab affliction in AfflictionPrefab.List) @@ -1806,42 +1806,21 @@ namespace Barotrauma } Identifier afflictionId = affliction.TranslationIdentifier; - Identifier nameIdentifier = $"afflictionname.{afflictionId}".ToIdentifier(); - if (!tags[language].Contains(nameIdentifier)) - { - if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } - missingTags[nameIdentifier].Add(language); - } - - Identifier descriptionIdentifier = $"afflictiondescription.{afflictionId}".ToIdentifier(); - if (!tags[language].Contains(descriptionIdentifier)) - { - if (!missingTags.ContainsKey(descriptionIdentifier)) { missingTags[descriptionIdentifier] = new HashSet(); } - missingTags[descriptionIdentifier].Add(language); - } + addIfMissing($"afflictionname.{afflictionId}".ToIdentifier(), language); + addIfMissing($"afflictiondescription.{afflictionId}".ToIdentifier(), language); } foreach (var talentTree in TalentTree.JobTalentTrees) { foreach (var talentSubTree in talentTree.TalentSubTrees) { - Identifier nameIdentifier = $"talenttree.{talentSubTree.Identifier}".ToIdentifier(); - if (!tags[language].Contains(nameIdentifier)) - { - if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } - missingTags[nameIdentifier].Add(language); - } + addIfMissing($"talenttree.{talentSubTree.Identifier}".ToIdentifier(), language); } } foreach (var talent in TalentPrefab.TalentPrefabs) { - Identifier nameIdentifier = $"talentname.{talent.Identifier}".ToIdentifier(); - if (!tags[language].Contains(nameIdentifier)) - { - if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } - missingTags[nameIdentifier].Add(language); - } + addIfMissing($"talentname.{talent.Identifier}".ToIdentifier(), language); } //check missing entity names @@ -1849,17 +1828,25 @@ namespace Barotrauma { Identifier nameIdentifier = ("entityname." + me.Identifier).ToIdentifier(); if (tags[language].Contains(nameIdentifier)) { continue; } + if (me.HideInMenus) { continue; } + + ContentXElement configElement = null; + if (me is ItemPrefab itemPrefab) { - nameIdentifier = itemPrefab.ConfigElement?.GetAttributeIdentifier("nameidentifier", nameIdentifier) ?? nameIdentifier; - if (nameIdentifier != null) - { - if (tags[language].Contains("entityname." + nameIdentifier)) { continue; } - } + configElement = itemPrefab.ConfigElement; + } + else if (me is StructurePrefab structurePrefab) + { + configElement = structurePrefab.ConfigElement; + } + if (configElement != null) + { + var overrideIdentifier = configElement.GetAttributeIdentifier("nameidentifier", null); + if (overrideIdentifier != null && tags[language].Contains("entityname." + overrideIdentifier)) { continue; } } - if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } - missingTags[nameIdentifier].Add(language); + addIfMissing(nameIdentifier, language); } } @@ -1868,11 +1855,7 @@ namespace Barotrauma foreach (LanguageIdentifier language in TextManager.AvailableLanguages) { if (language == TextManager.DefaultLanguage) { continue; } - if (!tags[language].Contains(englishTag)) - { - if (!missingTags.ContainsKey(englishTag)) { missingTags[englishTag] = new HashSet(); } - missingTags[englishTag].Add(language); - } + addIfMissing(englishTag, language); } } @@ -1920,6 +1903,24 @@ namespace Barotrauma Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; ToolBox.OpenFileWithShell(Path.GetFullPath(filePath)); SwapLanguage(TextManager.DefaultLanguage); + + void addIfMissing(Identifier tag, LanguageIdentifier language) + { + if (!tags[language].Contains(tag)) + { + if (!missingTags.ContainsKey(tag)) { missingTags[tag] = new HashSet(); } + missingTags[tag].Add(language); + } + } + void addIfMissingAll(LanguageIdentifier language, params Identifier[] potentialTags) + { + if (!potentialTags.Any(t => tags[language].Contains(t))) + { + var tag = potentialTags.First(); + if (!missingTags.ContainsKey(tag)) { missingTags[tag] = new HashSet(); } + missingTags[tag].Add(language); + } + } })); commands.Add(new Command("comparelocafiles", "comparelocafiles [file1] [file2]", (string[] args) => @@ -1944,7 +1945,10 @@ namespace Barotrauma } var content1 = getContent(doc1.Root); + var language1 = doc1.Root.GetAttributeIdentifier("language", string.Empty); + var content2 = getContent(doc2.Root); + var language2 = doc2.Root.GetAttributeIdentifier("language", string.Empty); foreach (KeyValuePair kvp in content1) { @@ -1952,12 +1956,9 @@ namespace Barotrauma { ThrowError($"File 2 doesn't contain the text tag \"{kvp.Key}\""); } - else + else if (language1 == language2 && content2[kvp.Key] != kvp.Value) { - if (content2[kvp.Key] != kvp.Value) - { - ThrowError($"Texts for the tag \"{kvp.Key}\" don't match:\n1. {kvp.Value}\n2. {content2[kvp.Key]}"); - } + ThrowError($"Texts for the tag \"{kvp.Key}\" don't match:\n1. {kvp.Value}\n2. {content2[kvp.Key]}"); } } foreach (KeyValuePair kvp in content2) @@ -2319,7 +2320,14 @@ namespace Barotrauma } Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; File.WriteAllLines(filePath, lines); - ToolBox.OpenFileWithShell(Path.GetFullPath(filePath)); + try + { + ToolBox.OpenFileWithShell(Path.GetFullPath(filePath)); + } + catch (Exception e) + { + ThrowError($"Failed to open the file \"{filePath}\".", e); + } System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs index 35ae05d0a..9c175f289 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs @@ -671,7 +671,7 @@ namespace Barotrauma if (Character.Controlled != null && ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent radio)) { radio.Channel = channel; - GameMain.Client?.CreateEntityEvent(radio.Item, new object[] { NetEntityEvent.Type.ChangeProperty, radio.SerializableProperties["channel".ToIdentifier()] }); + GameMain.Client?.CreateEntityEvent(radio.Item, new Item.ChangePropertyEventData(radio.SerializableProperties["channel".ToIdentifier()])); if (setText) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs index 32cfeee49..04eabc733 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs @@ -172,7 +172,7 @@ namespace Barotrauma { AutoScaleVertical = true, TextScale = 1.1f, - TextGetter = () => FormatCurrency(campaign.Money) + TextGetter = () => FormatCurrency(campaign.Wallet.Balance) }; var pendingAndCrewGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), anchor: Anchor.Center, @@ -630,7 +630,7 @@ namespace Barotrauma total += ((InfoSkill)c.UserData).CharacterInfo.Salary; }); totalBlock.Text = FormatCurrency(total); - bool enoughMoney = campaign != null ? total <= campaign.Money : true; + bool enoughMoney = campaign == null || campaign.Wallet.CanAfford(total); totalBlock.TextColor = enoughMoney ? Color.White : Color.Red; validateHiresButton.Enabled = enoughMoney && pendingList.Content.RectTransform.Children.Any(); } @@ -652,7 +652,7 @@ namespace Barotrauma int total = nonDuplicateHires.Aggregate(0, (total, info) => total + info.Salary); - if (total > campaign.Money) { return false; } + if (!campaign.Wallet.CanAfford(total)) { return false; } bool atLeastOneHired = false; foreach (CharacterInfo ci in nonDuplicateHires) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index c83bca84c..cba0182cf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -299,14 +299,16 @@ namespace Barotrauma return; } - if (GameMain.ShowFPS || GameMain.DebugDraw) + if (GameMain.ShowFPS || GameMain.DebugDraw || GameMain.ShowPerf) { - DrawString(spriteBatch, new Vector2(10, 10), + float y = 10.0f; + DrawString(spriteBatch, new Vector2(10, y), "FPS: " + Math.Round(GameMain.PerformanceCounter.AverageFramesPerSecond), Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); if (GameMain.GameSession != null && Timing.TotalTime > GameMain.GameSession.RoundStartTime + 1.0) { - DrawString(spriteBatch, new Vector2(10, 25), + y += GameSettings.CurrentConfig.Graphics.TextScale * 15.0f; + DrawString(spriteBatch, new Vector2(10, y), $"Physics: {GameMain.CurrentUpdateRate}", (GameMain.CurrentUpdateRate < Timing.FixedUpdateRate) ? Color.Red : Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); } @@ -336,8 +338,15 @@ namespace Barotrauma DrawString(spriteBatch, new Vector2(300, y), key + ": " + elapsedMillisecs.ToString("0.00"), Color.Lerp(Color.LightGreen, GUIStyle.Red, elapsedMillisecs / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); - y += 15; + foreach (string childKey in GameMain.PerformanceCounter.GetSavedPartialIdentifiers(key)) + { + elapsedMillisecs = GameMain.PerformanceCounter.GetPartialAverageElapsedMillisecs(key, childKey); + DrawString(spriteBatch, new Vector2(315, y), + childKey + ": " + elapsedMillisecs.ToString("0.00"), + Color.Lerp(Color.LightGreen, GUIStyle.Red, elapsedMillisecs / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); + y += 15; + } } if (Powered.Grids != null) @@ -1453,7 +1462,7 @@ namespace Barotrauma 3 => radii.Start, _ => throw new InvalidOperationException() }; - int getDirectionIndex(int vertexIndex) + static int getDirectionIndex(int vertexIndex) => (vertexIndex % 4) switch { 0 => (vertexIndex / 4) + 0, diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIColorPicker.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIColorPicker.cs index 5fd93f7da..4cc0ee1e7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIColorPicker.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIColorPicker.cs @@ -5,7 +5,7 @@ using Microsoft.Xna.Framework; namespace Barotrauma { - public class GUIColorPicker : GUIComponent + public class GUIColorPicker : GUIComponent, IDisposable { public delegate bool OnColorSelectedHandler(GUIColorPicker component, Color color); public OnColorSelectedHandler? OnColorSelected; @@ -34,11 +34,6 @@ namespace Barotrauma public GUIColorPicker(RectTransform rectT, string? style = null) : base(style, rectT) { } - ~GUIColorPicker() - { - DisposeTextures(); - } - private void Init() { int tWidth = Rect.Width; @@ -170,10 +165,12 @@ namespace Barotrauma } } - public void DisposeTextures() + public void Dispose() { mainTexture?.Dispose(); + mainTexture = null; hueTexture?.Dispose(); + hueTexture = null; } public void RefreshHue() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs index b4dc7513d..5ec73c90f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs @@ -161,7 +161,7 @@ namespace Barotrauma } } - public GUIDropDown(RectTransform rectT, LocalizedString text = null, int elementCount = 4, string style = "", bool selectMultiple = false, bool dropAbove = false) : base(style, rectT) + public GUIDropDown(RectTransform rectT, LocalizedString text = null, int elementCount = 4, string style = "", bool selectMultiple = false, bool dropAbove = false, Alignment textAlignment = Alignment.CenterLeft) : base(style, rectT) { text ??= new RawLString(""); @@ -170,9 +170,10 @@ namespace Barotrauma this.selectMultiple = selectMultiple; - button = new GUIButton(new RectTransform(Vector2.One, rectT), text, Alignment.CenterLeft, style: "GUIDropDown") + button = new GUIButton(new RectTransform(Vector2.One, rectT), text, textAlignment, style: "GUIDropDown") { - OnClicked = OnClicked + OnClicked = OnClicked, + TextBlock = { OverflowClip = true } }; GUIStyle.Apply(button, "", this); button.TextBlock.SetTextPos(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs index d21951cc1..33837ece7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs @@ -96,11 +96,15 @@ namespace Barotrauma switch (child.ScaleBasis) { case ScaleBasis.BothHeight: + child.MinSize = new Point(child.Rect.Height, child.MinSize.Y); + break; case ScaleBasis.Smallest when Rect.Height <= Rect.Width: case ScaleBasis.Largest when Rect.Height > Rect.Width: child.MinSize = new Point((int)((child.Rect.Height * child.RelativeSize.X) / child.RelativeSize.Y), child.MinSize.Y); break; case ScaleBasis.BothWidth: + child.MinSize = new Point(child.MinSize.X, child.Rect.Width); + break; case ScaleBasis.Smallest when Rect.Width <= Rect.Height: case ScaleBasis.Largest when Rect.Width > Rect.Height: child.MinSize = new Point(child.MinSize.X, (int)((child.Rect.Width * child.RelativeSize.Y) / child.RelativeSize.X)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs index a410c8db5..b7265c76f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs @@ -12,9 +12,12 @@ namespace Barotrauma Int, Float } + public delegate void OnValueEnteredHandler(GUINumberInput numberInput); + public OnValueEnteredHandler OnValueEntered; + public delegate void OnValueChangedHandler(GUINumberInput numberInput); public OnValueChangedHandler OnValueChanged; - + public GUITextBox TextBox { get; private set; } public GUIButton PlusButton { get; private set; } @@ -209,6 +212,8 @@ namespace Barotrauma { ClampFloatValue(); } + + OnValueEntered?.Invoke(this); }; TextBox.OnEnterPressed += (textBox, text) => { @@ -220,6 +225,8 @@ namespace Barotrauma { ClampFloatValue(); } + + OnValueEntered?.Invoke(this); return true; }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs index ff245d132..1a17d1124 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs @@ -312,6 +312,7 @@ namespace Barotrauma MoveButton(new Vector2( Math.Sign(PlayerInput.MousePosition.X - Bar.Rect.Center.X) * Bar.Rect.Width * barScale, Math.Sign(PlayerInput.MousePosition.Y - Bar.Rect.Center.Y) * Bar.Rect.Height * barScale)); + OnReleased?.Invoke(this, BarScroll); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs index fb87b3639..6c530a3a7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs @@ -271,7 +271,7 @@ namespace Barotrauma healList.PriceBlock.Text = UpgradeStore.FormatCurrency(totalCost); healList.PriceBlock.TextColor = GUIStyle.Red; healList.HealButton.Enabled = false; - if (medicalClinic.GetMoney() > totalCost) + if (medicalClinic.GetWallet().CanAfford(totalCost)) { healList.PriceBlock.TextColor = GUIStyle.TextColorNormal; if (medicalClinic.PendingHeals.Any()) @@ -467,7 +467,7 @@ namespace Barotrauma GUITextBlock moneyLabel = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), balanceLayout.RectTransform), string.Empty, textAlignment: Alignment.TopRight, font: GUIStyle.SubHeadingFont) { - TextGetter = () => UpgradeStore.FormatCurrency(medicalClinic.GetMoney()), + TextGetter = () => UpgradeStore.FormatCurrency(medicalClinic.GetWallet().Balance), AutoScaleVertical = true, TextScale = 1.1f }; @@ -577,7 +577,7 @@ namespace Barotrauma GUILayoutGroup buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), footerLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterRight); GUIButton healButton = new GUIButton(new RectTransform(new Vector2(0.33f, 1f), buttonLayout.RectTransform), TextManager.Get("medicalclinic.heal")) { - Enabled = medicalClinic.PendingHeals.Any() && medicalClinic.GetTotalCost() < medicalClinic.GetMoney(), + Enabled = medicalClinic.PendingHeals.Any() && medicalClinic.GetWallet().CanAfford(medicalClinic.GetTotalCost()), OnClicked = (button, _) => { button.Enabled = false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index 47df0dea8..87c7ebe15 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -3,6 +3,7 @@ using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.Linq; @@ -67,8 +68,7 @@ namespace Barotrauma private CargoManager CargoManager => campaignUI.Campaign.CargoManager; private Location CurrentLocation => campaignUI.Campaign.Map?.CurrentLocation; - private int PlayerMoney => campaignUI.Campaign.Money; - + private Wallet PlayerWallet => campaignUI.Campaign.Wallet; private bool IsBuying => activeTab switch { StoreTab.Buy => true, @@ -715,24 +715,30 @@ namespace Barotrauma private LocalizedString GetMerchantBalanceText() => GetCurrencyFormatted(CurrentLocation?.StoreCurrentBalance ?? 0); - private LocalizedString GetPlayerBalanceText() => GetCurrencyFormatted(PlayerMoney); + private LocalizedString GetPlayerBalanceText() => GetCurrencyFormatted(PlayerWallet.Balance); private GUILayoutGroup CreateDealsGroup(GUIListBox parentList, int elementCount = 4) { var elementHeight = (int)(GUI.yScale * 80); - var frame = new GUIFrame(new RectTransform(new Point(parentList.Content.Rect.Width, elementCount * elementHeight + 3), parent: parentList.Content.RectTransform), style: null); - frame.UserData = "deals"; + var frame = new GUIFrame(new RectTransform(new Point(parentList.Content.Rect.Width, elementCount * elementHeight + 3), parent: parentList.Content.RectTransform), style: null) + { + UserData = "deals" + }; var dealsGroup = new GUILayoutGroup(new RectTransform(Vector2.One, frame.RectTransform, anchor: Anchor.Center), childAnchor: Anchor.TopCenter); - var dealsHeader = new GUILayoutGroup(new RectTransform(new Point((int)(0.95f * parentList.Content.Rect.Width), elementHeight), parent: dealsGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); - dealsHeader.UserData = "header"; + var dealsHeader = new GUILayoutGroup(new RectTransform(new Point((int)(0.95f * parentList.Content.Rect.Width), elementHeight), parent: dealsGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + UserData = "header" + }; var iconWidth = (0.9f * dealsHeader.Rect.Height) / dealsHeader.Rect.Width; var dealsIcon = new GUIImage(new RectTransform(new Vector2(iconWidth, 0.9f), dealsHeader.RectTransform), "StoreDealIcon", scaleToFit: true); var text = TextManager.Get(parentList == storeBuyList ? "campaignstore.dailyspecials" : "campaignstore.requestedgoods"); var dealsText = new GUITextBlock(new RectTransform(new Vector2(1.0f - iconWidth, 0.9f), dealsHeader.RectTransform), text, font: GUIStyle.LargeFont); storeSpecialColor = dealsIcon.Color; dealsText.TextColor = storeSpecialColor; - var divider = new GUIImage(new RectTransform(new Point(dealsGroup.Rect.Width, 3), dealsGroup.RectTransform), "HorizontalLine"); - divider.UserData = "divider"; + var divider = new GUIImage(new RectTransform(new Point(dealsGroup.Rect.Width, 3), dealsGroup.RectTransform), "HorizontalLine") + { + UserData = "divider" + }; frame.CanBeFocused = dealsGroup.CanBeFocused = dealsHeader.CanBeFocused = dealsIcon.CanBeFocused = dealsText.CanBeFocused = divider.CanBeFocused = false; return dealsGroup; } @@ -1801,7 +1807,7 @@ namespace Barotrauma private void SetOwnedText(GUIComponent itemComponent, GUITextBlock ownedLabel = null) { ownedLabel ??= itemComponent?.FindChild("owned", recursive: true) as GUITextBlock; - if (itemComponent == null && ownedLabel == null) { return; } + if (itemComponent == null && ownedLabel == null) { return; } PurchasedItem purchasedItem = itemComponent?.UserData as PurchasedItem; ItemQuantity itemQuantity = null; LocalizedString ownedLabelText = string.Empty; @@ -1970,7 +1976,7 @@ namespace Barotrauma DebugConsole.ShowError($"Error clearing the shopping crate: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); return false; } - } + } private bool BuyItems() { @@ -1990,7 +1996,7 @@ namespace Barotrauma } itemsToRemove.ForEach(i => itemsToPurchase.Remove(i)); - if (itemsToPurchase.None() || totalPrice > PlayerMoney) { return false; } + if (itemsToPurchase.None() || !PlayerWallet.CanAfford(totalPrice)) { return false; } CargoManager.PurchaseItems(itemsToPurchase, true); GameMain.Client?.SendCampaignState(); @@ -2047,7 +2053,7 @@ namespace Barotrauma if (IsBuying) { shoppingCrateTotal.Text = GetCurrencyFormatted(buyTotal); - shoppingCrateTotal.TextColor = buyTotal > PlayerMoney ? Color.Red : Color.White; + shoppingCrateTotal.TextColor = !PlayerWallet.CanAfford(buyTotal) ? Color.Red : Color.White; } else { @@ -2093,7 +2099,7 @@ namespace Barotrauma ActiveShoppingCrateList.Content.RectTransform.Children.Any() && activeTab switch { - StoreTab.Buy => buyTotal <= PlayerMoney, + StoreTab.Buy => PlayerWallet.CanAfford(buyTotal), StoreTab.Sell => CurrentLocation != null && sellTotal <= CurrentLocation.StoreCurrentBalance, StoreTab.SellSub => CurrentLocation != null && sellFromSubTotal <= CurrentLocation.StoreCurrentBalance, _ => false @@ -2109,9 +2115,12 @@ namespace Barotrauma private float ownedItemsUpdateTimer = 0.0f, sellableItemsFromSubUpdateTimer = 0.0f; private const float timerUpdateInterval = 1.5f; + private readonly Stopwatch updateStopwatch = new Stopwatch(); public void Update(float deltaTime) { + updateStopwatch.Restart(); + if (GameMain.GraphicsWidth != resolutionWhenCreated.X || GameMain.GraphicsHeight != resolutionWhenCreated.Y) { CreateUI(); @@ -2124,10 +2133,10 @@ namespace Barotrauma { var prevOwnedItems = new Dictionary(OwnedItems); UpdateOwnedItems(); - var refresh = (prevOwnedItems.Count != OwnedItems.Count) || - (prevOwnedItems.Select(kvp => kvp.Value.Total).Sum() != OwnedItems.Select(kvp => kvp.Value.Total).Sum()) || - (OwnedItems.Any(kvp => kvp.Value.Total > 0 && !prevOwnedItems.ContainsKey(kvp.Key)) || - prevOwnedItems.Any(kvp => !OwnedItems.TryGetValue(kvp.Key, out ItemQuantity itemQuantity) || kvp.Value.Total != itemQuantity.Total)); + bool refresh = OwnedItems.Count != prevOwnedItems.Count || + OwnedItems.Values.Sum(v => v.Total) != prevOwnedItems.Values.Sum(v => v.Total) || + OwnedItems.Any(kvp => !prevOwnedItems.TryGetValue(kvp.Key, out ItemQuantity v) || kvp.Value.Total != v.Total) || + prevOwnedItems.Any(kvp => !OwnedItems.ContainsKey(kvp.Key)); if (refresh) { needsItemsToSellRefresh = true; @@ -2138,8 +2147,13 @@ namespace Barotrauma sellableItemsFromSubUpdateTimer += deltaTime; if (sellableItemsFromSubUpdateTimer >= timerUpdateInterval) { - needsItemsToSellFromSubRefresh = true; - needsRefresh = true; + var prevSubItems = new List(itemsToSellFromSub); + RefreshItemsToSellFromSub(); + needsRefresh = needsRefresh || + itemsToSellFromSub.Count != prevSubItems.Count || + itemsToSellFromSub.Sum(i => i.Quantity) != prevSubItems.Sum(i => i.Quantity) || + itemsToSellFromSub.Any(i => !(prevSubItems.FirstOrDefault(prev => prev.ItemPrefab == i.ItemPrefab) is PurchasedItem prev) || i.Quantity != prev.Quantity) || + prevSubItems.Any(prev => itemsToSellFromSub.None(i => i.ItemPrefab == prev.ItemPrefab)); } } @@ -2148,7 +2162,10 @@ namespace Barotrauma if (needsRefresh || HavePermissionsChanged()) { Refresh(updateOwned: ownedItemsUpdateTimer > 0.0f); } if (needsBuyingRefresh || HavePermissionsChanged(StoreTab.Buy)) { RefreshBuying(updateOwned: ownedItemsUpdateTimer > 0.0f); } if (needsSellingRefresh || HavePermissionsChanged(StoreTab.Sell)) { RefreshSelling(updateOwned: ownedItemsUpdateTimer > 0.0f); } - if (needsSellingFromSubRefresh || HavePermissionsChanged(StoreTab.SellSub)) { RefreshSellingFromSub(updateItemsToSellFromSub: sellableItemsFromSubUpdateTimer > 0.0f); } + if (needsSellingFromSubRefresh || HavePermissionsChanged(StoreTab.SellSub)) { RefreshSellingFromSub(updateOwned: ownedItemsUpdateTimer > 0.0f, updateItemsToSellFromSub: sellableItemsFromSubUpdateTimer > 0.0f); } + + updateStopwatch.Stop(); + GameMain.PerformanceCounter.AddPartialElapsedTicks("GameSessionUpdate", "StoreUpdate", updateStopwatch.ElapsedTicks); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index 2d8d5a598..4272b8dcc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -581,7 +581,7 @@ namespace Barotrauma private void ShowTransferPrompt() { - if (GameMain.GameSession.Campaign.Money < deliveryFee && deliveryFee > 0) + if (!GameMain.GameSession.Campaign.Wallet.CanAfford(deliveryFee) && deliveryFee > 0) { new GUIMessageBox(TextManager.Get("deliveryrequestheader"), TextManager.GetWithVariables("notenoughmoneyfordeliverytext", ("[currencyname]", currencyLongText), @@ -629,7 +629,7 @@ namespace Barotrauma private void ShowBuyPrompt(bool purchaseOnly) { - if (GameMain.GameSession.Campaign.Money < selectedSubmarine.Price) + if (!GameMain.GameSession.Campaign.Wallet.CanAfford(selectedSubmarine.Price)) { new GUIMessageBox(TextManager.Get("purchasesubmarineheader"), TextManager.GetWithVariables("notenoughmoneyforpurchasetext", ("[currencyname]", currencyLongText), diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index aa22ddcd0..c31514929 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -37,6 +37,12 @@ namespace Barotrauma private GUIFrame pendingChangesFrame = null; public static Color OwnCharacterBGColor = Color.Gold * 0.7f; + private bool isTransferMenuOpen; + private bool isSending; + private GUIComponent transferMenu; + private GUIButton transferMenuButton; + private float transferMenuOpenState; + private bool transferMenuStateCompleted; private class LinkedGUI { @@ -133,8 +139,43 @@ namespace Barotrauma SelectInfoFrameTab(SelectedTab); } - public void Update() + public void Update(float deltaTime) { + float menuOpenSpeed = deltaTime * 10f; + if (isTransferMenuOpen) + { + if (transferMenuStateCompleted) + { + transferMenuOpenState = transferMenuOpenState < 0.25f ? Math.Min(0.25f, transferMenuOpenState + (menuOpenSpeed / 2f)) : 0.25f; + } + else + { + if (transferMenuOpenState > 0.15f) + { + transferMenuStateCompleted = false; + transferMenuOpenState = Math.Max(0.15f, transferMenuOpenState - menuOpenSpeed); + } + else + { + transferMenuStateCompleted = true; + } + } + } + else + { + transferMenuStateCompleted = false; + if (transferMenuOpenState < 1f) + { + transferMenuOpenState = Math.Min(1f, transferMenuOpenState + menuOpenSpeed); + } + } + + if (transferMenu != null && transferMenuButton != null) + { + int pos = (int)(transferMenuOpenState * -transferMenu.Rect.Height); + transferMenu.RectTransform.AbsoluteOffset = new Point(0, pos); + transferMenuButton.RectTransform.AbsoluteOffset = new Point(0, -pos - transferMenu.Rect.Height); + } GameSession.UpdateTalentNotificationIndicator(talentPointNotification); if (Character.Controlled is { } controlled && talentResetButton != null && talentApplyButton != null) { @@ -243,10 +284,7 @@ namespace Barotrauma var reputationButton = createTabButton(InfoFrameTab.Reputation, "reputation"); var balanceFrame = new GUIFrame(new RectTransform(new Point(innerLayoutGroup.Rect.Width, innerLayoutGroup.Rect.Height - infoFrameHolderHeight), parent: innerLayoutGroup.RectTransform), style: "InnerFrame"); - new GUITextBlock(new RectTransform(Vector2.One, balanceFrame.RectTransform), "", textAlignment: Alignment.Right) - { - TextGetter = () => TextManager.GetWithVariable("campaignmoney", "[money]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", campaignMode.Money)) - }; + GUITextBlock balanceText = new GUITextBlock(new RectTransform(Vector2.One, balanceFrame.RectTransform), string.Empty, textAlignment: Alignment.Right); GUIFrame bottomDisclaimerFrame = new GUIFrame(new RectTransform(new Vector2(contentFrameSize.X, 0.1f), infoFrame.RectTransform) { AbsoluteOffset = new Point(contentFrame.Rect.X, contentFrame.Rect.Bottom + GUI.IntScale(8)) @@ -258,6 +296,18 @@ namespace Barotrauma { NetLobbyScreen.CreateChangesPendingFrame(pendingChangesFrame); } + + SetBalanceText(balanceText, campaignMode.Bank.Balance); + campaignMode.OnMoneyChanged.RegisterOverwriteExisting(nameof(CreateInfoFrame).ToIdentifier(), e => + { + if (e.Wallet != campaignMode.Bank) { return; } + SetBalanceText(balanceText, e.Wallet.Balance); + }); + + static void SetBalanceText(GUITextBlock text, int balance) + { + text.Text = TextManager.GetWithVariable("bankbalanceformat", "[money]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", balance)); + } } else { @@ -329,7 +379,8 @@ namespace Barotrauma private void CreateCrewListFrame(GUIFrame crewFrame) { - crew = GameMain.GameSession.CrewManager.GetCharacters(); + // FIXME remove TestScreen stuff + crew = GameMain.GameSession?.CrewManager?.GetCharacters() ?? new []{ TestScreen.dummyCharacter }; teamIDs = crew.Select(c => c.TeamID).Distinct().ToList(); // Show own team first when there's more than one team @@ -743,7 +794,7 @@ namespace Barotrauma Client client = userData as Client; GUIComponent existingPreview = infoFrameHolder.FindChild("SelectedCharacter"); - if (existingPreview != null) infoFrameHolder.RemoveChild(existingPreview); + if (existingPreview != null) { infoFrameHolder.RemoveChild(existingPreview); } GUIFrame background = new GUIFrame(new RectTransform(new Vector2(0.543f, 0.717f), infoFrameHolder.RectTransform, Anchor.TopLeft, Pivot.TopRight) { RelativeOffset = new Vector2(-0.145f, 0) }) { @@ -760,17 +811,291 @@ namespace Barotrauma { GUIComponent preview = character.Info.CreateInfoFrame(background, false, GetPermissionIcon(GameMain.Client.ConnectedClients.Find(c => c.Character == character))); GameMain.Client.SelectCrewCharacter(character, preview); + CreateWalletFrame(background, character); } } else if (client != null) { GUIComponent preview = CreateClientInfoFrame(background, client, GetPermissionIcon(client)); - if (GameMain.NetworkMember != null) GameMain.Client.SelectCrewClient(client, preview); + if (GameMain.NetworkMember != null) { GameMain.Client.SelectCrewClient(client, preview); } + CreateWalletFrame(background, client.Character); } return true; } + private void CreateWalletFrame(GUIComponent parent, Character character) + { + if (character is null) { throw new ArgumentNullException(nameof(character), "Tried to create a wallet frame for a null character");} + isTransferMenuOpen = false; + transferMenuOpenState = 1f; + ImmutableArray salaryCrew = Mission.GetSalaryEligibleCrew().Where(c => c != character).ToImmutableArray(); + + Wallet targetWallet = character.Wallet; + + GUIFrame walletFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.35f), parent.RectTransform, anchor: Anchor.TopLeft) + { + RelativeOffset = new Vector2(0, 1.02f) + }); + + GUILayoutGroup walletLayout = new GUILayoutGroup(new RectTransform(ToolBox.PaddingSizeParentRelative(walletFrame.RectTransform, 0.9f), walletFrame.RectTransform, anchor: Anchor.Center)); + + GUILayoutGroup headerLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.33f), walletLayout.RectTransform), isHorizontal: true); + GUIImage icon = new GUIImage(new RectTransform(Vector2.One, headerLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "StoreTradingIcon", scaleToFit: true); + float relativeX = icon.RectTransform.NonScaledSize.X / (float)icon.Parent.RectTransform.NonScaledSize.X; + GUILayoutGroup headerTextLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f - relativeX, 1f), headerLayout.RectTransform), isHorizontal: true) { Stretch = true }; + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), headerTextLayout.RectTransform), TextManager.Get("crewwallet.wallet"), font: GUIStyle.LargeFont); + GUITextBlock moneyBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), headerTextLayout.RectTransform), UpgradeStore.FormatCurrency(targetWallet.Balance), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right); + + GUILayoutGroup middleLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.66f), walletLayout.RectTransform)); + GUILayoutGroup salaryTextLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), middleLayout.RectTransform), isHorizontal: true); + GUITextBlock salaryTitle = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), salaryTextLayout.RectTransform), TextManager.Get("crewwallet.salary"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft); + GUITextBlock rewardBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), salaryTextLayout.RectTransform), $"{Mission.GetRewardShare(targetWallet.RewardDistribution, salaryCrew, Option.None()).Percentage}%", textAlignment: Alignment.BottomRight); + GUILayoutGroup sliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), middleLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.Center); + GUIScrollBar salarySlider = new GUIScrollBar(new RectTransform(new Vector2(0.9f, 1f), sliderLayout.RectTransform), style: "GUISlider", barSize: 0.03f) + { + Range = Vector2.UnitY, + BarScrollValue = targetWallet.RewardDistribution / 100f, + Step = 0.01f, + BarSize = 0.1f, + OnMoved = (bar, scroll) => + { + rewardBlock.Text = $"{Mission.GetRewardShare((int)(scroll * 100f), salaryCrew, Option.None()).Percentage}%"; + return true; + }, + OnReleased = (bar, scroll) => + { + int newRewardDistribution = (int)(scroll * 100); + if (newRewardDistribution == targetWallet.RewardDistribution) { return false; } + SetRewardDistribution(character, newRewardDistribution); + return true; + } + }; +// @formatter:off + GUIScissorComponent scissorComponent = new GUIScissorComponent(new RectTransform(new Vector2(0.85f, 1.25f), walletFrame.RectTransform, Anchor.BottomCenter, Pivot.TopCenter)) + { + CanBeFocused = false + }; + transferMenu = new GUIFrame(new RectTransform(Vector2.One, scissorComponent.Content.RectTransform)); + + GUILayoutGroup transferMenuLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.8f), transferMenu.RectTransform, Anchor.BottomLeft), childAnchor: Anchor.Center); + GUILayoutGroup paddedTransferMenuLayout = new GUILayoutGroup(new RectTransform(ToolBox.PaddingSizeParentRelative(transferMenuLayout.RectTransform, 0.85f), transferMenuLayout.RectTransform)); + GUILayoutGroup mainLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), paddedTransferMenuLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + GUILayoutGroup leftLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1f), mainLayout.RectTransform)); + GUITextBlock leftName = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), leftLayout.RectTransform), character.Name, textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont); + GUITextBlock leftBalance = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), leftLayout.RectTransform), UpgradeStore.FormatCurrency(targetWallet.Balance), textAlignment: Alignment.Left) { TextColor = GUIStyle.Blue }; + GUILayoutGroup rightLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1f), mainLayout.RectTransform), childAnchor: Anchor.TopRight); + GUITextBlock rightName = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), rightLayout.RectTransform), string.Empty, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterRight); + GUITextBlock rightBalance = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), rightLayout.RectTransform), string.Empty, textAlignment: Alignment.Right) { TextColor = GUIStyle.Red }; + GUILayoutGroup centerLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.9f), mainLayout.RectTransform, Anchor.Center), childAnchor: Anchor.Center) { IgnoreLayoutGroups = true }; + new GUIFrame(new RectTransform(new Vector2(0f, 1f), centerLayout.RectTransform, Anchor.Center), style: "VerticalLine") { IgnoreLayoutGroups = true }; + GUIButton centerButton = new GUIButton(new RectTransform(new Vector2(0.6f), centerLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight, anchor: Anchor.Center), style: "GUIButtonTransferArrow"); + + GUILayoutGroup inputLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.25f), paddedTransferMenuLayout.RectTransform), childAnchor: Anchor.Center); + GUINumberInput transferAmountInput = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1f), inputLayout.RectTransform), GUINumberInput.NumberType.Int, hidePlusMinusButtons: true) + { + MinValueInt = 0 + }; + + GUILayoutGroup buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.25f), paddedTransferMenuLayout.RectTransform), childAnchor: Anchor.Center); + GUILayoutGroup centerButtonLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.75f, 1f), buttonLayout.RectTransform), isHorizontal: true); + GUIButton confirmButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), centerButtonLayout.RectTransform), TextManager.Get("confirm"), style: "GUIButtonFreeScale") { Enabled = false }; + GUIButton resetButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), centerButtonLayout.RectTransform), TextManager.Get("reset"), style: "GUIButtonFreeScale") { Enabled = false }; +// @formatter:on + transferMenuButton = new GUIButton(new RectTransform(new Vector2(0.5f, 0.2f), walletFrame.RectTransform, Anchor.BottomCenter, Pivot.TopCenter), style: "UIToggleButtonVertical") + { + OnClicked = (button, o) => + { + isTransferMenuOpen = !isTransferMenuOpen; + if (!isTransferMenuOpen) + { + transferAmountInput.IntValue = 0; + } + ToggleTransferMenuIcon(button, open: isTransferMenuOpen); + return true; + } + }; + ToggleTransferMenuIcon(transferMenuButton, open: isTransferMenuOpen); + ToggleCenterButton(centerButton, isSending); + + if (GameMain.GameSession?.Campaign is MultiPlayerCampaign campaign) + { + if (!(Character.Controlled is { } myCharacter)) + { + salarySlider.Enabled = false; + transferAmountInput.Enabled = false; + centerButton.Enabled = false; + confirmButton.Enabled = false; + return; + } + + bool hasPermissions = campaign.AllowedToManageCampaign(); + salarySlider.Enabled = hasPermissions; + Wallet otherWallet; + + switch (hasPermissions) + { + case true: + rightName.Text = TextManager.Get("crewwallet.bank"); + otherWallet = campaign.Bank; + break; + case false when character == myCharacter: + rightName.Text = TextManager.Get("crewwallet.bank"); + otherWallet = campaign.Bank; + isSending = true; + ToggleCenterButton(centerButton, isSending); + break; + default: + rightName.Text = myCharacter.Name; + otherWallet = campaign.PersonalWallet; + break; + } + + if (!hasPermissions) + { + centerButton.Enabled = centerButton.CanBeFocused = false; + salarySlider.Enabled = salarySlider.CanBeFocused = false; + } + + leftBalance.Text = UpgradeStore.FormatCurrency(otherWallet.Balance); + + UpdateAllInputs(); + + centerButton.OnClicked = (btn, o) => + { + isSending = !isSending; + ToggleCenterButton(btn, isSending); + UpdateAllInputs(); + return true; + }; + + transferAmountInput.OnValueChanged = input => + { + UpdateInputs(); + }; + + transferAmountInput.OnValueEntered = input => + { + UpdateAllInputs(); + }; + + campaign.OnMoneyChanged.RegisterOverwriteExisting(nameof(CreateWalletFrame).ToIdentifier(), e => + { + if (e.Wallet == targetWallet) + { + moneyBlock.Text = UpgradeStore.FormatCurrency(e.Info.Balance); + salarySlider.BarScrollValue = e.Info.RewardDistribution / 100f; + } + UpdateAllInputs(); + }); + + resetButton.OnClicked = (button, o) => + { + transferAmountInput.IntValue = 0; + UpdateAllInputs(); + return true; + }; + + confirmButton.OnClicked = (button, o) => + { + int amount = transferAmountInput.IntValue; + if (amount == 0) { return false; } + + Option target1 = Option.Some(character), + target2 = otherWallet == campaign.Bank ? Option.None() : Option.Some(myCharacter); + if (isSending) { (target1, target2) = (target2, target1); } + + SendTransaction(target1, target2, amount); + isTransferMenuOpen = false; + ToggleTransferMenuIcon(transferMenuButton, isTransferMenuOpen); + return true; + }; + + void UpdateAllInputs() + { + UpdateInputs(); + UpdateMaxInput(); + } + + void UpdateInputs() + { + confirmButton.Enabled = resetButton.Enabled = transferAmountInput.IntValue > 0; + if (transferAmountInput.IntValue == 0) + { + rightBalance.Text = UpgradeStore.FormatCurrency(otherWallet.Balance); + rightBalance.TextColor = GUIStyle.TextColorNormal; + leftBalance.Text = UpgradeStore.FormatCurrency(targetWallet.Balance); + leftBalance.TextColor = GUIStyle.TextColorNormal; + } + else if (isSending) + { + rightBalance.Text = UpgradeStore.FormatCurrency(otherWallet.Balance + transferAmountInput.IntValue); + rightBalance.TextColor = GUIStyle.Blue; + leftBalance.Text = UpgradeStore.FormatCurrency(targetWallet.Balance - transferAmountInput.IntValue); + leftBalance.TextColor = GUIStyle.Red; + } + else + { + rightBalance.Text = UpgradeStore.FormatCurrency(otherWallet.Balance - transferAmountInput.IntValue); + rightBalance.TextColor = GUIStyle.Red; + leftBalance.Text = UpgradeStore.FormatCurrency(targetWallet.Balance + transferAmountInput.IntValue); + leftBalance.TextColor = GUIStyle.Blue; + } + } + + void UpdateMaxInput() + { + transferAmountInput.MaxValueInt = isSending ? targetWallet.Balance : otherWallet.Balance; + } + } + + static void ToggleTransferMenuIcon(GUIButton btn, bool open) + { + foreach (GUIComponent child in btn.Children) + { + child.SpriteEffects = open ? SpriteEffects.None : SpriteEffects.FlipVertically; + } + } + + static void ToggleCenterButton(GUIButton btn, bool isSending) + { + foreach (GUIComponent child in btn.Children) + { + child.SpriteEffects = isSending ? SpriteEffects.None : SpriteEffects.FlipHorizontally; + } + } + + static void SendTransaction(Option to, Option from, int amount) + { + INetSerializableStruct transfer = new NetWalletTransfer + { + Sender = from.Select(option => option.ID), + Receiver = to.Select(option => option.ID), + Amount = amount + }; + IWriteMessage msg = new WriteOnlyMessage().WithHeader(ClientPacketHeader.MONEY); + transfer.Write(msg); + GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable); + } + + static void SetRewardDistribution(Character character, int newValue) + { + INetSerializableStruct transfer = new NetWalletSalaryUpdate + { + Target = character.ID, + NewRewardDistribution = newValue + }; + IWriteMessage msg = new WriteOnlyMessage().WithHeader(ClientPacketHeader.REWARD_DISTRIBUTION); + transfer.Write(msg); + GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable); + } + + static int GetRewardDistributionPercentage(int distribution, ImmutableArray crew) + { + return Mission.GetRewardShare(distribution, crew, Option.None()).Percentage; + } + } + private GUIComponent CreateClientInfoFrame(GUIFrame frame, Client client, Sprite permissionIcon = null) { GUIComponent paddedFrame; @@ -1209,8 +1534,6 @@ namespace Barotrauma } } private Color unselectedColor = new Color(240, 255, 255, 225); - private Color selectedColor = new Color(220, 255, 220, 225); - private Color ownedColor = new Color(140, 180, 140, 225); private Color unselectableColor = new Color(100, 100, 100, 225); private Color pressedColor = new Color(60, 60, 60, 225); @@ -1427,7 +1750,7 @@ namespace Barotrauma GUILayoutGroup talentOptionLayoutGroup = new GUILayoutGroup(new RectTransform(Vector2.One, talentOptionCenterGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; - foreach (TalentPrefab talent in talentOption.Talents) + foreach (TalentPrefab talent in talentOption.Talents.OrderBy(t => t.Identifier)) { GUIFrame talentFrame = new GUIFrame(new RectTransform(Vector2.One, talentOptionLayoutGroup.RectTransform), style: null) { @@ -1652,7 +1975,7 @@ namespace Barotrauma controlledCharacter.GiveTalent(talent); if (GameMain.Client != null) { - GameMain.Client.CreateEntityEvent(controlledCharacter, new object[] { NetEntityEvent.Type.UpdateTalents }); + GameMain.Client.CreateEntityEvent(controlledCharacter, new Character.UpdateTalentsEventData()); } } selectedTalents = controlledCharacter.Info.GetUnlockedTalentsInTree().ToList(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index a5be0901b..f4ec7f408 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -41,7 +41,7 @@ namespace Barotrauma private readonly CampaignUI campaignUI; private CampaignMode? Campaign => campaignUI.Campaign; - private int AvailableMoney => Campaign?.Money ?? 0; + private Wallet PlayerWallet => Campaign?.Wallet ?? Wallet.Invalid; private UpgradeTab selectedUpgradeTab = UpgradeTab.Upgrade; private GUIMessageBox? currectConfirmation; @@ -61,7 +61,7 @@ namespace Barotrauma private Vector2[][] subHullVertices = new Vector2[0][]; private List submarineWalls = new List(); - public MapEntity? HoveredItem; + public MapEntity? HoveredEntity; private bool highlightWalls; private UpgradeCategory? currentUpgradeCategory; @@ -105,6 +105,7 @@ namespace Barotrauma Campaign.UpgradeManager.OnUpgradesChanged += RefreshAll; Campaign.CargoManager.OnPurchasedItemsChanged += RefreshAll; Campaign.CargoManager.OnSoldItemsChanged += RefreshAll; + Campaign.OnMoneyChanged.RegisterOverwriteExisting(nameof(UpgradeStore).ToIdentifier(), e => { RefreshAll(); } ); } public void RefreshAll() @@ -184,7 +185,7 @@ namespace Barotrauma } // reset the order first - foreach (UpgradeCategory category in UpgradeCategory.Categories) + foreach (UpgradeCategory category in UpgradeCategory.Categories.OrderBy(c => c.Name)) { GUIComponent component = categoryList.Content.FindChild(c => c.UserData is CategoryData categoryData && categoryData.Category == category); component?.SetAsLastChild(); @@ -286,7 +287,7 @@ namespace Barotrauma GUILayoutGroup rightLayout = new GUILayoutGroup(rectT(0.5f, 1, topHeaderLayout), childAnchor: Anchor.TopRight); GUILayoutGroup priceLayout = new GUILayoutGroup(rectT(1, 0.8f, rightLayout), childAnchor: Anchor.Center) { RelativeSpacing = 0.08f }; new GUITextBlock(rectT(1f, 0f, priceLayout), TextManager.Get("CampaignStore.Balance"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right); - new GUITextBlock(rectT(1f, 0f, priceLayout), FormatCurrency(AvailableMoney, format: true), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right) { TextGetter = () => FormatCurrency(AvailableMoney, format: true) }; + new GUITextBlock(rectT(1f, 0f, priceLayout), FormatCurrency(PlayerWallet.Balance, format: true), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right) { TextGetter = () => FormatCurrency(PlayerWallet.Balance, format: true) }; new GUIFrame(rectT(0.5f, 0.1f, rightLayout, Anchor.BottomRight), style: "HorizontalLine") { IgnoreLayoutGroups = true }; repairButton.OnClicked = upgradeButton.OnClicked = (button, o) => @@ -343,15 +344,15 @@ namespace Barotrauma private void DrawItemSwapPreview(SpriteBatch spriteBatch, GUICustomComponent component) { var selectedItem = customizeTabOpen ? - activeItemSwapSlideDown?.UserData as Item ?? HoveredItem as Item : - HoveredItem as Item; + activeItemSwapSlideDown?.UserData as Item ?? HoveredEntity as Item : + HoveredEntity as Item; if (selectedItem?.Prefab.SwappableItem == null) { return; } Sprite schematicsSprite = selectedItem.Prefab.SwappableItem.SchematicSprite; if (schematicsSprite == null) { return; } float schematicsScale = Math.Min(component.Rect.Width / 2 / schematicsSprite.size.X, component.Rect.Height / schematicsSprite.size.Y); Vector2 center = new Vector2(component.Rect.Center.X, component.Rect.Center.Y); - schematicsSprite.Draw(spriteBatch, new Vector2(component.Rect.X, center.Y), GUIStyle.Green, new Vector2(0, schematicsSprite.size.Y / 2), + schematicsSprite.Draw(spriteBatch, new Vector2(component.Rect.X, center.Y), GUIStyle.Green, new Vector2(0, schematicsSprite.size.Y / 2), scale: schematicsScale); var swappableItemList = selectedUpgradeCategoryLayout?.FindChild("prefablist", true) as GUIListBox; @@ -426,14 +427,14 @@ namespace Barotrauma return false; } - if (AvailableMoney >= hullRepairCost) + if (PlayerWallet.CanAfford(hullRepairCost)) { LocalizedString body = TextManager.GetWithVariable("WallRepairs.PurchasePromptBody", "[amount]", hullRepairCost.ToString()); currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), body, () => { - if (AvailableMoney >= hullRepairCost) + if (PlayerWallet.Balance >= hullRepairCost) { - Campaign.Money -= hullRepairCost; + PlayerWallet.TryDeduct(hullRepairCost); GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "hullrepairs"); Campaign.PurchasedHullRepairs = true; button.Enabled = false; @@ -461,14 +462,14 @@ namespace Barotrauma CreateRepairEntry(currentStoreLayout.Content, TextManager.Get("repairallitems"), "RepairItemsButton", itemRepairCost, (button, o) => { - if (AvailableMoney >= itemRepairCost && !Campaign.PurchasedItemRepairs) + if (PlayerWallet.Balance >= itemRepairCost && !Campaign.PurchasedItemRepairs) { LocalizedString body = TextManager.GetWithVariable("ItemRepairs.PurchasePromptBody", "[amount]", itemRepairCost.ToString()); currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), body, () => { - if (AvailableMoney >= itemRepairCost && !Campaign.PurchasedItemRepairs) + if (PlayerWallet.Balance >= itemRepairCost && !Campaign.PurchasedItemRepairs) { - Campaign.Money -= itemRepairCost; + PlayerWallet.TryDeduct(itemRepairCost); GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "devicerepairs"); Campaign.PurchasedItemRepairs = true; button.Enabled = false; @@ -507,14 +508,14 @@ namespace Barotrauma return false; } - if (AvailableMoney >= shuttleRetrieveCost && !Campaign.PurchasedLostShuttles) + if (PlayerWallet.CanAfford(shuttleRetrieveCost) && !Campaign.PurchasedLostShuttles) { LocalizedString body = TextManager.GetWithVariable("ReplaceLostShuttles.PurchasePromptBody", "[amount]", shuttleRetrieveCost.ToString()); currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), body, () => { - if (AvailableMoney >= shuttleRetrieveCost && !Campaign.PurchasedLostShuttles) + if (PlayerWallet.Balance >= shuttleRetrieveCost && !Campaign.PurchasedLostShuttles) { - Campaign.Money -= shuttleRetrieveCost; + PlayerWallet.TryDeduct(shuttleRetrieveCost); GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "retrieveshuttle"); Campaign.PurchasedLostShuttles = true; button.Enabled = false; @@ -572,13 +573,13 @@ namespace Barotrauma new GUITextBlock(rectT(1, 0, textLayout), title, font: GUIStyle.SubHeadingFont) { CanBeFocused = false, AutoScaleHorizontal = true }; new GUITextBlock(rectT(1, 0, textLayout), FormatCurrency(price)); GUILayoutGroup buyButtonLayout = new GUILayoutGroup(rectT(0.2f, 1, contentLayout), childAnchor: Anchor.Center) { UserData = "buybutton" }; - new GUIButton(rectT(0.7f, 0.5f, buyButtonLayout), string.Empty, style: "RepairBuyButton") { ClickSound = GUISoundType.HireRepairClick, Enabled = AvailableMoney >= price && !isDisabled, OnClicked = onPressed }; + new GUIButton(rectT(0.7f, 0.5f, buyButtonLayout), string.Empty, style: "RepairBuyButton") { ClickSound = GUISoundType.HireRepairClick, Enabled = PlayerWallet.Balance >= price && !isDisabled, OnClicked = onPressed }; contentLayout.Recalculate(); buyButtonLayout.Recalculate(); if (disableElement) { - frameChild.Enabled = AvailableMoney >= price && !isDisabled; + frameChild.Enabled = PlayerWallet.Balance >= price && !isDisabled; } if (!HasPermission) @@ -610,9 +611,9 @@ namespace Barotrauma Dictionary> upgrades = new Dictionary>(); - foreach (UpgradeCategory category in UpgradeCategory.Categories) + foreach (UpgradeCategory category in UpgradeCategory.Categories.OrderBy(c => c.Name)) { - foreach (UpgradePrefab prefab in UpgradePrefab.Prefabs) + foreach (UpgradePrefab prefab in UpgradePrefab.Prefabs.OrderBy(p => p.Name)) { if (prefab.UpgradeCategories.Contains(category)) { @@ -963,12 +964,12 @@ namespace Barotrauma buttonStyle: isPurchased ? "WeaponInstallButton" : "StoreAddToCrateButton")); if (!(frames.Last().FindChild(c => c is GUIButton, recursive: true) is GUIButton buyButton)) { continue; } - if (Campaign.Money >= price) + if (PlayerWallet.CanAfford(price)) { buyButton.Enabled = true; buyButton.OnClicked += (button, o) => { - LocalizedString promptBody = TextManager.GetWithVariables(isPurchased ? "upgrades.itemswappromptbody" : "upgrades.purchaseitemswappromptbody", + LocalizedString promptBody = TextManager.GetWithVariables(isPurchased ? "upgrades.itemswappromptbody" : "upgrades.purchaseitemswappromptbody", ("[itemtoinstall]", replacement.Name), ("[amount]", (replacement.SwappableItem.GetPrice(Campaign?.Map?.CurrentLocation) * linkedItems.Count).ToString())); currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), promptBody, () => @@ -1183,7 +1184,7 @@ namespace Barotrauma buyButton.OnClicked += (button, o) => { - LocalizedString promptBody = TextManager.GetWithVariables("Upgrades.PurchasePromptBody", + LocalizedString promptBody = TextManager.GetWithVariables("Upgrades.PurchasePromptBody", ("[upgradename]", prefab.Name), ("[amount]", prefab.Price.GetBuyprice(Campaign.UpgradeManager.GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation).ToString())); currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), promptBody, () => @@ -1334,16 +1335,16 @@ namespace Barotrauma { if (GUI.MouseOn == frame) { - if (HoveredItem != item) { CreateItemTooltip(item); } - HoveredItem = item; + if (HoveredEntity != item) { CreateItemTooltip(item); } + HoveredEntity = item; if (PlayerInput.PrimaryMouseButtonClicked() && selectedUpgradeTab == UpgradeTab.Upgrade && currentStoreLayout != null) { if (customizeTabOpen) { if (selectedUpgradeCategoryLayout != null) { - var linkedItems = HoveredItem is Item ? Campaign.UpgradeManager.GetLinkedItemsToSwap((Item)HoveredItem) : new List(); - if (selectedUpgradeCategoryLayout.FindChild(c => c.UserData as Item == HoveredItem || linkedItems.Contains((Item)c.UserData), recursive: true) is GUIButton itemElement) + var linkedItems = HoveredEntity is Item hoveredItem ? Campaign.UpgradeManager.GetLinkedItemsToSwap(hoveredItem) : new List(); + if (selectedUpgradeCategoryLayout.FindChild(c => c.UserData is Item item && (item == HoveredEntity || linkedItems.Contains(item)), recursive: true) is GUIButton itemElement) { if (!itemElement.Selected) { itemElement.OnClicked(itemElement, itemElement.UserData); } (itemElement.Parent?.Parent?.Parent as GUIListBox)?.ScrollToElement(itemElement); @@ -1370,8 +1371,8 @@ namespace Barotrauma // use pnpoly algorithm to detect if our mouse is within any of the hull polygons if (subHullVertices.Any(hullVertex => ToolBox.PointIntersectsWithPolygon(PlayerInput.MousePosition, hullVertex))) { - if (HoveredItem != firstStructure && !(firstStructure is null)) { CreateItemTooltip(firstStructure); } - HoveredItem = firstStructure; + if (HoveredEntity != firstStructure && !(firstStructure is null)) { CreateItemTooltip(firstStructure); } + HoveredEntity = firstStructure; isMouseOnStructure = true; GUI.MouseCursor = CursorState.Hand; @@ -1382,7 +1383,7 @@ namespace Barotrauma } } - if (!isMouseOnStructure) { HoveredItem = null; } + if (!isMouseOnStructure) { HoveredEntity = null; } } // flip the tooltip if it is outside of the screen @@ -1582,12 +1583,12 @@ namespace Barotrauma priceLabel.Text = TextManager.Get("Upgrade.MaxedUpgrade"); } } - + GUIButton button = buttonParent.GetChild(); if (button != null) { button.Enabled = currentLevel < prefab.MaxLevel; - if (WaitForServerUpdate || !campaign.AllowedToManageCampaign() || price > campaign.Money) + if (WaitForServerUpdate || !campaign.AllowedToManageCampaign() || !campaign.Wallet.CanAfford(price)) { button.Enabled = false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs index f25e1db11..81e700dc4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs @@ -184,7 +184,7 @@ namespace Barotrauma } // Exchange money Location.StoreCurrentBalance -= itemValue; - campaign.Money += itemValue; + campaign.Wallet.TryDeduct(itemValue); GameAnalyticsManager.AddMoneyGainedEvent(itemValue, GameAnalyticsManager.MoneySource.Store, item.ItemPrefab.Identifier.Value); // Remove from the sell crate diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 86d82528c..d00290a64 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -236,7 +236,7 @@ namespace Barotrauma if (crewManager != null) { - Order order = new Order(orderPrefab, Identifier.Empty, CharacterInfo.HighestManualOrderPriority, Order.OrderType.Current, null, null, orderGiver: Character.Controlled); + Order order = orderPrefab.CreateInstance(OrderPrefab.OrderTargetType.Entity, orderGiver: Character.Controlled); crewManager.SetCharacterOrder(null, order); if (crewManager.IsSinglePlayer) { HumanAIController.ReportProblem(Character.Controlled, order); } } @@ -290,16 +290,6 @@ namespace Barotrauma return crewArea.Rect; } - public IEnumerable GetCharacters() - { - return characters; - } - - public IEnumerable GetCharacterInfos() - { - return characterInfos; - } - /// /// Remove the character from the crew (and crew menus). /// @@ -708,7 +698,8 @@ namespace Barotrauma List availableSpeakers = Character.CharacterList.FindAll(c => c.AIController is HumanAIController && !c.IsDead && - c.SpeechImpediment <= 100.0f); + c.SpeechImpediment <= 100.0f && + c.CharacterHealth.GetAllAfflictions(a => a is AfflictionHusk huskInfection && huskInfection.Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: true }).None()); pendingConversationLines.AddRange(NPCConversation.CreateRandom(availableSpeakers)); } @@ -837,7 +828,7 @@ namespace Barotrauma var orderGiver = order?.OrderGiver; if (IsSinglePlayer) { - character.SetOrder(order, speak: orderGiver != character); + character.SetOrder(order, isNewOrder, speak: orderGiver != character); string message = order?.GetChatMessage(character.Name, orderGiver?.CurrentHull?.DisplayName?.Value, givingOrderToSelf: character == orderGiver, orderOption: order?.Option ?? Identifier.Empty, isNewOrder: isNewOrder); orderGiver?.Speak(message); } @@ -2437,26 +2428,26 @@ namespace Barotrauma optionNodes.Add(new OptionNode(node, Keys.D0 + hotkey % 10)); } + /// + /// The order giver doesn't need to be set for the Order instances as it will be set when the node button is clicked. + /// private void CreateShortcutNodes() { - bool HasAppropriateJobId(Character c, Identifier jobId) => c.Info?.Job != null && c.Info.Job.Prefab.AppropriateOrders.Contains(jobId); - bool HasAppropriateJob(Character c, string jobId) => HasAppropriateJobId(c, jobId.ToIdentifier()); - - var sub = GetTargetSubmarine(); - if (sub == null) { return; } + if (!(GetTargetSubmarine() is { } sub)) { return; } shortcutNodes.Clear(); - if (CanFitMoreNodes() && sub.GetItems(false).Find(i => i.HasTag("reactor") && i.IsPlayerTeamInteractable)?.GetComponent() is Reactor reactor) + var subItems = sub.GetItems(false); + if (CanFitMoreNodes() && subItems.Find(i => i.HasTag("reactor") && i.IsPlayerTeamInteractable)?.GetComponent() is Reactor reactor) { float reactorOutput = -reactor.CurrPowerConsumption; // If player is not an engineer AND the reactor is not powered up AND nobody is using the reactor - // ---> Create shortcut node for "Operate Reactor" order's "Power Up" option + // --> Create shortcut node for "Operate Reactor" order's "Power Up" option if (ShouldDelegateOrder("operatereactor") && reactorOutput < float.Epsilon && characters.None(c => c.SelectedConstruction == reactor.Item)) { var orderPrefab = OrderPrefab.Prefabs["operatereactor"]; - var order = new Order(orderPrefab, orderPrefab.Options[0], reactor.Item, reactor, Character.Controlled); + var order = new Order(orderPrefab, orderPrefab.Options[0], reactor.Item, reactor); if (IsNonDuplicateOrder(order)) { - shortcutNodes.Add(CreateOrderOptionNode(shortcutNodeSize, null, Point.Zero, order, -1)); + AddOrderNode(order); } } } @@ -2464,11 +2455,11 @@ namespace Barotrauma // If player is not a captain AND nobody is using the nav terminal AND the nav terminal is powered up // --> Create shortcut node for Steer order if (CanFitMoreNodes() && ShouldDelegateOrder("steer") && IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs["steer"]) && - sub.GetItems(false).Find(i => i.HasTag("navterminal") && i.IsPlayerTeamInteractable) is Item nav && characters.None(c => c.SelectedConstruction == nav) && + subItems.Find(i => i.HasTag("navterminal") && i.IsPlayerTeamInteractable) is Item nav && characters.None(c => c.SelectedConstruction == nav) && nav.GetComponent() is Steering steering && steering.Voltage > steering.MinVoltage) { - var order = new Order(OrderPrefab.Prefabs["steer"], steering.Item, steering, Character.Controlled); - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); + var order = new Order(OrderPrefab.Prefabs["steer"], steering.Item, steering); + AddOrderNode(order); } // If player is not a security officer AND invaders are reported // --> Create shorcut node for Fight Intruders order @@ -2476,8 +2467,7 @@ namespace Barotrauma ActiveOrders.Any(o => o.Order.Identifier == "reportintruders") && IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs["fightintruders"])) { - var order = new Order(OrderPrefab.Prefabs["fightintruders"], null, orderGiver: Character.Controlled); - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); + AddOrderNodeWithIdentifier("fightintruders"); } // If player is not a mechanic AND a breach has been reported // --> Create shorcut node for Fix Leaks order @@ -2485,8 +2475,7 @@ namespace Barotrauma IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs["fixleaks"]) && ActiveOrders.Any(o => o.Order.Identifier == "reportbreach")) { - var order = new Order(OrderPrefab.Prefabs["fixleaks"], null, orderGiver: Character.Controlled); - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); + AddOrderNodeWithIdentifier("fixleaks"); } // --> Create shortcut nodes for the Repair orders if (CanFitMoreNodes() && ActiveOrders.Any(o => o.Order.Identifier == "reportbrokendevices")) @@ -2494,33 +2483,27 @@ namespace Barotrauma var reportBrokenDevices = OrderPrefab.Prefabs["reportbrokendevices"]; // TODO: Doesn't work for player issued reports, because they don't have a target. bool useSpecificRepairOrder = false; - string tag = "repairelectrical"; - if (CanFitMoreNodes() && ShouldDelegateOrder(tag) && + if (CanFitMoreNodes() && ShouldDelegateOrder("repairelectrical") && ActiveOrders.Any(o => o.Order.Prefab == reportBrokenDevices && o.Order.TargetItemComponent is Repairable r && r.requiredSkills.Any(s => s.Identifier == "electrical"))) { - if (IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs[tag])) + if (IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs["repairelectrical"])) { - var order = new Order(OrderPrefab.Prefabs[tag], null, orderGiver: Character.Controlled); - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); + AddOrderNodeWithIdentifier("repairelectrical"); } useSpecificRepairOrder = true; } - tag = "repairmechanical"; - if (CanFitMoreNodes() && ShouldDelegateOrder(tag) && + if (CanFitMoreNodes() && ShouldDelegateOrder("repairmechanical") && ActiveOrders.Any(o => o.Order.Prefab == reportBrokenDevices && o.Order.TargetItemComponent is Repairable r && r.requiredSkills.Any(s => s.Identifier == "mechanical"))) { - if (IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs[tag])) + if (IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs["repairmechanical"])) { - var order = new Order(OrderPrefab.Prefabs[tag], null, orderGiver: Character.Controlled); - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); + AddOrderNodeWithIdentifier("repairmechanical"); } useSpecificRepairOrder = true; } - tag = "repairsystems"; - if (!useSpecificRepairOrder && CanFitMoreNodes() && ShouldDelegateOrder(tag) && OrderPrefab.Prefabs[tag] is OrderPrefab repairOrder && IsNonDuplicateOrderPrefab(repairOrder)) + if (!useSpecificRepairOrder && CanFitMoreNodes() && ShouldDelegateOrder("repairsystems") && OrderPrefab.Prefabs["repairsystems"] is OrderPrefab repairOrder && IsNonDuplicateOrderPrefab(repairOrder)) { - var order = new Order(OrderPrefab.Prefabs[tag], null, orderGiver: Character.Controlled); - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); + AddOrderNodeWithIdentifier("repairsystems"); } } // If fire is reported @@ -2528,8 +2511,7 @@ namespace Barotrauma if (CanFitMoreNodes() && IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs["extinguishfires"]) && ActiveOrders.Any(o => o.Order.Identifier == "reportfire")) { - var order = new Order(OrderPrefab.Prefabs["extinguishfires"], null, orderGiver: Character.Controlled); - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); + AddOrderNodeWithIdentifier("extinguishfires"); } if (CanFitMoreNodes() && characterContext?.Info?.Job?.Prefab?.AppropriateOrders != null) { @@ -2541,8 +2523,8 @@ namespace Barotrauma { if (!orderPrefab.MustSetTarget || orderPrefab.GetMatchingItems(sub, true, interactableFor: characterContext ?? Character.Controlled).Any()) { - var order = new Order(orderPrefab, null, orderGiver: Character.Controlled); - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); + var order = orderPrefab.CreateInstance(OrderPrefab.OrderTargetType.Entity); + AddOrderNode(order); } if (!CanFitMoreNodes()) { break; } } @@ -2550,9 +2532,8 @@ namespace Barotrauma } if (CanFitMoreNodes() && characterContext != null && !characterContext.IsDismissed) { - var order = new Order(OrderPrefab.Dismissal, null, orderGiver: Character.Controlled); - shortcutNodes.Add( - CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); + var order = OrderPrefab.Dismissal.CreateInstance(OrderPrefab.OrderTargetType.Entity); + AddOrderNode(order); } shortcutNodes.RemoveAll(n => n.UserData is Order o && !IsOrderAvailable(o)); if (shortcutNodes.Count < 1) { return; } @@ -2593,18 +2574,30 @@ namespace Barotrauma characterContext.CurrentOrders.None(oi => oi?.Identifier == orderPrefab?.Identifier) : characterContext.CurrentOrders.None(oi => oi?.Identifier == orderPrefab?.Identifier && oi.Option == option)); } + void AddOrderNodeWithIdentifier(string identifier) + { + var order = OrderPrefab.Prefabs[identifier].CreateInstance(OrderPrefab.OrderTargetType.Entity); + AddOrderNode(order); + } + void AddOrderNode(Order order) + { + var node = order.Option.IsEmpty ? + CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1) : + CreateOrderOptionNode(shortcutNodeSize, null, Point.Zero, order, -1); + shortcutNodes.Add(node); + } } private void CreateOrderNodes(OrderCategory orderCategory) { - var orderPrefabs = OrderPrefab.Prefabs.Where(o => o.Category == orderCategory && !o.IsReport && IsOrderAvailable(o)).ToArray(); + var orderPrefabs = OrderPrefab.Prefabs.Where(o => o.Category == orderCategory && !o.IsReport && IsOrderAvailable(o)).OrderBy(o => o.Identifier).ToArray(); Order order; bool disableNode; var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, GetCircumferencePointCount(orderPrefabs.Length), GetFirstNodeAngle(orderPrefabs.Length)); for (int i = 0; i < orderPrefabs.Length; i++) { - order = new Order(orderPrefabs[i], null, orderGiver: Character.Controlled); + order = orderPrefabs[i].CreateInstance(OrderPrefab.OrderTargetType.Entity); disableNode = !CanCharacterBeHeard() || (order.MustSetTarget && (order.ItemComponentType != null || order.GetTargetItems().Any() || order.RequireItems.Any()) && order.GetMatchingItems(true, interactableFor: characterContext ?? Character.Controlled).None()); @@ -2617,11 +2610,13 @@ namespace Barotrauma /// /// Create order nodes based on the item context /// + /// + /// The order giver doesn't need to be set for the Order instances as it will be set when the node button is clicked. + /// private void CreateContextualOrderNodes() { if (contextualOrders.None()) { - string orderIdentifier; // Check if targeting an item or a hull if (itemContext != null && itemContext.IsPlayerTeamInteractable) { @@ -2636,66 +2631,62 @@ namespace Barotrauma { if (p.TargetItemsMatchItem(itemContext, option)) { - contextualOrders.Add(new Order(p, itemContext, targetComponent, Character.Controlled).WithOption(option)); + contextualOrders.Add(new Order(p, option, itemContext, targetComponent)); } } } else if (p.TargetItemsMatchItem(itemContext) || p.TryGetTargetItemComponent(itemContext, out targetComponent)) { contextualOrders.Add(p.HasOptions ? - new Order(p, null, orderGiver: Character.Controlled) : - new Order(p, itemContext, targetComponent, Character.Controlled)); + p.CreateInstance(OrderPrefab.OrderTargetType.Entity) : + new Order(p, itemContext, targetComponent)); } } // If targeting a periscope connected to a turret, show the 'operateweapons' order - orderIdentifier = "operateweapons"; - var operateWeaponsPrefab = OrderPrefab.Prefabs[orderIdentifier]; - if (contextualOrders.None(o => o.Identifier == orderIdentifier) && itemContext.Components.Any(c => c is Controller)) + var operateWeaponsPrefab = OrderPrefab.Prefabs["operateweapons"]; + if (contextualOrders.None(o => o.Identifier == "operateweapons") && itemContext.Components.Any(c => c is Controller)) { var turret = itemContext.GetConnectedComponents().FirstOrDefault(c => operateWeaponsPrefab.TargetItemsMatchItem(c.Item)) ?? itemContext.GetConnectedComponents(recursive: true).FirstOrDefault(c => operateWeaponsPrefab.TargetItemsMatchItem(c.Item)); if (turret != null) { - contextualOrders.Add(new Order(operateWeaponsPrefab, turret.Item, turret, Character.Controlled)); + contextualOrders.Add(new Order(operateWeaponsPrefab, turret.Item, turret)); } } // If targeting a repairable item with condition below the repair threshold, show the 'repairsystems' order - orderIdentifier = "repairsystems"; - if (contextualOrders.None(order => order.Identifier == orderIdentifier) && itemContext.Repairables.Any(r => r.IsBelowRepairThreshold)) + if (contextualOrders.None(order => order.Identifier == "repairsystems") && itemContext.Repairables.Any(r => r.IsBelowRepairThreshold)) { if (itemContext.Repairables.Any(r => r != null && r.requiredSkills.Any(s => s != null && s.Identifier.Equals("electrical")))) { - contextualOrders.Add(new Order(OrderPrefab.Prefabs["repairelectrical"], itemContext, targetItem: null, Character.Controlled)); + contextualOrders.Add(new Order(OrderPrefab.Prefabs["repairelectrical"], itemContext, targetItem: null)); } else if (itemContext.Repairables.Any(r => r != null && r.requiredSkills.Any(s => s != null && s.Identifier.Equals("mechanical")))) { - contextualOrders.Add(new Order(OrderPrefab.Prefabs["repairmechanical"], itemContext, targetItem: null, Character.Controlled)); + contextualOrders.Add(new Order(OrderPrefab.Prefabs["repairmechanical"], itemContext, targetItem: null)); } else { - contextualOrders.Add(new Order(OrderPrefab.Prefabs[orderIdentifier], itemContext, targetItem: null, Character.Controlled)); + contextualOrders.Add(new Order(OrderPrefab.Prefabs["repairsystems"], itemContext, targetItem: null)); } } // Remove the 'pumpwater' order if the target pump is auto-controlled (as it will immediately overwrite the work done by the bot) - orderIdentifier = "pumpwater"; - if (contextualOrders.FirstOrDefault(order => order.Identifier.Equals(orderIdentifier)) is Order pumpOrder && + if (contextualOrders.FirstOrDefault(order => order.Identifier.Equals("pumpwater")) is Order pumpOrder && itemContext.Components.FirstOrDefault(c => c.GetType() == pumpOrder.ItemComponentType) is Pump pump && pump.IsAutoControlled) { contextualOrders.Remove(pumpOrder); } - orderIdentifier = "cleanupitems"; - if (contextualOrders.None(info => info.Identifier.Equals(orderIdentifier))) + if (contextualOrders.None(info => info.Identifier.Equals("cleanupitems"))) { if (AIObjectiveCleanupItems.IsValidTarget(itemContext, Character.Controlled, checkInventory: false) || AIObjectiveCleanupItems.IsValidContainer(itemContext, Character.Controlled)) { - contextualOrders.Add(new Order(OrderPrefab.Prefabs[orderIdentifier], itemContext, targetItem: null, Character.Controlled)); + contextualOrders.Add(new Order(OrderPrefab.Prefabs["cleanupitems"], itemContext, targetItem: null)); } } AddIgnoreOrder(itemContext); } else if (hullContext != null) { - contextualOrders.Add(new Order(OrderPrefab.Prefabs["fixleaks"], hullContext, targetItem: null, Character.Controlled)); + contextualOrders.Add(new Order(OrderPrefab.Prefabs["fixleaks"], hullContext, targetItem: null)); if (wallContext != null) { AddIgnoreOrder(wallContext); @@ -2729,26 +2720,23 @@ namespace Barotrauma } } } - orderIdentifier = "wait"; - if (contextualOrders.None(order => order.Identifier.Equals(orderIdentifier))) + if (contextualOrders.None(order => order.Identifier.Equals("wait"))) { Vector2 position = GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition); Hull hull = Hull.FindHull(position, guess: Character.Controlled?.CurrentHull); - contextualOrders.Add(new Order(OrderPrefab.Prefabs[orderIdentifier], new OrderTarget(position, hull), Character.Controlled)); + contextualOrders.Add(new Order(OrderPrefab.Prefabs["wait"], new OrderTarget(position, hull))); } if (contextualOrders.None(order => order.Category != OrderCategory.Movement) && characters.Any(c => c != Character.Controlled)) { - orderIdentifier = "follow"; - if (contextualOrders.None(order => order.Identifier.Equals(orderIdentifier))) + if (contextualOrders.None(order => order.Identifier.Equals("follow"))) { - contextualOrders.Add(new Order(OrderPrefab.Prefabs[orderIdentifier], null, orderGiver: Character.Controlled)); + contextualOrders.Add(OrderPrefab.Prefabs["follow"].CreateInstance(OrderPrefab.OrderTargetType.Entity)); } } // Show 'dismiss' order only when there are crew members with active orders - orderIdentifier = "dismissed"; - if (contextualOrders.None(order => order.Identifier.Equals(orderIdentifier)) && characters.Any(c => !c.IsDismissed)) + if (contextualOrders.None(order => order.IsDismissal) && characters.Any(c => !c.IsDismissed)) { - contextualOrders.Add(new Order(OrderPrefab.Prefabs[orderIdentifier], null, orderGiver: Character.Controlled)); + contextualOrders.Add(OrderPrefab.Dismissal.CreateInstance(OrderPrefab.OrderTargetType.Entity)); } } contextualOrders.RemoveAll(o => !IsOrderAvailable(o)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Data/CampaignMetadata.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/CampaignMetadata.cs similarity index 100% rename from Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Data/CampaignMetadata.cs rename to Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/CampaignMetadata.cs diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/Wallet.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/Wallet.cs new file mode 100644 index 000000000..8c8832a05 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/Wallet.cs @@ -0,0 +1,26 @@ +namespace Barotrauma +{ + internal partial class Wallet + { + public bool IsOwnWallet => + GameMain.GameSession?.Campaign switch + { + null => false, + SinglePlayerCampaign spCampaign => this == spCampaign.Bank, + MultiPlayerCampaign mpCampaign => this == mpCampaign.PersonalWallet, + _ => false + }; + + partial void SettingsChanged(Option balanceChanged, Option rewardChanged) + { + CampaignMode campaign = GameMain.GameSession?.Campaign; + WalletChangedData data = new WalletChangedData + { + BalanceChanged = balanceChanged, + RewardDistributionChanged = rewardChanged, + }; + + campaign?.OnMoneyChanged.Invoke(new WalletChangedEvent(this, data, CreateWalletInfo())); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index 9f8368a3b..7186c3bd9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -63,6 +63,8 @@ namespace Barotrauma } } + public virtual Wallet Wallet => GetWallet(); + public override void ShowStartMessage() { foreach (Mission mission in Missions.ToList()) @@ -310,7 +312,7 @@ namespace Barotrauma if (ShowCampaignUI || ForceMapUI) { campaignUIContainer?.AddToGUIUpdateList(); - if (CampaignUI?.UpgradeStore?.HoveredItem != null) + if (CampaignUI?.UpgradeStore?.HoveredEntity != null) { if (CampaignUI.SelectedTab != InteractionType.Upgrade) { return; } CampaignUI?.UpgradeStore?.ItemInfoFrame.AddToGUIUpdateList(order: 1); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index d628ec7fc..597939e79 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -21,7 +21,7 @@ namespace Barotrauma private UInt16 pendingSaveID = 1; public UInt16 PendingSaveID { - get + get { return pendingSaveID; } @@ -34,6 +34,14 @@ namespace Barotrauma } } + public Wallet PersonalWallet => Character.Controlled?.Wallet ?? Wallet.Invalid; + public override Wallet Wallet => GetWallet(); + + public override Wallet GetWallet(Client client = null) + { + return PersonalWallet; + } + public static void StartCampaignSetup(IEnumerable saveFiles) { var parent = GameMain.NetLobbyScreen.CampaignSetupFrame; @@ -195,6 +203,11 @@ namespace Barotrauma yield return CoroutineStatus.Running; } + if (GameMain.Client == null) + { + yield return CoroutineStatus.Failure; + } + if (GameMain.Client.LateCampaignJoin) { GameMain.Client.LateCampaignJoin = false; @@ -618,7 +631,6 @@ namespace Barotrauma bool forceMapUI = msg.ReadBoolean(); - int money = msg.ReadInt32(); bool purchasedHullRepairs = msg.ReadBoolean(); bool purchasedItemRepairs = msg.ReadBoolean(); bool purchasedLostShuttles = msg.ReadBoolean(); @@ -812,12 +824,10 @@ namespace Barotrauma GameMain.NetLobbyScreen.ToggleCampaignMode(true); } - bool shouldRefresh = campaign.Money != money || - campaign.PurchasedHullRepairs != purchasedHullRepairs || + bool shouldRefresh = campaign.PurchasedHullRepairs != purchasedHullRepairs || campaign.PurchasedItemRepairs != purchasedItemRepairs || campaign.PurchasedLostShuttles != purchasedLostShuttles; - campaign.Money = money; campaign.PurchasedHullRepairs = purchasedHullRepairs; campaign.PurchasedItemRepairs = purchasedItemRepairs; campaign.PurchasedLostShuttles = purchasedLostShuttles; @@ -896,6 +906,43 @@ namespace Barotrauma } } + public void ClientReadMoney(IReadMessage inc) + { + NetWalletUpdate update = INetSerializableStruct.Read(inc); + foreach (NetWalletTransaction transaction in update.Transactions) + { + WalletInfo info = transaction.Info; + switch (transaction.CharacterID) + { + case Some { Value: var charID}: + { + Character targetCharacter = Character.CharacterList?.FirstOrDefault(c => c.ID == charID); + if (targetCharacter is null) { break; } + Wallet wallet = targetCharacter.Wallet; + + wallet.Balance = info.Balance; + wallet.RewardDistribution = info.RewardDistribution; + TryInvokeEvent(wallet, transaction.ChangedData, info); + break; + } + case None _: + { + Bank.Balance = info.Balance; + TryInvokeEvent(Bank, transaction.ChangedData, info); + break; + } + } + } + + void TryInvokeEvent(Wallet wallet, WalletChangedData data, WalletInfo info) + { + if (data.BalanceChanged.IsSome() || data.RewardDistributionChanged.IsSome()) + { + OnMoneyChanged.Invoke(new WalletChangedEvent(wallet, data, info)); + } + } + } + public override void Save(XElement element) { //do nothing, the clients get the save files from the server @@ -908,10 +955,10 @@ namespace Barotrauma string gamesessionDocPath = Path.Combine(SaveUtil.TempPath, "gamesession.xml"); XDocument doc = XMLExtensions.TryLoadXml(gamesessionDocPath); - if (doc == null) + if (doc == null) { DebugConsole.ThrowError($"Failed to load the state of a multiplayer campaign. Could not open the file \"{gamesessionDocPath}\"."); - return; + return; } Load(doc.Root.Element("MultiPlayerCampaign")); GameMain.GameSession.OwnedSubmarines = SaveUtil.LoadOwnedSubmarines(doc, out SubmarineInfo selectedSub); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index fb1181c12..7f8e0c43f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Xml.Linq; +using Barotrauma.Networking; namespace Barotrauma { @@ -108,6 +109,9 @@ namespace Barotrauma case "pets": petsElement = subElement; break; + case Wallet.LowerCaseSaveElementName: + Bank = new Wallet(subElement); + break; case "stats": LoadStats(subElement); break; @@ -121,7 +125,15 @@ namespace Barotrauma InitUI(); - Money = element.GetAttributeInt("money", 0); + int oldMoney = element.GetAttributeInt("money", 0); + if (oldMoney > 0) + { + Bank = new Wallet + { + Balance = oldMoney + }; + } + PurchasedLostShuttles = element.GetAttributeBool("purchasedlostshuttles", false); PurchasedHullRepairs = element.GetAttributeBool("purchasedhullrepairs", false); PurchasedItemRepairs = element.GetAttributeBool("purchaseditemrepairs", false); @@ -729,7 +741,6 @@ namespace Barotrauma public override void Save(XElement element) { XElement modeElement = new XElement("SinglePlayerCampaign", - new XAttribute("money", Money), new XAttribute("purchasedlostshuttles", PurchasedLostShuttles), new XAttribute("purchasedhullrepairs", PurchasedHullRepairs), new XAttribute("purchaseditemrepairs", PurchasedItemRepairs), @@ -756,13 +767,14 @@ namespace Barotrauma petsElement = new XElement("pets"); PetBehavior.SavePets(petsElement); - modeElement.Add(petsElement); + modeElement.Add(petsElement); CrewManager.Save(modeElement); CampaignMetadata.Save(modeElement); Map.Save(modeElement); CargoManager?.SavePurchasedItems(modeElement); UpgradeManager?.Save(modeElement); + modeElement.Add(Bank.Save()); element.Add(modeElement); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs index 2cdcec576..725d24de9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs @@ -24,7 +24,7 @@ namespace Barotrauma public TestGameMode(GameModePreset preset) : base(preset) { - foreach (JobPrefab jobPrefab in JobPrefab.Prefabs) + foreach (JobPrefab jobPrefab in JobPrefab.Prefabs.OrderBy(p => p.Identifier)) { for (int i = 0; i < jobPrefab.InitialCount; i++) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs index 14daa8a4a..0f794fa40 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs @@ -190,7 +190,7 @@ namespace Barotrauma } else { - tabMenu.Update(); + tabMenu.Update(deltaTime); if ((PlayerInput.KeyHit(InputType.InfoTab) || PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Escape)) && !(GUI.KeyboardDispatcher.Subscriber is GUITextBox)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs index 26f8f67b8..0811b26ce 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs @@ -181,6 +181,11 @@ namespace Barotrauma } } + private void OnMoneyChanged(WalletChangedEvent e) + { + if (e.Wallet.IsOwnWallet) { OnUpdate?.Invoke(); } + } + // if you have more than 5000 ping there are probably more important things to worry about but hey just in case private static DateTimeOffset GetTimeout() => DateTimeOffset.Now.AddSeconds(5).AddMilliseconds(GetPing()); @@ -241,6 +246,7 @@ namespace Barotrauma private void HealRequestReceived(IReadMessage inc) { NetHealRequest request = INetSerializableStruct.Read(inc); + if (request.Result == HealRequestResult.Success) { HealAllPending(force: true); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index 1a0f12157..a25c7787f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -2,6 +2,7 @@ using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.Linq; @@ -15,7 +16,6 @@ namespace Barotrauma private int jobColumnWidth, characterColumnWidth, statusColumnWidth; - private readonly SubmarineInfo sub; private readonly List selectedMissions; private readonly Location startLocation, endLocation; @@ -30,11 +30,8 @@ namespace Barotrauma public GUIComponent Frame { get; private set; } - - - public RoundSummary(SubmarineInfo sub, GameMode gameMode, IEnumerable selectedMissions, Location startLocation, Location endLocation) + public RoundSummary(GameMode gameMode, IEnumerable selectedMissions, Location startLocation, Location endLocation) { - this.sub = sub; this.gameMode = gameMode; this.selectedMissions = selectedMissions.ToList(); this.startLocation = startLocation; @@ -320,6 +317,16 @@ namespace Barotrauma { LocalizedString rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", reward)); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(displayedMission.GetMissionRewardText(Submarine.MainSub))); + if (Character.Controlled is { } controlled) + { + var (share, percentage) = Mission.GetRewardShare(controlled.Wallet.RewardDistribution, Mission.GetSalaryEligibleCrew(), Option.Some(reward)); + if (share > 0) + { + string shareFormatted = string.Format(CultureInfo.InvariantCulture, "{0:N0}", share); + RichString yourShareString = RichString.Rich(TextManager.GetWithVariables("crewwallet.missionreward.get", ("[money]", $"{shareFormatted}"), ("[share]", $"{percentage}"))); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), yourShareString); + } + } } if (displayedMission != missionsToDisplay.Last()) @@ -407,7 +414,7 @@ namespace Barotrauma CreatePathUnlockElement(locationFrame, null, startLocation); } - foreach (Faction faction in campaignMode.Factions) + foreach (Faction faction in campaignMode.Factions.OrderBy(f => f.Prefab.MenuOrder).ThenBy(f => f.Prefab.Name)) { float initialReputation = faction.Reputation.Value; if (initialFactionReputations.ContainsKey(faction)) @@ -728,7 +735,7 @@ namespace Barotrauma if (Math.Abs(reputationChange) > 0) { string changeText = $"{(reputationChange > 0 ? "+" : "") + reputationChange}"; - string colorStr = XMLExtensions.ColorToString(reputationChange > 0 ? GUIStyle.Green : GUIStyle.Red); + string colorStr = XMLExtensions.ToStringHex(reputationChange > 0 ? GUIStyle.Green : GUIStyle.Red); var richText = RichString.Rich($"{reputationText} (‖color:{colorStr}‖{changeText}‖color:end‖)"); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), sliderHolder.RectTransform), richText, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs index 5ed620e91..2b372fd27 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs @@ -283,9 +283,9 @@ namespace Barotrauma.Items.Components } } - public override void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public override void ClientEventRead(IReadMessage msg, float sendingTime) { - base.ClientRead(type, msg, sendingTime); + base.ClientEventRead(msg, sendingTime); bool open = msg.ReadBoolean(); bool broken = msg.ReadBoolean(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs index b87d69d5e..1684642d0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs @@ -54,7 +54,7 @@ namespace Barotrauma.Items.Components } } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { CurrPowerConsumption = powerConsumption; charging = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs index 33dafbe91..590e908f9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs @@ -68,7 +68,7 @@ namespace Barotrauma.Items.Components } } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { Tainted = msg.ReadBoolean(); if (Tainted) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs index 67ce2148e..d903016ad 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs @@ -156,7 +156,7 @@ namespace Barotrauma.Items.Components private readonly object mutex = new object(); - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { Health = msg.ReadRangedSingle(0, MaxHealth, 8); int startOffset = msg.ReadRangedInteger(-1, MaximumVines); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs index 47a8e6089..77da549e2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs @@ -2,6 +2,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; +using System.Diagnostics.Tracing; namespace Barotrauma.Items.Components { @@ -58,18 +59,23 @@ namespace Barotrauma.Items.Components GUI.DrawRectangle(spriteBatch, new Vector2(attachPos.X - 2, -attachPos.Y - 2), Vector2.One * 5, GUIStyle.Red, thickness: 3); } - public void ClientWrite(IWriteMessage msg, object[] extraData = null) + public override bool ValidateEventData(NetEntityEvent.IData data) + => TryExtractEventData(data, out _); + + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { if (!attachable || body == null) { return; } + + var eventData = ExtractEventData(extraData); - Vector2 attachPos = (Vector2)extraData[2]; + Vector2 attachPos = eventData.AttachPos; msg.Write(attachPos.X); msg.Write(attachPos.Y); } - public override void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public override void ClientEventRead(IReadMessage msg, float sendingTime) { - base.ClientRead(type, msg, sendingTime); + base.ClientEventRead(msg, sendingTime); bool readAttachData = msg.ReadBoolean(); if (!readAttachData) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs index b68703a72..ad2a8cdf2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs @@ -96,7 +96,7 @@ namespace Barotrauma.Items.Components ContentXElement getElementFromList(List list, int index) => CharacterInfo.IsValidIndex(index, list) ? list[index] - : characterInfo.GetRandomElement(list); + : null; var disguisedHairElement = getElementFromList(disguisedHairs, disguisedHairIndex); var disguisedBeardElement = getElementFromList(disguisedBeards, disguisedBeardIndex); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index 595945e37..b153c5b80 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -566,14 +566,14 @@ namespace Barotrauma.Items.Components protected virtual void CreateGUI() { } //Starts a coroutine that will read the correct state of the component from the NetBuffer when correctionTimer reaches zero. - protected void StartDelayedCorrection(ServerNetObject type, IReadMessage buffer, float sendingTime, bool waitForMidRoundSync = false) + protected void StartDelayedCorrection(IReadMessage buffer, float sendingTime, bool waitForMidRoundSync = false) { - if (delayedCorrectionCoroutine != null) CoroutineManager.StopCoroutines(delayedCorrectionCoroutine); + if (delayedCorrectionCoroutine != null) { CoroutineManager.StopCoroutines(delayedCorrectionCoroutine); } - delayedCorrectionCoroutine = CoroutineManager.StartCoroutine(DoDelayedCorrection(type, buffer, sendingTime, waitForMidRoundSync)); + delayedCorrectionCoroutine = CoroutineManager.StartCoroutine(DoDelayedCorrection(buffer, sendingTime, waitForMidRoundSync)); } - private IEnumerable DoDelayedCorrection(ServerNetObject type, IReadMessage buffer, float sendingTime, bool waitForMidRoundSync) + private IEnumerable DoDelayedCorrection(IReadMessage buffer, float sendingTime, bool waitForMidRoundSync) { while (GameMain.Client != null && (correctionTimer > 0.0f || (waitForMidRoundSync && GameMain.Client.MidRoundSyncing))) @@ -587,7 +587,7 @@ namespace Barotrauma.Items.Components yield return CoroutineStatus.Success; } - ((IServerSerializable)this).ClientRead(type, buffer, sendingTime); + ((IServerSerializable)this).ClientEventRead(buffer, sendingTime); correctionTimer = 0.0f; delayedCorrectionCoroutine = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs index 076f6621b..7c7821844 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs @@ -282,7 +282,7 @@ namespace Barotrauma.Items.Components textBlock.DrawManually(spriteBatch); } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { Text = msg.ReadString(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LevelResource.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LevelResource.cs index b16765b69..bcef57295 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LevelResource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LevelResource.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { partial class LevelResource : ItemComponent, IServerSerializable { - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { deattachTimer = msg.ReadSingle(); if (deattachTimer >= DeattachDuration) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index 448c56945..52cf7fef1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -56,8 +56,9 @@ namespace Barotrauma.Items.Components } else { - Light.Position = item.DrawPosition; - if (item.Submarine != null) { Light.Position -= item.Submarine.DrawPosition; } + Vector2 pos = item.DrawPosition; + if (item.Submarine != null) { pos -= item.Submarine.DrawPosition; } + Light.Position = pos; } PhysicsBody body = Light.ParentBody; if (body != null) @@ -120,7 +121,7 @@ namespace Barotrauma.Items.Components yield return CoroutineStatus.Success; } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { IsActive = msg.ReadBoolean(); lastReceivedState = IsActive; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs index 0eb2cbf58..df20deb01 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs @@ -80,7 +80,7 @@ namespace Barotrauma.Items.Components } #endif - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { State = msg.ReadBoolean(); ushort userID = msg.ReadUInt16(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs index 67cc5fe8b..85791f95b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs @@ -251,12 +251,12 @@ namespace Barotrauma.Items.Components return true; } - public void ClientWrite(IWriteMessage msg, object[] extraData = null) + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { msg.Write(pendingState); } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { ushort userID = msg.ReadUInt16(); Character user = userID == Entity.NullEntityID ? null : Entity.FindEntityByID(userID) as Character; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs index 89280fa88..e9406abde 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs @@ -156,17 +156,17 @@ namespace Barotrauma.Items.Components } } - public void ClientWrite(IWriteMessage msg, object[] extraData = null) + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { //targetForce can only be adjusted at 10% intervals -> no need for more accuracy than this msg.WriteRangedInteger((int)(targetForce / 10.0f), -10, 10); } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { if (correctionTimer > 0.0f) { - StartDelayedCorrection(type, msg.ExtractBits(5 + 16), sendingTime); + StartDelayedCorrection(msg.ExtractBits(5 + 16), sendingTime); return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index 4a3da093d..7d2a5101f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -705,13 +705,13 @@ namespace Barotrauma.Items.Components requiredTimeBlock.Text = ToolBox.SecondsToReadableTime(timeUntilReady > 0.0f ? timeUntilReady : requiredTime); } - public void ClientWrite(IWriteMessage msg, object[] extraData = null) + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { uint recipeHash = pendingFabricatedItem?.RecipeHash ?? 0; msg.Write(recipeHash); } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { FabricatorState newState = (FabricatorState)msg.ReadByte(); float newTimeUntilReady = msg.ReadSingle(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs index c65e6c41a..e50bbcc8d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs @@ -216,14 +216,14 @@ namespace Barotrauma.Items.Components } } - public void ClientWrite(IWriteMessage msg, object[] extraData = null) + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { //flowpercentage can only be adjusted at 10% intervals -> no need for more accuracy than this msg.WriteRangedInteger((int)(flowPercentage / 10.0f), -10, 10); msg.Write(IsActive); } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { int msgStartPos = msg.BitPosition; @@ -244,7 +244,7 @@ namespace Barotrauma.Items.Components { int msgLength = msg.BitPosition - msgStartPos; msg.BitPosition = msgStartPos; - StartDelayedCorrection(type, msg.ExtractBits(msgLength), sendingTime); + StartDelayedCorrection(msg.ExtractBits(msgLength), sendingTime); return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs index 6627d8491..cfe675dbb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs @@ -608,9 +608,10 @@ namespace Barotrauma.Items.Components Vector2 pointerPos = pos - new Vector2(0, 30) * scale; + float scaleMultiplier = 0.95f; if (optimalRangeNormalized.X == optimalRangeNormalized.Y) { - sectorSprite.Draw(spriteBatch, pointerPos, GUIStyle.Red, MathHelper.PiOver2, scale); + sectorSprite.Draw(spriteBatch, pointerPos, GUIStyle.Red, MathHelper.PiOver2, scale * scaleMultiplier); } else { @@ -619,7 +620,6 @@ namespace Barotrauma.Items.Components spriteBatch.GraphicsDevice.ScissorRectangle = new Rectangle(0, 0, GameMain.GraphicsWidth, (int)(pointerPos.Y + (meterSprite.size.Y - meterSprite.Origin.Y) * scale) - 3); spriteBatch.Begin(SpriteSortMode.Deferred, rasterizerState: GameMain.ScissorTestEnable); - float scaleMultiplier = 0.95f; sectorSprite.Draw(spriteBatch, pointerPos, optimalRangeColor, MathHelper.PiOver2 + (allowedSectorRad.X + allowedSectorRad.Y) / 2.0f, scale * scaleMultiplier); sectorSprite.Draw(spriteBatch, pointerPos, offRangeColor, optimalSectorRad.X, scale * scaleMultiplier); sectorSprite.Draw(spriteBatch, pointerPos, warningColor, allowedSectorRad.X, scale * scaleMultiplier); @@ -711,7 +711,7 @@ namespace Barotrauma.Items.Components tempRangeIndicator?.Remove(); } - public void ClientWrite(IWriteMessage msg, object[] extraData = null) + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { msg.Write(autoTemp); msg.Write(PowerOn); @@ -721,11 +721,11 @@ namespace Barotrauma.Items.Components correctionTimer = CorrectionDelay; } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { if (correctionTimer > 0.0f) { - StartDelayedCorrection(type, msg.ExtractBits(1 + 1 + 8 + 8 + 8 + 8), sendingTime); + StartDelayedCorrection(msg.ExtractBits(1 + 1 + 8 + 8 + 8 + 8), sendingTime); return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index 8b549c0b8..90f0e04b3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -766,7 +766,7 @@ namespace Barotrauma.Items.Components if (activePingsCount == 0) { disruptedDirections.Clear(); } foreach (AITarget t in AITarget.List) { - if (t.Entity is Character c && c.Params.HideInSonar) { continue; } + if (t.Entity is Character c && !c.IsUnconscious && c.Params.HideInSonar) { continue; } if (t.SoundRange <= 0.0f || float.IsNaN(t.SoundRange) || float.IsInfinity(t.SoundRange)) { continue; } float distSqr = Vector2.DistanceSquared(t.WorldPosition, transducerCenter); @@ -1264,7 +1264,7 @@ namespace Barotrauma.Items.Components } foreach (AITarget aiTarget in AITarget.List) { - float disruption = aiTarget.Entity is Character c ? c.Params.SonarDisruption : aiTarget.SonarDisruption; + float disruption = aiTarget.Entity is Character c && !c.IsUnconscious ? c.Params.SonarDisruption : aiTarget.SonarDisruption; if (disruption <= 0.0f || aiTarget.InDetectable) { continue; } float distSqr = Vector2.DistanceSquared(aiTarget.WorldPosition, pingSource); if (distSqr > worldPingRadiusSqr) { continue; } @@ -1413,7 +1413,7 @@ namespace Barotrauma.Items.Components foreach (Character c in Character.CharacterList) { if (c.AnimController.CurrentHull != null || !c.Enabled) { continue; } - if (c.Params.HideInSonar) { continue; } + if (!c.IsUnconscious && c.Params.HideInSonar) { continue; } if (DetectSubmarineWalls && c.AnimController.CurrentHull == null && item.CurrentHull != null) { continue; } if (c.AnimController.SimplePhysicsEnabled) @@ -1742,7 +1742,7 @@ namespace Barotrauma.Items.Components MineralClusters = null; } - public void ClientWrite(IWriteMessage msg, object[] extraData = null) + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { msg.Write(currentMode == Mode.Active); if (currentMode == Mode.Active) @@ -1758,7 +1758,7 @@ namespace Barotrauma.Items.Components } } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { int msgStartPos = msg.BitPosition; @@ -1782,7 +1782,7 @@ namespace Barotrauma.Items.Components { int msgLength = msg.BitPosition - msgStartPos; msg.BitPosition = msgStartPos; - StartDelayedCorrection(type, msg.ExtractBits(msgLength), sendingTime); + StartDelayedCorrection(msg.ExtractBits(msgLength), sendingTime); return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index 2d2786db4..2fbf7b38d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -822,7 +822,7 @@ namespace Barotrauma.Items.Components Connection dockingConnection = item.Connections?.FirstOrDefault(c => c.Name == "toggle_docking"); if (dockingConnection != null) { - connectedPorts = item.GetConnectedComponentsRecursive(dockingConnection, ignoreInactiveRelays: true); + connectedPorts = item.GetConnectedComponentsRecursive(dockingConnection, ignoreInactiveRelays: true, allowTraversingBackwards: false); } checkConnectedPortsTimer = CheckConnectedPortsInterval; } @@ -894,7 +894,7 @@ namespace Barotrauma.Items.Components pathFinder = null; } - public void ClientWrite(IWriteMessage msg, object[] extraData = null) + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { msg.Write(AutoPilot); msg.Write(dockingNetworkMessagePending); @@ -921,7 +921,7 @@ namespace Barotrauma.Items.Components } } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { int msgStartPos = msg.BitPosition; @@ -962,7 +962,7 @@ namespace Barotrauma.Items.Components { int msgLength = (int)(msg.BitPosition - msgStartPos); msg.BitPosition = msgStartPos; - StartDelayedCorrection(type, msg.ExtractBits(msgLength), sendingTime); + StartDelayedCorrection(msg.ExtractBits(msgLength), sendingTime); return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs index 2a011d6df..cd6883441 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs @@ -163,16 +163,16 @@ namespace Barotrauma.Items.Components indicatorSize * item.Scale, Color.Black, depth: item.SpriteDepth - 0.000015f); } - public void ClientWrite(IWriteMessage msg, object[] extraData) + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData) { msg.WriteRangedInteger((int)(rechargeSpeed / MaxRechargeSpeed * 10), 0, 10); } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { if (correctionTimer > 0.0f) { - StartDelayedCorrection(type, msg.ExtractBits(4 + 8), sendingTime); + StartDelayedCorrection(msg.ExtractBits(4 + 8), sendingTime); return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs index bd467a7b8..8523f56f1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs @@ -13,7 +13,7 @@ namespace Barotrauma.Items.Components { private readonly List particleEmitters = new List(); - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { bool launch = msg.ReadBoolean(); if (launch) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index 1a1497aba..87a2c21e7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -427,7 +427,7 @@ namespace Barotrauma.Items.Components item.CreateClientEvent(this); } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { deteriorationTimer = msg.ReadSingle(); deteriorateAlwaysResetTimer = msg.ReadSingle(); @@ -446,7 +446,7 @@ namespace Barotrauma.Items.Components } } - public void ClientWrite(IWriteMessage msg, object[] extraData = null) + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { msg.WriteRangedInteger((int)requestStartFixAction, 0, 2); msg.Write(qteSuccess); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs index 783864f76..2dc8ec191 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs @@ -196,7 +196,7 @@ namespace Barotrauma.Items.Components } } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { snapped = msg.ReadBoolean(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Scanner.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Scanner.cs index 865f0915d..765d00bff 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Scanner.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Scanner.cs @@ -16,7 +16,7 @@ namespace Barotrauma.Items.Components } } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { bool wasScanCompletedPreviously = IsScanCompleted; scanTimer = msg.ReadSingle(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs index 2202530c8..89d1a7583 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs @@ -76,13 +76,14 @@ namespace Barotrauma.Items.Components UserData = i, OnClicked = (button, userData) => { + int signalIndex = (int)userData; if (GameMain.IsSingleplayer) { - SendSignal((int)userData, Character.Controlled); + SendSignal(signalIndex, Character.Controlled); } else { - item.CreateClientEvent(this, new object[] { userData }); + item.CreateClientEvent(this, new EventData(signalIndex)); } return true; } @@ -104,12 +105,12 @@ namespace Barotrauma.Items.Components Container.Inventory.RectTransform = containerHolder.RectTransform; } - public void ClientWrite(IWriteMessage msg, object[] extraData = null) + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { Write(msg, extraData); } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { SendSignal(msg.ReadRangedInteger(0, Signals.Length - 1), sender: null, isServerMessage: true); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs index 057679a25..0d026f53b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs @@ -139,7 +139,7 @@ namespace Barotrauma.Items.Components } } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { if (GameMain.Client.MidRoundSyncing) { @@ -161,7 +161,7 @@ namespace Barotrauma.Items.Components } int msgLength = (int)(msg.BitPosition - msgStartPos); msg.BitPosition = (int)msgStartPos; - StartDelayedCorrection(type, msg.ExtractBits(msgLength), sendingTime, waitForMidRoundSync: true); + StartDelayedCorrection(msg.ExtractBits(msgLength), sendingTime, waitForMidRoundSync: true); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs index f22fbcce1..50a7f11d3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs @@ -137,13 +137,14 @@ namespace Barotrauma.Items.Components }; btn.OnClicked += (_, userdata) => { + CustomInterfaceElement btnElement = userdata as CustomInterfaceElement;; if (GameMain.Client == null) { - ButtonClicked(userdata as CustomInterfaceElement); + ButtonClicked(btnElement); } else { - GameMain.Client.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ComponentState, item.GetComponentIndex(this), userdata as CustomInterfaceElement }); + item.CreateClientEvent(this, new EventData(btnElement)); } return true; }; @@ -301,7 +302,7 @@ namespace Barotrauma.Items.Components } } - public void ClientWrite(IWriteMessage msg, object[] extraData = null) + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { //extradata contains an array of buttons clicked by the player (or nothing if the player didn't click anything) for (int i = 0; i < customInterfaceElementList.Count; i++) @@ -323,12 +324,12 @@ namespace Barotrauma.Items.Components } else { - msg.Write(extraData != null && extraData.Any(d => d as CustomInterfaceElement == customInterfaceElementList[i])); + msg.Write(extraData is Item.ComponentStateEventData { ComponentData: EventData eventData } && eventData.BtnElement == customInterfaceElementList[i]); } } } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { for (int i = 0; i < customInterfaceElementList.Count; i++) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/MemoryComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/MemoryComponent.cs index 68e7884e9..2b0aeec29 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/MemoryComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/MemoryComponent.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { partial class MemoryComponent : ItemComponent { - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { Value = msg.ReadString(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs index 7538b6fc4..8c7ef71ea 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs @@ -7,6 +7,16 @@ namespace Barotrauma.Items.Components { partial class Terminal : ItemComponent, IClientSerializable, IServerSerializable { + private readonly struct ClientEventData : IEventData + { + public readonly string Text; + + public ClientEventData(string text) + { + Text = text; + } + } + private GUIListBox historyBox; private GUITextBlock fillerBlock; private GUITextBox inputBox; @@ -42,7 +52,7 @@ namespace Barotrauma.Items.Components } else { - item.CreateClientEvent(this, new object[] { text }); + item.CreateClientEvent(this, new ClientEventData(text)); } textBox.Text = string.Empty; return true; @@ -133,17 +143,15 @@ namespace Barotrauma.Items.Components } } - public void ClientWrite(IWriteMessage msg, object[] extraData = null) + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { - if (extraData is null) { return; } - - if (extraData[2] is string str) + if (TryExtractEventData(extraData, out ClientEventData eventData)) { - msg.Write(str); + msg.Write(eventData.Text); } } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { SendOutput(msg.ReadString()); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/WifiComponent.cs index dd8c385eb..64793fa05 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/WifiComponent.cs @@ -21,7 +21,7 @@ namespace Barotrauma.Items.Components ShapeExtensions.DrawCircle(spriteBatch, pos, range, 32, Color.Cyan * 0.5f, 3); } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { Channel = msg.ReadRangedInteger(MinChannel, MaxChannel); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs index 4e1de585c..fcede0408 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs @@ -11,6 +11,16 @@ namespace Barotrauma.Items.Components { partial class Wire : ItemComponent, IDrawableComponent, IServerSerializable, IClientSerializable { + private readonly struct ClientEventData : IEventData + { + public readonly int NodeCount; + + public ClientEventData(int nodeCount) + { + NodeCount = nodeCount; + } + } + public static Color higlightColor = Color.LightGreen; public static Color editorHighlightColor = Color.Yellow; public static Color editorSelectedColor = Color.Red; @@ -555,7 +565,7 @@ namespace Barotrauma.Items.Components return false; } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { int eventIndex = msg.ReadRangedInteger(0, (int)Math.Ceiling(MaxNodeCount / (float)MaxNodesPerNetworkEvent)); int nodeCount = msg.ReadRangedInteger(0, MaxNodesPerNetworkEvent); @@ -586,9 +596,13 @@ namespace Barotrauma.Items.Components (item.ParentInventory is CharacterInventory characterInventory && ((characterInventory.Owner as Character)?.HasEquippedItem(item) ?? false)); } - public void ClientWrite(IWriteMessage msg, object[] extraData = null) + public override bool ValidateEventData(NetEntityEvent.IData data) + => TryExtractEventData(data, out _); + + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { - int nodeCount = (int)extraData[2]; + var eventData = ExtractEventData(extraData); + int nodeCount = eventData.NodeCount; msg.Write((byte)nodeCount); if (nodeCount > 0) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/TriggerComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/TriggerComponent.cs index 503c61912..77fe1ba5a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/TriggerComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/TriggerComponent.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { partial class TriggerComponent : ItemComponent, IServerSerializable { - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { CurrentForceFluctuation = msg.ReadRangedSingle(0.0f, 1.0f, 8); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index ded8496e3..e2df43900 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -44,7 +44,9 @@ namespace Barotrauma.Items.Components private readonly Dictionary widgets = new Dictionary(); private float prevAngle; - + + private float currentBarrelSpin = 0f; + private bool flashLowPower; private bool flashNoAmmo, flashLoaderBroken; private float flashTimer; @@ -716,7 +718,7 @@ namespace Barotrauma.Items.Components } } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { UInt16 projectileID = msg.ReadUInt16(); float newTargetRotation = msg.ReadRangedSingle(minRotation, maxRotation, 16); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs index 0c31c61a5..c8eb6b435 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs @@ -107,7 +107,7 @@ namespace Barotrauma.Items.Components } } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { bool isDocked = msg.ReadBoolean(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 19ea4b3b9..1c27aaf3c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -1598,7 +1598,7 @@ namespace Barotrauma int maxStackSize = Math.Min(containedItem.Prefab.MaxStackSize, itemContainer.GetMaxStackSize(0)); if (maxStackSize > 1 || containedItem.Prefab.HideConditionBar) { - containedState = itemContainer.Inventory.slots[0].ItemCount / (float)maxStackSize; + containedState = itemContainer.Inventory.slots[0].Items.Count / (float)maxStackSize; } } } @@ -1702,7 +1702,7 @@ namespace Barotrauma } if (maxStackSize > 1 && inventory != null) { - int itemCount = slot.MouseOn() ? inventory.slots[slotIndex].ItemCount : inventory.slots[slotIndex].Items.Where(it => !DraggingItems.Contains(it)).Count(); + int itemCount = slot.MouseOn() ? inventory.slots[slotIndex].Items.Count : inventory.slots[slotIndex].Items.Where(it => !DraggingItems.Contains(it)).Count(); if (item.IsFullCondition || MathUtils.NearlyEqual(item.Condition, 0.0f) || itemCount > 1) { Vector2 stackCountPos = new Vector2(rect.Right, rect.Bottom); @@ -1795,20 +1795,10 @@ namespace Barotrauma } } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { UInt16 lastEventID = msg.ReadUInt16(); - byte slotCount = msg.ReadByte(); - receivedItemIDs = new List[slotCount]; - for (int i = 0; i < slotCount; i++) - { - receivedItemIDs[i] = new List(); - int itemCount = msg.ReadRangedInteger(0, MaxStackSize); - for (int j = 0; j < itemCount; j++) - { - receivedItemIDs[i].Add(msg.ReadUInt16()); - } - } + SharedRead(msg, out receivedItemIDs); //delay applying the new state if less than 1 second has passed since this client last sent a state to the server //prevents the inventory from briefly reverting to an old state if items are moved around in quick succession @@ -1895,7 +1885,7 @@ namespace Barotrauma receivedItemIDs = null; } - public void ClientWrite(IWriteMessage msg, object[] extraData = null) + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { SharedWrite(msg, extraData); syncItemsDelay = 1.0f; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 011ec97e9..8cb513864 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -6,6 +6,7 @@ using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using Barotrauma.Extensions; using Barotrauma.MapCreatures.Behavior; @@ -1262,25 +1263,19 @@ namespace Barotrauma } } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { - if (type == ServerNetObject.ENTITY_POSITION) - { - ClientReadPosition(type, msg, sendingTime); - return; - } - - NetEntityEvent.Type eventType = - (NetEntityEvent.Type)msg.ReadRangedInteger(0, Enum.GetValues(typeof(NetEntityEvent.Type)).Length - 1); + EventType eventType = + (EventType)msg.ReadRangedInteger((int)EventType.MinValue, (int)EventType.MaxValue); switch (eventType) { - case NetEntityEvent.Type.ComponentState: + case EventType.ComponentState: { int componentIndex = msg.ReadRangedInteger(0, components.Count - 1); if (components[componentIndex] is IServerSerializable serverSerializable) { - serverSerializable.ClientRead(type, msg, sendingTime); + serverSerializable.ClientEventRead(msg, sendingTime); } else { @@ -1288,12 +1283,12 @@ namespace Barotrauma } } break; - case NetEntityEvent.Type.InventoryState: + case EventType.InventoryState: { int containerIndex = msg.ReadRangedInteger(0, components.Count - 1); if (components[containerIndex] is ItemContainer container) { - container.Inventory.ClientRead(type, msg, sendingTime); + container.Inventory.ClientEventRead(msg, sendingTime); } else { @@ -1301,7 +1296,7 @@ namespace Barotrauma } } break; - case NetEntityEvent.Type.Status: + case EventType.Status: float prevCondition = condition; condition = msg.ReadSingle(); if (prevCondition > 0.0f && condition <= 0.0f) @@ -1314,10 +1309,10 @@ namespace Barotrauma } SetActiveSprite(); break; - case NetEntityEvent.Type.AssignCampaignInteraction: + case EventType.AssignCampaignInteraction: CampaignInteractionType = (CampaignMode.InteractionType)msg.ReadByte(); break; - case NetEntityEvent.Type.ApplyStatusEffect: + case EventType.ApplyStatusEffect: { ActionType actionType = (ActionType)msg.ReadRangedInteger(0, Enum.GetValues(typeof(ActionType)).Length - 1); byte componentIndex = msg.ReadByte(); @@ -1347,10 +1342,10 @@ namespace Barotrauma } } break; - case NetEntityEvent.Type.ChangeProperty: + case EventType.ChangeProperty: ReadPropertyChange(msg, false); break; - case NetEntityEvent.Type.Upgrade: + case EventType.Upgrade: Identifier identifier = msg.ReadIdentifier(); byte level = msg.ReadByte(); if (UpgradePrefab.Find(identifier) is { } upgradePrefab) @@ -1371,51 +1366,65 @@ namespace Barotrauma AddUpgrade(upgrade, false); } break; - case NetEntityEvent.Type.Invalid: - break; + default: + throw new Exception($"Malformed incoming item event: unsupported event type {eventType}"); } } - public void ClientWrite(IWriteMessage msg, object[] extraData = null) + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { - if (extraData == null || extraData.Length == 0 || !(extraData[0] is NetEntityEvent.Type)) + Exception error(string reason) { - return; + string errorMsg = $"Failed to write a network event for the item \"{Name}\" - {reason}"; + GameAnalyticsManager.AddErrorEventOnce($"Item.ClientWrite:{Name}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + return new Exception(errorMsg); } + + if (extraData is null) { throw error("event data was null"); } + if (!(extraData is IEventData eventData)) { throw error($"event data was of the wrong type (\"{extraData.GetType().Name}\")"); } - NetEntityEvent.Type eventType = (NetEntityEvent.Type)extraData[0]; - msg.WriteRangedInteger((int)eventType, 0, Enum.GetValues(typeof(NetEntityEvent.Type)).Length - 1); - switch (eventType) + EventType eventType = eventData.EventType; + msg.WriteRangedInteger((int)eventType, (int)EventType.MinValue, (int)EventType.MaxValue); + switch (eventData) { - case NetEntityEvent.Type.ComponentState: - int componentIndex = (int)extraData[1]; + case ComponentStateEventData componentStateEventData: + { + var component = componentStateEventData.Component; + if (component is null) { throw error("component was null"); } + if (!(component is IClientSerializable clientSerializable)) { throw error($"component was not {nameof(IClientSerializable)}"); } + int componentIndex = components.IndexOf(component); + if (componentIndex < 0) { throw error("component did not belong to item"); } msg.WriteRangedInteger(componentIndex, 0, components.Count - 1); - (components[componentIndex] as IClientSerializable).ClientWrite(msg, extraData); - break; - case NetEntityEvent.Type.InventoryState: - int containerIndex = (int)extraData[1]; + clientSerializable.ClientEventWrite(msg, extraData); + } + break; + case InventoryStateEventData inventoryStateEventData: + { + var container = inventoryStateEventData.Component; + if (container is null) { throw error("container was null"); } + int containerIndex = components.IndexOf(container); + if (containerIndex < 0) { throw error("container did not belong to item"); } msg.WriteRangedInteger(containerIndex, 0, components.Count - 1); - (components[containerIndex] as ItemContainer).Inventory.ClientWrite(msg, extraData); - break; - case NetEntityEvent.Type.Treatment: - UInt16 characterID = (UInt16)extraData[1]; - Limb targetLimb = (Limb)extraData[2]; + container.Inventory.ClientEventWrite(msg, extraData); + } + break; + case TreatmentEventData treatmentEventData: + Character targetCharacter = treatmentEventData.TargetCharacter; - Character targetCharacter = FindEntityByID(characterID) as Character; - - msg.Write(characterID); - msg.Write(targetCharacter == null ? (byte)255 : (byte)Array.IndexOf(targetCharacter.AnimController.Limbs, targetLimb)); + msg.Write(targetCharacter.ID); + msg.Write(treatmentEventData.LimbIndex); break; - case NetEntityEvent.Type.ChangeProperty: - WritePropertyChange(msg, extraData, true); + case ChangePropertyEventData changePropertyEventData: + WritePropertyChange(msg, changePropertyEventData, inGameEditableOnly: true); editingHUDRefreshTimer = 1.0f; break; - case NetEntityEvent.Type.Combine: - UInt16 combineTargetID = (UInt16)extraData[1]; - msg.Write(combineTargetID); + case CombineEventData combineEventData: + Item combineTarget = combineEventData.CombineTarget; + msg.Write(combineTarget.ID); break; + default: + throw error($"Unsupported event type {eventData.GetType().Name}"); } - msg.WritePadBits(); } partial void UpdateNetPosition(float deltaTime) @@ -1451,7 +1460,7 @@ namespace Barotrauma rect.Y = (int)(displayPos.Y + rect.Height / 2.0f); } - public void ClientReadPosition(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientReadPosition(IReadMessage msg, float sendingTime) { if (body == null) { @@ -1465,7 +1474,7 @@ namespace Barotrauma return; } - var posInfo = body.ClientRead(type, msg, sendingTime, parentDebugName: Name); + var posInfo = body.ClientRead(msg, sendingTime, parentDebugName: Name); msg.ReadPadBits(); if (posInfo != null) { @@ -1510,24 +1519,18 @@ namespace Barotrauma } public void CreateClientEvent(T ic) where T : ItemComponent, IClientSerializable + => CreateClientEvent(ic, null); + + public void CreateClientEvent(T ic, ItemComponent.IEventData extraData) where T : ItemComponent, IClientSerializable { - if (GameMain.Client == null) return; + if (GameMain.Client == null) { return; } - int index = components.IndexOf(ic); - if (index == -1) return; + #warning TODO: this should throw an exception + if (!components.Contains(ic)) { return; } - GameMain.Client.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.ComponentState, index }); - } - - public void CreateClientEvent(T ic, object[] extraData) where T : ItemComponent, IClientSerializable - { - if (GameMain.Client == null) return; - - int index = components.IndexOf(ic); - if (index == -1) return; - - object[] data = new object[] { NetEntityEvent.Type.ComponentState, index }.Concat(extraData).ToArray(); - GameMain.Client.CreateEntityEvent(this, data); + var eventData = new ComponentStateEventData(ic, extraData); + if (!ic.ValidateEventData(eventData)) { throw new Exception($"Component event creation failed: {typeof(T).Name}.{nameof(ItemComponent.ValidateEventData)} returned false"); } + GameMain.Client.CreateEntityEvent(this, eventData); } public static Item ReadSpawnData(IReadMessage msg, bool spawn = true) @@ -1576,6 +1579,36 @@ namespace Barotrauma bool allowStealing = msg.ReadBoolean(); int quality = msg.ReadRangedInteger(0, Items.Components.Quality.MaxQuality); byte teamID = msg.ReadByte(); + + bool hasIdCard = msg.ReadBoolean(); + string ownerName = "", ownerTags = ""; + int ownerBeardIndex = -1, ownerHairIndex = -1, ownerMoustacheIndex = -1, ownerFaceAttachmentIndex = -1; + Color ownerHairColor = Microsoft.Xna.Framework.Color.White, + ownerFacialHairColor = Microsoft.Xna.Framework.Color.White, + ownerSkinColor = Microsoft.Xna.Framework.Color.White; + Identifier ownerJobId = Identifier.Empty; + Vector2 ownerSheetIndex = Vector2.Zero; + if (hasIdCard) + { + ownerName = msg.ReadString(); + ownerTags = msg.ReadString(); + + ownerBeardIndex = msg.ReadByte() - 1; + ownerHairIndex = msg.ReadByte() - 1; + ownerMoustacheIndex = msg.ReadByte() - 1; + ownerFaceAttachmentIndex = msg.ReadByte() - 1; + + ownerHairColor = msg.ReadColorR8G8B8(); + ownerFacialHairColor = msg.ReadColorR8G8B8(); + ownerSkinColor = msg.ReadColorR8G8B8(); + + ownerJobId = msg.ReadIdentifier(); + + int x = msg.ReadByte(); + int y = msg.ReadByte(); + ownerSheetIndex = (x, y); + } + bool tagsChanged = msg.ReadBoolean(); string tags = ""; if (tagsChanged) @@ -1587,6 +1620,7 @@ namespace Barotrauma tags = string.Join(',',itemPrefab.Tags.Where(t => !removedTags.Contains(t)).Concat(addedTags)); } } + bool isNameTag = msg.ReadBoolean(); string writtenName = ""; if (isNameTag) @@ -1672,6 +1706,17 @@ namespace Barotrauma foreach (IdCard idCard in item.GetComponents()) { idCard.TeamID = (CharacterTeamType)teamID; + idCard.OwnerName = ownerName; + idCard.OwnerTags = ownerTags; + idCard.OwnerBeardIndex = ownerBeardIndex; + idCard.OwnerHairIndex = ownerHairIndex; + idCard.OwnerMoustacheIndex = ownerMoustacheIndex; + idCard.OwnerFaceAttachmentIndex = ownerFaceAttachmentIndex; + idCard.OwnerHairColor = ownerHairColor; + idCard.OwnerFacialHairColor = ownerFacialHairColor; + idCard.OwnerSkinColor = ownerSkinColor; + idCard.OwnerJobId = ownerJobId; + idCard.OwnerSheetIndex = ownerSheetIndex; } if (descriptionChanged) { item.Description = itemDesc; } if (tagsChanged) { item.Tags = tags; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemEventData.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemEventData.cs new file mode 100644 index 000000000..95f314a73 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemEventData.cs @@ -0,0 +1,35 @@ +using System; + +namespace Barotrauma +{ + partial class Item + { + private readonly struct CombineEventData : IEventData + { + public EventType EventType => EventType.Combine; + public readonly Item CombineTarget; + + public CombineEventData(Item combineTarget) + { + CombineTarget = combineTarget; + } + } + + private readonly struct TreatmentEventData : IEventData + { + public EventType EventType => EventType.Treatment; + public readonly Character TargetCharacter; + public readonly Limb TargetLimb; + public byte LimbIndex + => TargetCharacter?.AnimController?.Limbs is { } limbs + ? (byte)Array.IndexOf(limbs, TargetLimb) + : byte.MaxValue; + + public TreatmentEventData(Character targetCharacter, Limb targetLimb) + { + TargetCharacter = targetCharacter; + TargetLimb = targetLimb; + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index b14c94720..665a42380 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -30,7 +30,7 @@ namespace Barotrauma private float serverUpdateDelay; private float remoteWaterVolume, remoteOxygenPercentage; - private List remoteFireSources; + private NetworkFireSource[] remoteFireSources = null; private readonly List remoteBackgroundSections = new List(); private readonly List remoteDecals = new List(); @@ -175,15 +175,16 @@ namespace Barotrauma { if (!pendingSectionUpdates.Any() && !pendingDecalUpdates.Any()) { - GameMain.NetworkMember?.CreateEntityEvent(this); + GameMain.NetworkMember?.CreateEntityEvent(this, new StatusEventData()); } foreach (Decal decal in pendingDecalUpdates) { - GameMain.NetworkMember?.CreateEntityEvent(this, new object[] { decal }); + GameMain.NetworkMember?.CreateEntityEvent(this, new DecalEventData(decal)); } + pendingDecalUpdates.Clear(); foreach (int pendingSectionUpdate in pendingSectionUpdates) { - GameMain.NetworkMember?.CreateEntityEvent(this, new object[] { pendingSectionUpdate }); + GameMain.NetworkMember?.CreateEntityEvent(this, new BackgroundSectionsEventData(pendingSectionUpdate)); } pendingSectionUpdates.Clear(); networkUpdatePending = false; @@ -595,132 +596,101 @@ namespace Barotrauma } } - public void ClientWrite(IWriteMessage msg, object[] extraData = null) + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { - if (extraData == null) - { - msg.WriteRangedInteger(0, 0, 2); - msg.WriteRangedSingle(MathHelper.Clamp(waterVolume / Volume, 0.0f, 1.5f), 0.0f, 1.5f, 8); + if (!(extraData is IEventData eventData)) { throw new Exception($"Malformed hull event: expected {nameof(Hull)}.{nameof(IEventData)}"); } - msg.Write(FireSources.Count > 0); - if (FireSources.Count > 0) - { - msg.WriteRangedInteger(Math.Min(FireSources.Count, 16), 0, 16); - for (int i = 0; i < Math.Min(FireSources.Count, 16); i++) - { - var fireSource = FireSources[i]; - Vector2 normalizedPos = new Vector2( - (fireSource.Position.X - rect.X) / rect.Width, - (fireSource.Position.Y - (rect.Y - rect.Height)) / rect.Height); - - msg.WriteRangedSingle(MathHelper.Clamp(normalizedPos.X, 0.0f, 1.0f), 0.0f, 1.0f, 8); - msg.WriteRangedSingle(MathHelper.Clamp(normalizedPos.Y, 0.0f, 1.0f), 0.0f, 1.0f, 8); - msg.WriteRangedSingle(MathHelper.Clamp(fireSource.Size.X / rect.Width, 0.0f, 1.0f), 0, 1.0f, 8); - } - } - } - else if (extraData[0] is Decal decal) + msg.WriteRangedInteger((int)eventData.EventType, (int)EventType.MinValue, (int)EventType.MaxValue); + switch (eventData) { - msg.WriteRangedInteger(1, 0, 2); - int decalIndex = decals.IndexOf(decal); - msg.Write((byte)(decalIndex < 0 ? 255 : decalIndex)); - msg.WriteRangedSingle(decal.BaseAlpha, 0.0f, 1.0f, 8); - } - else - { - msg.WriteRangedInteger(2, 0, 2); - int sectorToUpdate = (int)extraData[0]; - int start = sectorToUpdate * BackgroundSectionsPerNetworkEvent; - int end = Math.Min((sectorToUpdate + 1) * BackgroundSectionsPerNetworkEvent, BackgroundSections.Count - 1); - msg.WriteRangedInteger(sectorToUpdate, 0, BackgroundSections.Count - 1); - for (int i = start; i < end; i++) - { - msg.WriteRangedSingle(BackgroundSections[i].ColorStrength, 0.0f, 1.0f, 8); - msg.Write(BackgroundSections[i].Color.PackedValue); - } + case StatusEventData statusEventData: + SharedStatusWrite(msg); + break; + case BackgroundSectionsEventData backgroundSectionsEventData: + SharedBackgroundSectionsWrite(msg, backgroundSectionsEventData); + break; + case DecalEventData decalEventData: + var decal = decalEventData.Decal; + int decalIndex = decals.IndexOf(decal); + msg.Write((byte)(decalIndex < 0 ? 255 : decalIndex)); + msg.WriteRangedSingle(decal.BaseAlpha, 0.0f, 1.0f, 8); + break; + default: + throw new Exception($"Malformed hull event: did not expect {eventData.GetType().Name}"); } } - public void ClientRead(ServerNetObject type, IReadMessage message, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { - bool isBallastFloraUpdate = message.ReadBoolean(); - if (isBallastFloraUpdate) + EventType eventType = (EventType)msg.ReadRangedInteger((int)EventType.MinValue, (int)EventType.MaxValue); + switch (eventType) { - BallastFloraBehavior.NetworkHeader header = (BallastFloraBehavior.NetworkHeader) message.ReadByte(); - if (header == BallastFloraBehavior.NetworkHeader.Spawn) - { - Identifier identifier = message.ReadIdentifier(); - float x = message.ReadSingle(); - float y = message.ReadSingle(); - BallastFlora = new BallastFloraBehavior(this, BallastFloraPrefab.Find(identifier), new Vector2(x, y), firstGrowth: true) - { - PowerConsumptionTimer = message.ReadSingle() - }; - } - else - { - BallastFlora?.ClientRead(message, header); - } - return; - } - remoteWaterVolume = message.ReadRangedSingle(0.0f, 1.5f, 8) * Volume; - remoteOxygenPercentage = message.ReadRangedSingle(0.0f, 100.0f, 8); - - bool hasFireSources = message.ReadBoolean(); - remoteFireSources = new List(); - if (hasFireSources) - { - int fireSourceCount = message.ReadRangedInteger(0, 16); - for (int i = 0; i < fireSourceCount; i++) - { - remoteFireSources.Add(new Vector3( - MathHelper.Clamp(message.ReadRangedSingle(0.0f, 1.0f, 8), 0.05f, 0.95f), - MathHelper.Clamp(message.ReadRangedSingle(0.0f, 1.0f, 8), 0.05f, 0.95f), - message.ReadRangedSingle(0.0f, 1.0f, 8))); - } - } - - bool hasExtraData = message.ReadBoolean(); - if (hasExtraData) - { - bool hasSectionUpdate = message.ReadBoolean(); - if (hasSectionUpdate) - { - int sectorToUpdate = message.ReadRangedInteger(0, BackgroundSections.Count - 1); - int start = sectorToUpdate * BackgroundSectionsPerNetworkEvent; - int end = Math.Min((sectorToUpdate + 1) * BackgroundSectionsPerNetworkEvent, BackgroundSections.Count - 1); - for (int i = start; i < end; i++) - { - float colorStrength = message.ReadRangedSingle(0.0f, 1.0f, 8); - Color color = new Color(message.ReadUInt32()); - var remoteBackgroundSection = remoteBackgroundSections.Find(s => s.Index == i); - if (remoteBackgroundSection != null) + case EventType.Status: + remoteOxygenPercentage = msg.ReadRangedSingle(0.0f, 100.0f, 8); + + SharedStatusRead( + msg, + out float newWaterVolume, + out NetworkFireSource[] newFireSources); + + remoteWaterVolume = newWaterVolume; + remoteFireSources = newFireSources; + break; + case EventType.BackgroundSections: + SharedBackgroundSectionRead( + msg, + bsnu => { - remoteBackgroundSection.SetColorStrength(colorStrength); - remoteBackgroundSection.SetColor(color); - } - else - { - remoteBackgroundSections.Add(new BackgroundSection(new Rectangle(0, 0, 1, 1), (ushort)i, colorStrength, color, 0)); - } - } + int i = bsnu.SectionIndex; + Color color = bsnu.Color; + float colorStrength = bsnu.ColorStrength; + + var remoteBackgroundSection = remoteBackgroundSections.Find(s => s.Index == i); + if (remoteBackgroundSection != null) + { + remoteBackgroundSection.SetColorStrength(colorStrength); + remoteBackgroundSection.SetColor(color); + } + else + { + remoteBackgroundSections.Add(new BackgroundSection(new Rectangle(0, 0, 1, 1), (ushort)i, colorStrength, color, 0)); + } + }, out _); paintAmount = BackgroundSections.Sum(s => s.ColorStrength); - } - else - { - int decalCount = message.ReadRangedInteger(0, MaxDecalsPerHull); + break; + case EventType.Decal: + int decalCount = msg.ReadRangedInteger(0, MaxDecalsPerHull); if (decalCount == 0) { decals.Clear(); } remoteDecals.Clear(); for (int i = 0; i < decalCount; i++) { - UInt32 decalId = message.ReadUInt32(); - int spriteIndex = message.ReadByte(); - float normalizedXPos = message.ReadRangedSingle(0.0f, 1.0f, 8); - float normalizedYPos = message.ReadRangedSingle(0.0f, 1.0f, 8); - float decalScale = message.ReadRangedSingle(0.0f, 2.0f, 12); + UInt32 decalId = msg.ReadUInt32(); + int spriteIndex = msg.ReadByte(); + float normalizedXPos = msg.ReadRangedSingle(0.0f, 1.0f, 8); + float normalizedYPos = msg.ReadRangedSingle(0.0f, 1.0f, 8); + float decalScale = msg.ReadRangedSingle(0.0f, 2.0f, 12); remoteDecals.Add(new RemoteDecal(decalId, spriteIndex, new Vector2(normalizedXPos, normalizedYPos), decalScale)); } - } + break; + case EventType.BallastFlora: + BallastFloraBehavior.NetworkHeader header = (BallastFloraBehavior.NetworkHeader) msg.ReadByte(); + if (header == BallastFloraBehavior.NetworkHeader.Spawn) + { + Identifier identifier = msg.ReadIdentifier(); + float x = msg.ReadSingle(); + float y = msg.ReadSingle(); + BallastFlora = new BallastFloraBehavior(this, BallastFloraPrefab.Find(identifier), new Vector2(x, y), firstGrowth: true) + { + PowerConsumptionTimer = msg.ReadSingle() + }; + } + else + { + BallastFlora?.ClientRead(msg, header); + } + break; + default: + throw new Exception($"Malformed incoming hull event: {eventType} is not a supported event type"); } if (serverUpdateDelay > 0.0f) { return; } @@ -756,17 +726,15 @@ namespace Barotrauma remoteDecals.Clear(); } - if (remoteFireSources == null) { return; } - + if (remoteFireSources is null) { return; } + WaterVolume = remoteWaterVolume; OxygenPercentage = remoteOxygenPercentage; - for (int i = 0; i < remoteFireSources.Count; i++) + for (int i = 0; i < remoteFireSources.Length; i++) { - Vector2 pos = new Vector2( - rect.X + rect.Width * remoteFireSources[i].X, - rect.Y - rect.Height + (rect.Height * remoteFireSources[i].Y)); - float size = remoteFireSources[i].Z * rect.Width; + Vector2 pos = remoteFireSources[i].Position; + float size = remoteFireSources[i].Size; var newFire = i < FireSources.Count ? FireSources[i] : @@ -782,7 +750,7 @@ namespace Barotrauma } } - for (int i = FireSources.Count - 1; i >= remoteFireSources.Count; i--) + for (int i = FireSources.Count - 1; i >= remoteFireSources.Length; i--) { FireSources[i].Remove(); if (i < FireSources.Count) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs index 87c8a8a64..67a6e6fa2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs @@ -121,34 +121,42 @@ namespace Barotrauma { renderer?.DrawForeground(spriteBatch, cam, LevelObjectManager); } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { - bool isGlobalUpdate = msg.ReadBoolean(); - if (isGlobalUpdate) + EventType eventType = (EventType)msg.ReadByte(); + + switch (eventType) { - foreach (LevelWall levelWall in ExtraWalls) + case EventType.GlobalDestructibleWall: { - if (levelWall.Body.BodyType == BodyType.Static) { continue; } - - Vector2 bodyPos = new Vector2( - msg.ReadSingle(), - msg.ReadSingle()); - levelWall.MoveState = msg.ReadRangedSingle(0.0f, MathHelper.TwoPi, 16); - DestructibleLevelWall destructibleWall = levelWall as DestructibleLevelWall; - if (Vector2.DistanceSquared(bodyPos, levelWall.Body.Position) > 0.5f && (destructibleWall == null || !destructibleWall.Destroyed)) + foreach (LevelWall levelWall in ExtraWalls) { - levelWall.Body.SetTransformIgnoreContacts(ref bodyPos, levelWall.Body.Rotation); + if (levelWall.Body.BodyType == BodyType.Static) { continue; } + + Vector2 bodyPos = new Vector2( + msg.ReadSingle(), + msg.ReadSingle()); + levelWall.MoveState = msg.ReadRangedSingle(0.0f, MathHelper.TwoPi, 16); + if (Vector2.DistanceSquared(bodyPos, levelWall.Body.Position) > 0.5f + && !(levelWall is DestructibleLevelWall { Destroyed: true })) + { + levelWall.Body.SetTransformIgnoreContacts(ref bodyPos, levelWall.Body.Rotation); + } } } - } - else - { - int index = msg.ReadUInt16(); - byte damageByte = msg.ReadByte(); - if (index < ExtraWalls.Count && ExtraWalls[index] is DestructibleLevelWall destructibleWall) + break; + case EventType.SingleDestructibleWall: { - destructibleWall.SetDamage(destructibleWall.MaxHealth * damageByte / 255.0f); + int index = msg.ReadUInt16(); + float damageByte = msg.ReadByte(); + if (index < ExtraWalls.Count && ExtraWalls[index] is DestructibleLevelWall destructibleWall) + { + destructibleWall.SetDamage(destructibleWall.MaxHealth * damageByte / 255.0f); + } } + break; + default: + throw new Exception($"Malformed incoming level event: {eventType} is not a supported event type"); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs index 76bf3c231..bdf919218 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -229,7 +229,7 @@ namespace Barotrauma } } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { int objIndex = msg.ReadRangedInteger(0, objects.Count); objects[objIndex].ClientRead(msg); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs index 71df6f48a..ea9eebd9c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs @@ -106,7 +106,8 @@ namespace Barotrauma WaterEffect.Parameters["xBlurDistance"].SetValue(BlurAmount / 100.0f); } else - { WaterEffect.CurrentTechnique = WaterEffect.Techniques["WaterShader"]; + { + WaterEffect.CurrentTechnique = WaterEffect.Techniques["WaterShader"]; } Vector2 offset = WavePos; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index 420a52d66..d1764fd10 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -188,8 +188,10 @@ namespace Barotrauma.Lights if (light.ParentBody != null) { light.ParentBody.UpdateDrawPosition(); - light.Position = light.ParentBody.DrawPosition; - if (light.ParentSub != null) { light.Position -= light.ParentSub.DrawPosition; } + + Vector2 pos = light.ParentBody.DrawPosition; + if (light.ParentSub != null) { pos -= light.ParentSub.DrawPosition; } + light.Position = pos; } float range = light.LightSourceParams.TextureRange; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index 0f85e7fda..92112a32a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -70,6 +70,8 @@ namespace Barotrauma private (SubmarineInfo pendingSub, float realWorldCrushDepth) pendingSubInfo; + private RichString beaconStationActiveText, beaconStationInactiveText; + /*private (Rectangle targetArea, string tip)? connectionTooltip; private string sanitizedConnectionTooltip; private List connectionTooltipRichTextData; @@ -153,6 +155,9 @@ namespace Barotrauma DebugConsole.ThrowError($"Could not find campaign map sprites for the biome \"{missingBiome.Identifier}\". Using the sprites of the first biome instead..."); } + beaconStationActiveText = RichString.Rich(TextManager.Get("BeaconStationActiveTooltip")); + beaconStationInactiveText = RichString.Rich(TextManager.Get("BeaconStationInactiveTooltip")); + RemoveFogOfWar(StartLocation); GenerateLocationConnectionVisuals(); @@ -619,7 +624,7 @@ namespace Barotrauma if (Vector2.Distance(PlayerInput.MousePosition, typeChangeIconPos) < generationParams.TypeChangeIcon.SourceRect.Width * zoom && (tooltip == null || IsPreferredTooltip(typeChangeIconPos))) { - tooltip = (new Rectangle(typeChangeIconPos.ToPoint(), new Point(30)), location.LastTypeChangeMessage); + tooltip = (new Rectangle(typeChangeIconPos.ToPoint(), new Point(30)), RichString.Rich(location.LastTypeChangeMessage)); } } if (location != CurrentLocation && generationParams.MissionIcon != null) @@ -920,7 +925,7 @@ namespace Barotrauma if (connection.LevelData.HasBeaconStation) { var beaconStationIconStyle = connection.LevelData.IsBeaconActive ? "BeaconStationActive" : "BeaconStationInactive"; - DrawIcon(beaconStationIconStyle, (int)(28 * zoom), TextManager.Get(connection.LevelData.IsBeaconActive ? "BeaconStationActiveTooltip" : "BeaconStationInactiveTooltip")); + DrawIcon(beaconStationIconStyle, (int)(28 * zoom), connection.LevelData.IsBeaconActive ? beaconStationActiveText : beaconStationInactiveText); } if (connection.Locked) @@ -942,9 +947,9 @@ namespace Barotrauma DrawIcon( "LockedLocationConnection", (int)(28 * zoom), - TextManager.GetWithVariables(unlockEvent.UnlockPathTooltip ?? "LockedPathTooltip", + RichString.Rich(TextManager.GetWithVariables(unlockEvent.UnlockPathTooltip ?? "LockedPathTooltip", ("[requiredreputation]", Reputation.GetFormattedReputationText(MathUtils.InverseLerp(unlockReputation.MinReputation, unlockReputation.MaxReputation, unlockEvent.UnlockPathReputation), unlockEvent.UnlockPathReputation, addColorTags: true)), - ("[currentreputation]", unlockReputation.GetFormattedReputationText(addColorTags: true)))); + ("[currentreputation]", unlockReputation.GetFormattedReputationText(addColorTags: true))))); } else { @@ -955,7 +960,7 @@ namespace Barotrauma if (connection.LevelData.HasHuntingGrounds) { - DrawIcon("HuntingGrounds", (int)(28 * zoom), TextManager.Get("HuntingGroundsTooltip")); + DrawIcon("HuntingGrounds", (int)(28 * zoom), RichString.Rich(TextManager.Get("HuntingGroundsTooltip"))); } if (crushDepthWarningIconStyle != null) @@ -976,7 +981,7 @@ namespace Barotrauma } } - void DrawIcon(string iconStyle, int iconSize, LocalizedString tooltipText) + void DrawIcon(string iconStyle, int iconSize, RichString tooltipText) { Vector2 iconPos = (connectionStart.Value + connectionEnd.Value) / 2; Vector2 iconDiff = Vector2.Normalize(connectionEnd.Value - connectionStart.Value) * iconSize; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index 339d904b8..24f85d6bd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -526,22 +526,17 @@ namespace Barotrauma return true; } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { byte sectionCount = msg.ReadByte(); bool invalidMessage = false; - if (type != ServerNetObject.ENTITY_EVENT && type != ServerNetObject.ENTITY_EVENT_INITIAL) - { - DebugConsole.NewMessage($"Error while reading a network event for the structure \"{Name} ({ID})\". Invalid event type ({type}).", Color.Red); - return; - } - else if (sectionCount != Sections.Length) + if (sectionCount != Sections.Length) { invalidMessage = true; string errorMsg = $"Error while reading a network event for the structure \"{Name} ({ID})\". Section count does not match (server: {sectionCount} client: {Sections.Length})"; - DebugConsole.NewMessage(errorMsg, Color.Red); GameAnalyticsManager.AddErrorEventOnce("Structure.ClientRead:SectionCountMismatch", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + throw new Exception(errorMsg); } for (int i = 0; i < sectionCount; i++) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index 90fb358ed..b6968b44f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -15,7 +15,7 @@ using Barotrauma.Items.Components; namespace Barotrauma { - partial class Submarine : Entity, IServerSerializable + partial class Submarine : Entity, IServerPositionSync { public static Vector2 MouseToWorldGrid(Camera cam, Submarine sub) { @@ -547,19 +547,50 @@ namespace Barotrauma } } - int disabledItemLightCount = 0; - foreach (Item item in Item.ItemList) + float entityCountWarningThreshold = 0.75f; + + if (Item.ItemList.Count > SubEditorScreen.MaxItems * entityCountWarningThreshold) { - if (item.ParentInventory == null) { continue; } - disabledItemLightCount += item.GetComponents().Count(); + if (!IsWarningSuppressed(SubEditorScreen.WarningType.ItemCount)) + { + errorMsgs.Add(TextManager.Get("subeditor.itemcountwarning").Value); + warnings.Add(SubEditorScreen.WarningType.ItemCount); + } } - int count = GameMain.LightManager.Lights.Count(l => l.CastShadows) - disabledItemLightCount; - if (count > 45) + + if ((MapEntity.mapEntityList.Count - Item.ItemList.Count - Hull.HullList.Count - WayPoint.WayPointList.Count - Gap.GapList.Count) > SubEditorScreen.MaxStructures * entityCountWarningThreshold) { - if (!IsWarningSuppressed(SubEditorScreen.WarningType.TooManyLights)) + if (!IsWarningSuppressed(SubEditorScreen.WarningType.StructureCount)) + { + errorMsgs.Add(TextManager.Get("subeditor.structurecountwarning").Value); + warnings.Add(SubEditorScreen.WarningType.StructureCount); + } + } + + if (Structure.WallList.Count > SubEditorScreen.MaxStructures * entityCountWarningThreshold) + { + if (!IsWarningSuppressed(SubEditorScreen.WarningType.WallCount)) + { + errorMsgs.Add(TextManager.Get("subeditor.wallcountwarning").Value); + warnings.Add(SubEditorScreen.WarningType.WallCount); + } + } + + if (GetLightCount() > SubEditorScreen.MaxLights * entityCountWarningThreshold) + { + if (!IsWarningSuppressed(SubEditorScreen.WarningType.LightCount)) + { + errorMsgs.Add(TextManager.Get("subeditor.lightcountwarning").Value); + warnings.Add(SubEditorScreen.WarningType.LightCount); + } + } + + if (GetShadowCastingLightCount() > SubEditorScreen.MaxShadowCastingLights * entityCountWarningThreshold) + { + if (!IsWarningSuppressed(SubEditorScreen.WarningType.ShadowCastingLightCount)) { errorMsgs.Add(TextManager.Get("subeditor.shadowcastinglightswarning").Value); - warnings.Add(SubEditorScreen.WarningType.TooManyLights); + warnings.Add(SubEditorScreen.WarningType.ShadowCastingLightCount); } } @@ -627,14 +658,31 @@ namespace Barotrauma } } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public static int GetLightCount() { - if (type != ServerNetObject.ENTITY_POSITION) + int disabledItemLightCount = 0; + foreach (Item item in Item.ItemList) { - DebugConsole.NewMessage($"Error while reading a network event for the submarine \"{Info.Name} ({ID})\". Invalid event type ({type}).", Color.Red); + if (item.ParentInventory == null) { continue; } + disabledItemLightCount += item.GetComponents().Count(); } + return GameMain.LightManager.Lights.Count() - disabledItemLightCount; + } - var posInfo = PhysicsBody.ClientRead(type, msg, sendingTime, parentDebugName: Info.Name); + public static int GetShadowCastingLightCount() + { + int disabledItemLightCount = 0; + foreach (Item item in Item.ItemList) + { + if (item.ParentInventory == null) { continue; } + disabledItemLightCount += item.GetComponents().Count(); + } + return GameMain.LightManager.Lights.Count(l => l.CastShadows) - disabledItemLightCount; + } + + public void ClientReadPosition(IReadMessage msg, float sendingTime) + { + var posInfo = PhysicsBody.ClientRead(msg, sendingTime, parentDebugName: Info.Name); msg.ReadPadBits(); if (posInfo != null) @@ -648,5 +696,10 @@ namespace Barotrauma subBody.PositionBuffer.Insert(index, posInfo); } } + + public void ClientEventRead(IReadMessage msg, float sendingTime) + { + throw new Exception($"Error while reading a network event for the submarine \"{Info.Name} ({ID})\". Submarines are not even supposed to receive events!"); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs index 3a61415c6..9e0472592 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs @@ -115,7 +115,7 @@ namespace Barotrauma.Networking } else { - orderMessageInfo.TargetCharacter?.SetOrder(order); + orderMessageInfo.TargetCharacter?.SetOrder(order, orderMessageInfo.IsNewOrder); } } } @@ -125,7 +125,8 @@ namespace Barotrauma.Networking Order order = null; if (orderMessageInfo.TargetPosition != null) { - order = new Order(orderPrefab, orderOption, orderMessageInfo.Priority, Order.OrderType.Current, null, orderMessageInfo.TargetPosition, orderGiver: senderCharacter); + order = new Order(orderPrefab, orderOption, orderMessageInfo.TargetPosition, orderGiver: senderCharacter) + .WithManualPriority(orderMessageInfo.Priority); } else if (orderMessageInfo.WallSectionIndex != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs index 7770929fc..f235624b7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.IO.Pipes; +using System.Linq; namespace Barotrauma.Networking { @@ -12,6 +13,9 @@ namespace Barotrauma.Networking public static void Start(ProcessStartInfo processInfo) { + CrashString = null; + CrashReportFilePath = null; + writePipe = new AnonymousPipeServerStream(PipeDirection.Out, System.IO.HandleInheritability.Inheritable); readPipe = new AnonymousPipeServerStream(PipeDirection.In, System.IO.HandleInheritability.Inheritable); @@ -42,8 +46,8 @@ namespace Barotrauma.Networking public static void ClosePipes() { - writePipe?.Close(); - readPipe?.Close(); + writePipe?.Dispose(); writePipe = null; + readPipe?.Dispose(); readPipe = null; shutDown = true; } @@ -54,5 +58,20 @@ namespace Barotrauma.Networking PrivateShutDown(); } + + public static string CrashString { get; private set; } + public static string CrashReportFilePath { get; private set; } + + public static LocalizedString CrashMessage + => string.IsNullOrEmpty(CrashReportFilePath) + ? TextManager.Get("ServerProcessClosed") + : TextManager.GetWithVariable("ServerProcessCrashed", "[reportfilepath]", CrashReportFilePath); + + static partial void HandleCrashString(string str) + { + DebugConsole.ThrowError($"The server has crashed: {str}"); + CrashReportFilePath = str.Split("||").FirstOrDefault() ?? "servercrashreport.log"; + CrashString = str; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs index 5fb9aac87..972c3f083 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs @@ -5,7 +5,7 @@ namespace Barotrauma { partial class EntitySpawner : Entity, IServerSerializable { - public void ClientRead(ServerNetObject type, IReadMessage message, float sendingTime) + public void ClientEventRead(IReadMessage message, float sendingTime) { bool remove = message.ReadBoolean(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 23dd76fe6..4083f49cb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -666,7 +666,7 @@ namespace Barotrauma.Networking if (ChildServerRelay.Process?.HasExited ?? true) { Disconnect(); - var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), TextManager.Get("ServerProcessClosed")); + var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage); msgBox.Buttons[0].OnClicked += ReturnToPreviousMenu; } } @@ -937,6 +937,9 @@ namespace Barotrauma.Networking case ServerPacketHeader.MEDICAL: campaign?.MedicalClinic?.ClientRead(inc); break; + case ServerPacketHeader.MONEY: + campaign?.ClientReadMoney(inc); + break; case ServerPacketHeader.READY_CHECK: ReadyCheck.ClientRead(inc); break; @@ -1203,7 +1206,7 @@ namespace Barotrauma.Networking if (disconnectReason == DisconnectReason.ServerCrashed && IsServerOwner) { - msg = TextManager.Get("ServerProcessCrashed"); + msg = TextManager.GetWithVariable("ServerProcessCrashed", "[reportfilepath]", ChildServerRelay.CrashReportFilePath); } } @@ -2240,10 +2243,11 @@ namespace Barotrauma.Networking } } + readonly List debugEntityList = new List(); private void ReadIngameUpdate(IReadMessage inc) { - List entities = new List(); - + debugEntityList.Clear(); + float sendingTime = inc.ReadSingle() - 0.0f;//TODO: reimplement inc.SenderConnection.RemoteTimeOffset; ServerNetObject? prevObjHeader = null; @@ -2284,15 +2288,15 @@ namespace Barotrauma.Networking uint msgLength = inc.ReadVariableUInt32(); int msgEndPos = (int)(inc.BitPosition + msgLength * 8); - var entity = Entity.FindEntityByID(id) as IServerSerializable; + var entity = Entity.FindEntityByID(id) as IServerPositionSync; if (msgEndPos > inc.LengthBits) { DebugConsole.ThrowError($"Error while reading a position update for the entity \"({entity?.ToString() ?? "null"})\". Message length exceeds the size of the buffer."); return; } - entities.Add(entity); - if (entity != null && (entity is Item || entity is Character || entity is Submarine)) + debugEntityList.Add(entity); + if (entity != null) { if (entity is Item != isItem) { @@ -2307,7 +2311,7 @@ namespace Barotrauma.Networking } else { - entity.ClientRead(objHeader.Value, inc, sendingTime); + entity.ClientReadPosition(inc, sendingTime); } } @@ -2321,7 +2325,7 @@ namespace Barotrauma.Networking break; case ServerNetObject.ENTITY_EVENT: case ServerNetObject.ENTITY_EVENT_INITIAL: - if (!entityEventManager.Read(objHeader.Value, inc, sendingTime, entities)) + if (!entityEventManager.Read(objHeader.Value, inc, sendingTime, debugEntityList)) { return; } @@ -2340,7 +2344,6 @@ namespace Barotrauma.Networking prevBytePos = inc.BytePosition; } } - catch (Exception ex) { List errorLines = new List @@ -2359,7 +2362,7 @@ namespace Barotrauma.Networking objHeader == ServerNetObject.ENTITY_EVENT || objHeader == ServerNetObject.ENTITY_EVENT_INITIAL || objHeader == ServerNetObject.ENTITY_POSITION || prevObjHeader == ServerNetObject.ENTITY_POSITION) { - foreach (IServerSerializable ent in entities) + foreach (IServerSerializable ent in debugEntityList) { if (ent == null) { @@ -2480,7 +2483,7 @@ namespace Barotrauma.Networking outmsg.Write(GameMain.NetLobbyScreen.CampaignCharacterDiscarded); } - Character.Controlled?.ClientWrite(outmsg); + Character.Controlled?.ClientWriteInput(outmsg); GameMain.GameScreen.Cam?.ClientWrite(outmsg); entityEventManager.Write(outmsg, clientPeer?.ServerConnection); @@ -2711,7 +2714,7 @@ namespace Barotrauma.Networking } } - public override void CreateEntityEvent(INetSerializable entity, object[] extraData) + public override void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData = null) { if (!(entity is IClientSerializable clientSerializable)) { @@ -2770,7 +2773,7 @@ namespace Barotrauma.Networking if (ChildServerRelay.Process != null) { int checks = 0; - while (ChildServerRelay.Process != null && !ChildServerRelay.Process.HasExited) + while (ChildServerRelay.Process is { HasExited: false }) { if (checks > 10) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs index 0d67a7e1a..50486ace0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Microsoft.Xna.Framework; namespace Barotrauma.Networking { @@ -41,16 +42,16 @@ namespace Barotrauma.Networking thisClient = client; } - public void CreateEvent(IClientSerializable entity, object[] extraData = null) + public void CreateEvent(IClientSerializable entity, NetEntityEvent.IData extraData = null) { if (GameMain.Client?.Character == null) { return; } if (!ValidateEntity(entity)) { return; } - var newEvent = new ClientEntityEvent(entity, (UInt16)(ID + 1)) - { - CharacterStateID = GameMain.Client.Character.LastNetworkUpdateID - }; + var newEvent = new ClientEntityEvent( + entity, + eventId: (UInt16)(ID + 1), + characterStateId: GameMain.Client.Character.LastNetworkUpdateID); if (extraData != null) { newEvent.SetData(extraData); } for (int i = events.Count - 1; i >= 0; i--) @@ -144,6 +145,7 @@ namespace Barotrauma.Networking entities.Clear(); + msg.ReadPadBits(); UInt16 firstEventID = msg.ReadUInt16(); int eventCount = msg.ReadByte(); @@ -172,7 +174,6 @@ namespace Barotrauma.Networking DebugConsole.NewMessage("received msg " + thisEventID + " (null entity)", Microsoft.Xna.Framework.Color.Orange); } - msg.ReadPadBits(); entities.Add(null); if (thisEventID == (UInt16)(lastReceivedID + 1)) { lastReceivedID++; } continue; @@ -207,7 +208,6 @@ namespace Barotrauma.Networking } msg.BitPosition += msgLength * 8; - msg.ReadPadBits(); } else { @@ -239,22 +239,22 @@ namespace Barotrauma.Networking //msg.BitPosition = (int)(msgPosition + msgLength * 8); } } - catch (Exception e) { - string errorMsg = "Failed to read event for entity \"" + entity.ToString() + "\" (" + e.Message + ")! (MidRoundSyncing: " + thisClient.MidRoundSyncing + ")\n" + e.StackTrace.CleanupStackTrace(); + string errorMsg = $"Failed to read event {thisEventID} for entity \"{entity}\"" + + $"{(entity is Entity { ID: var entityId } ? $", id {entityId}" : "")} "; + DebugConsole.ThrowError(errorMsg, e); + + errorMsg += $"({e.Message})! (MidRoundSyncing: {thisClient.MidRoundSyncing})\n{e.StackTrace.CleanupStackTrace()}"; errorMsg += "\nPrevious entities:"; for (int j = entities.Count - 2; j >= 0; j--) { errorMsg += "\n" + (entities[j] == null ? "NULL" : entities[j].ToString()); } - DebugConsole.ThrowError("Failed to read event for entity \"" + entity.ToString() + "\"!", e); - GameAnalyticsManager.AddErrorEventOnce("ClientEntityEventManager.Read:ReadFailed" + entity.ToString(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg); msg.BitPosition = (int)(msgPosition + msgLength * 8); - msg.ReadPadBits(); } } } @@ -272,7 +272,7 @@ namespace Barotrauma.Networking protected void ReadEvent(IReadMessage buffer, IServerSerializable entity, float sendingTime) { - entity.ClientRead(ServerNetObject.ENTITY_EVENT, buffer, sendingTime); + entity.ClientEventRead(buffer, sendingTime); } public void Clear() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/NetEntityEvent.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/NetEntityEvent.cs index bcecc22dd..0bca588f9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/NetEntityEvent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/NetEntityEvent.cs @@ -4,20 +4,21 @@ namespace Barotrauma.Networking { class ClientEntityEvent : NetEntityEvent { - private IClientSerializable serializable; + private readonly IClientSerializable serializable; - public UInt16 CharacterStateID; + public readonly UInt16 CharacterStateID; - public ClientEntityEvent(IClientSerializable entity, UInt16 id) - : base(entity, id) + public ClientEntityEvent(IClientSerializable entity, UInt16 eventId, UInt16 characterStateId) + : base(entity, eventId) { serializable = entity; + CharacterStateID = characterStateId; } public void Write(IWriteMessage msg) { msg.Write(CharacterStateID); - serializable.ClientWrite(msg, Data); + serializable.ClientEventWrite(msg, Data); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs index 38b49f398..47201cef6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs @@ -84,7 +84,7 @@ namespace Barotrauma.Networking if (ownerKey != 0 && (ChildServerRelay.Process?.HasExited ?? true)) { Close(); - var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), TextManager.Get("ServerProcessClosed")); + var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage); msgBox.Buttons[0].OnClicked += (btn, obj) => { GameMain.MainMenuScreen.Select(); return false; }; return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs index d96fb7c5f..580f16b8a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs @@ -193,7 +193,7 @@ namespace Barotrauma.Networking if (ChildServerRelay.HasShutDown || (ChildServerRelay.Process?.HasExited ?? true)) { Close(); - var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), TextManager.Get("ServerProcessClosed")); + var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage); msgBox.Buttons[0].OnClicked += (btn, obj) => { GameMain.MainMenuScreen.Select(); return false; }; return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs index 4057e960c..092f871d4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs @@ -71,7 +71,7 @@ namespace Barotrauma.Networking }, delay: delay); } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { bool respawnPromptPending = false; var newState = (State)msg.ReadRangedInteger(0, Enum.GetNames(typeof(State)).Length); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs index f14b1e743..a0ac4f61a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs @@ -153,7 +153,7 @@ namespace Barotrauma } } - public PosInfo ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime, string parentDebugName) + public PosInfo ClientRead(IReadMessage msg, float sendingTime, string parentDebugName) { float MaxVel = NetConfig.MaxPhysicsBodyVelocity; float MaxAngularVel = NetConfig.MaxPhysicsBodyAngularVelocity; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs index 9e15a34c1..9c62c6816 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs @@ -278,6 +278,17 @@ namespace Barotrauma characterInfos.Add((new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: jobPrefab, variant: variant), jobPrefab)); } } + if (characterInfos.Count == 0) + { + DebugConsole.ThrowError($"No starting crew found! If you're using mods, it may be that the mods have overridden the vanilla jobs without specifying which types of characters the starting crew should consist of. If you're the developer of the mod, ensure that you've set the {nameof(JobPrefab.InitialCount)} properties for the custom jobs."); + DebugConsole.AddWarning("Choosing the first available jobs as the starting crew..."); + foreach (JobPrefab jobPrefab in JobPrefab.Prefabs) + { + var variant = Rand.Range(0, jobPrefab.Variants); + characterInfos.Add((new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: jobPrefab, variant: variant), jobPrefab)); + if (characterInfos.Count >= 3) { break; } + } + } characterInfos.Sort((a, b) => Math.Sign(b.Job.MinKarma - a.Job.MinKarma)); characterInfoColumns.ClearChildren(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index 7de7bbc32..e8b404bb2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -143,14 +143,13 @@ namespace Barotrauma { if (Campaign.PurchasedHullRepairs) { - Campaign.Money += CampaignMode.HullRepairCost; + Campaign.Wallet.Refund(CampaignMode.HullRepairCost); Campaign.PurchasedHullRepairs = false; } else { - if (Campaign.Money >= CampaignMode.HullRepairCost) + if (Campaign.Wallet.TryDeduct(CampaignMode.HullRepairCost)) { - Campaign.Money -= CampaignMode.HullRepairCost; GameAnalyticsManager.AddMoneySpentEvent(CampaignMode.HullRepairCost, GameAnalyticsManager.MoneySink.Service, "hullrepairs"); Campaign.PurchasedHullRepairs = true; } @@ -189,14 +188,13 @@ namespace Barotrauma { if (Campaign.PurchasedItemRepairs) { - Campaign.Money += CampaignMode.ItemRepairCost; + Campaign.Wallet.Refund(CampaignMode.ItemRepairCost); Campaign.PurchasedItemRepairs = false; } else { - if (Campaign.Money >= CampaignMode.ItemRepairCost) + if (Campaign.Wallet.TryDeduct(CampaignMode.ItemRepairCost)) { - Campaign.Money -= CampaignMode.ItemRepairCost; GameAnalyticsManager.AddMoneySpentEvent(CampaignMode.ItemRepairCost, GameAnalyticsManager.MoneySink.Service, "devicerepairs"); Campaign.PurchasedItemRepairs = true; } @@ -242,14 +240,13 @@ namespace Barotrauma if (Campaign.PurchasedLostShuttles) { - Campaign.Money += CampaignMode.ShuttleReplaceCost; + Campaign.Wallet.Refund(CampaignMode.ShuttleReplaceCost); Campaign.PurchasedLostShuttles = false; } else { - if (Campaign.Money >= CampaignMode.ShuttleReplaceCost) + if (Campaign.Wallet.TryDeduct(CampaignMode.ShuttleReplaceCost)) { - Campaign.Money -= CampaignMode.ShuttleReplaceCost; GameAnalyticsManager.AddMoneySpentEvent(CampaignMode.ShuttleReplaceCost, GameAnalyticsManager.MoneySink.Service, "retrieveshuttle"); Campaign.PurchasedLostShuttles = true; } @@ -445,7 +442,7 @@ namespace Barotrauma { Color = MapGenerationParams.Instance.IndicatorColor, HoverColor = Color.Lerp(MapGenerationParams.Instance.IndicatorColor, Color.White, 0.5f), - ToolTip = TextManager.Get(connection.LevelData.IsBeaconActive ? "BeaconStationActiveTooltip" : "BeaconStationInactiveTooltip") + ToolTip = RichString.Rich(TextManager.Get(connection.LevelData.IsBeaconActive ? "BeaconStationActiveTooltip" : "BeaconStationInactiveTooltip")) }; new GUITextBlock(new RectTransform(Vector2.One, beaconStationContent.RectTransform), TextManager.Get("submarinetype.beaconstation", "beaconstationsonarlabel"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft) @@ -462,7 +459,7 @@ namespace Barotrauma { Color = MapGenerationParams.Instance.IndicatorColor, HoverColor = Color.Lerp(MapGenerationParams.Instance.IndicatorColor, Color.White, 0.5f), - ToolTip = TextManager.Get("HuntingGroundsTooltip") + ToolTip = RichString.Rich(TextManager.Get("HuntingGroundsTooltip")) }; new GUITextBlock(new RectTransform(Vector2.One, huntingGroundsContent.RectTransform), TextManager.Get("missionname.huntinggrounds"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft) @@ -705,11 +702,11 @@ namespace Barotrauma { case CampaignMode.InteractionType.Repair: repairHullsButton.Enabled = - (Campaign.PurchasedHullRepairs || Campaign.Money >= CampaignMode.HullRepairCost) && + (Campaign.PurchasedHullRepairs || Campaign.Wallet.CanAfford(CampaignMode.HullRepairCost)) && Campaign.AllowedToManageCampaign(); repairHullsButton.GetChild().Selected = Campaign.PurchasedHullRepairs; repairItemsButton.Enabled = - (Campaign.PurchasedItemRepairs || Campaign.Money >= CampaignMode.ItemRepairCost) && + (Campaign.PurchasedItemRepairs || Campaign.Wallet.CanAfford(CampaignMode.ItemRepairCost)) && Campaign.AllowedToManageCampaign(); repairItemsButton.GetChild().Selected = Campaign.PurchasedItemRepairs; @@ -721,7 +718,7 @@ namespace Barotrauma else { replaceShuttlesButton.Enabled = - (Campaign.PurchasedLostShuttles || Campaign.Money >= CampaignMode.ShuttleReplaceCost) && + (Campaign.PurchasedLostShuttles || Campaign.Wallet.CanAfford(CampaignMode.ShuttleReplaceCost)) && Campaign.AllowedToManageCampaign(); replaceShuttlesButton.GetChild().Selected = Campaign.PurchasedLostShuttles; } @@ -742,7 +739,7 @@ namespace Barotrauma public static LocalizedString GetMoney() { - return TextManager.GetWithVariable("PlayerCredits", "[credits]", (GameMain.GameSession?.Campaign == null) ? "0" : string.Format(CultureInfo.InvariantCulture, "{0:N0}", GameMain.GameSession.Campaign.Money)); + return TextManager.GetWithVariable("PlayerCredits", "[credits]", (GameMain.GameSession?.Campaign == null) ? "0" : string.Format(CultureInfo.InvariantCulture, "{0:N0}", GameMain.GameSession.Campaign.Wallet.Balance)); } private void UpdateMaxMissions(Location location) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index 1556924c7..3f382e8f7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -1668,11 +1668,7 @@ namespace Barotrauma.CharacterEditor if (contentPackage == null) { -#if DEBUG - contentPackage = ContentPackageManager.EnabledPackages.All.LastOrDefault(); -#else contentPackage = ContentPackageManager.EnabledPackages.All.LastOrDefault(cp => cp != vanilla); -#endif } if (contentPackage == null) { @@ -1680,13 +1676,11 @@ namespace Barotrauma.CharacterEditor DebugConsole.ThrowError(GetCharacterEditorTranslation("NoContentPackageSelected")); return false; } -#if !DEBUG if (vanilla != null && contentPackage == vanilla) { GUI.AddMessage(GetCharacterEditorTranslation("CannotEditVanillaCharacters"), GUIStyle.Red, font: GUIStyle.LargeFont); return false; } -#endif // Content package if (contentPackage is RegularPackage regular && !ContentPackageManager.EnabledPackages.Regular.Contains(regular)) { @@ -1721,9 +1715,9 @@ namespace Barotrauma.CharacterEditor } else { - config.SetAttributeValue("speciesname", name); - config.SetAttributeValue("humanoid", isHumanoid); - var ragdollElement = config.Element("ragdolls"); + config.SetAttributeValue("speciesname", name, StringComparison.OrdinalIgnoreCase); + config.SetAttributeValue("humanoid", isHumanoid, StringComparison.OrdinalIgnoreCase); + var ragdollElement = config.GetChildElement("ragdolls"); if (ragdollElement == null) { config.Add(new XElement("ragdolls", CreateRagdollPath())); @@ -1736,7 +1730,7 @@ namespace Barotrauma.CharacterEditor ragdollElement.ReplaceWith(new XElement("ragdolls", CreateRagdollPath())); } } - var animationElement = config.Element("animations"); + var animationElement = config.GetChildElement("animations"); if (animationElement == null) { config.Add(new XElement("animations", CreateAnimationPath())); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs index 48336c09b..8fb996949 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs @@ -160,8 +160,7 @@ namespace Barotrauma.CharacterEditor bool isTextureSelected = false; void UpdatePaths() { - string pathBase = ContentPackage == GameMain.VanillaContent ? $"Content/Characters/{Name}/{Name}" - : $"{ContentPath.ModDirStr}/Characters/{Name}/{Name}"; + string pathBase = $"{ContentPath.ModDirStr}/Characters/{Name}/{Name}"; XMLPath = $"{pathBase}.xml"; xmlPathElement.Text = XMLPath; if (updateTexturePath) @@ -307,10 +306,10 @@ namespace Barotrauma.CharacterEditor contentPackageDropDown = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.5f), rightContainer.RectTransform, Anchor.TopRight)); foreach (ContentPackage contentPackage in ContentPackageManager.EnabledPackages.All) { -#if !DEBUG - if (contentPackage == GameMain.VanillaContent) { continue; } -#endif - contentPackageDropDown.AddItem(contentPackage.Name, userData: contentPackage, toolTip: contentPackage.Path); + if (contentPackage != GameMain.VanillaContent) + { + contentPackageDropDown.AddItem(contentPackage.Name, userData: contentPackage, toolTip: contentPackage.Path); + } } contentPackageDropDown.OnSelected = (obj, userdata) => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index e035855de..8abc2da45 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -1329,6 +1329,11 @@ namespace Barotrauma } SeedBox.Enabled = !CampaignFrame.Visible && !CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); levelDifficultyScrollBar.Enabled = !CampaignFrame.Visible && !CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + levelDifficultyScrollBar.ToolTip = string.Empty; + if (!levelDifficultyScrollBar.Enabled) + { + levelDifficultyScrollBar.ToolTip = TextManager.Get("campaigndifficultydisabled"); + } traitorProbabilityButtons[0].Enabled = traitorProbabilityButtons[1].Enabled = traitorProbabilityText.Enabled = !CampaignFrame.Visible && !CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs index 95f6f37c6..f2d230caf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs @@ -32,7 +32,10 @@ namespace Barotrauma private GUIScrollBar zoomBar; private readonly List selectedSprites = new List(); private readonly List dirtySprites = new List(); - private Sprite selectedTexture; + private Texture2D SelectedTexture => lastSprite?.Texture; + private Sprite lastSprite; + private string selectedTexturePath; + private Rectangle textureRect; private float zoom = 1; private const float MinZoom = 0.25f, MaxZoom = 10.0f; @@ -82,12 +85,11 @@ namespace Barotrauma { OnClicked = (button, userData) => { - if (!(textureList.SelectedData is Texture2D selectedTexture)) { return false; } var selected = selectedSprites; Sprite firstSelected = selected.First(); selected.ForEach(s => s.ReloadTexture()); RefreshLists(); - textureList.Select(firstSelected.Texture, autoScroll: false); + textureList.Select(firstSelected.FullPath, autoScroll: false); selected.ForEachMod(s => spriteList.Select(s, autoScroll: false)); texturePathText.Text = TextManager.GetWithVariable("spriteeditor.texturesreloaded", "[filepath]", firstSelected.FilePath.Value); texturePathText.TextColor = GUIStyle.Green; @@ -101,10 +103,10 @@ namespace Barotrauma { OnClicked = (button, userData) => { - if (selectedTexture == null) { return false; } + if (SelectedTexture == null) { return false; } foreach (Sprite sprite in loadedSprites) { - if (sprite.FullPath != selectedTexture.FullPath) { continue; } + if (sprite.FullPath != selectedTexturePath) { continue; } var element = sprite.SourceElement; if (element == null) { continue; } // Not all sprites have a sourcerect defined, in which case we'll want to use the current source rect instead of an empty rect. @@ -206,23 +208,20 @@ namespace Barotrauma { OnSelected = (listBox, userData) => { - var previousSprite = selectedTexture; - selectedTexture = userData as Sprite; - if (previousSprite != selectedTexture) + var newTexturePath = userData as string; + if (selectedTexturePath == null || selectedTexturePath != newTexturePath) { + selectedTexturePath = newTexturePath; ResetZoom(); + spriteList.Select(loadedSprites.First(s => s.FilePath == selectedTexturePath), autoScroll: false); + UpdateScrollBar(spriteList); } foreach (GUIComponent child in spriteList.Content.Children) { var textBlock = (GUITextBlock)child; var sprite = (Sprite)textBlock.UserData; - textBlock.TextColor = new Color(textBlock.TextColor, sprite.FilePath == selectedTexture.FilePath ? 1.0f : 0.4f); - if (sprite.FilePath == selectedTexture.FilePath) { textBlock.Visible = true; } - } - if (selectedSprites.None(s => s.FilePath == selectedTexture.FilePath)) - { - spriteList.Select(loadedSprites.First(s => s.FilePath == selectedTexture.FilePath), autoScroll: false); - UpdateScrollBar(spriteList); + textBlock.TextColor = new Color(textBlock.TextColor, sprite.FilePath == selectedTexturePath ? 1.0f : 0.4f); + if (sprite.FilePath == selectedTexturePath) { textBlock.Visible = true; } } texturePathText.TextColor = Color.LightGray; topPanelContents.Visible = true; @@ -251,9 +250,12 @@ namespace Barotrauma { OnSelected = (listBox, userData) => { - if (!(userData is Sprite sprite)) return false; - SelectSprite(sprite); - return true; + if (userData is Sprite sprite) + { + SelectSprite(sprite); + return true; + } + return false; } }; @@ -410,12 +412,12 @@ namespace Barotrauma private bool SaveSprites(IEnumerable sprites) { - if (selectedTexture == null) { return false; } + if (SelectedTexture == null) { return false; } if (sprites.None()) { return false; } HashSet docsToSave = new HashSet(); foreach (Sprite sprite in sprites) { - if (sprite.FullPath != selectedTexture.FullPath) { continue; } + if (sprite.FullPath != selectedTexturePath) { continue; } var element = sprite.SourceElement; if (element == null) { continue; } element.SetAttributeValue("sourcerect", XMLExtensions.RectToString(sprite.SourceRect)); @@ -469,11 +471,11 @@ namespace Barotrauma // Select rects with the mouse if (Widget.selectedWidgets.None() || Widget.EnableMultiSelect) { - if (selectedTexture != null && GUI.MouseOn == null) + if (SelectedTexture != null && GUI.MouseOn == null) { foreach (Sprite sprite in loadedSprites) { - if (sprite.FullPath != selectedTexture.FullPath) { continue; } + if (sprite.FullPath != selectedTexturePath) { continue; } if (PlayerInput.PrimaryMouseButtonClicked()) { var scaledRect = new Rectangle(textureRect.Location + sprite.SourceRect.Location.Multiply(zoom), sprite.SourceRect.Size.Multiply(zoom)); @@ -637,20 +639,20 @@ namespace Barotrauma var viewArea = GetViewArea; - if (selectedTexture != null) + if (SelectedTexture != null) { textureRect = new Rectangle( - (int)(viewArea.Center.X - selectedTexture.Texture.Bounds.Width / 2f * zoom), - (int)(viewArea.Center.Y - selectedTexture.Texture.Bounds.Height / 2f * zoom), - (int)(selectedTexture.Texture.Bounds.Width * zoom), - (int)(selectedTexture.Texture.Bounds.Height * zoom)); + (int)(viewArea.Center.X - SelectedTexture.Bounds.Width / 2f * zoom), + (int)(viewArea.Center.Y - SelectedTexture.Bounds.Height / 2f * zoom), + (int)(SelectedTexture.Bounds.Width * zoom), + (int)(SelectedTexture.Bounds.Height * zoom)); - spriteBatch.Draw(selectedTexture.Texture, + spriteBatch.Draw(SelectedTexture, viewArea.Center.ToVector2(), sourceRectangle: null, color: Color.White, rotation: 0.0f, - origin: new Vector2(selectedTexture.Texture.Bounds.Width / 2.0f, selectedTexture.Texture.Bounds.Height / 2.0f), + origin: new Vector2(SelectedTexture.Bounds.Width / 2.0f, SelectedTexture.Bounds.Height / 2.0f), scale: zoom, effects: SpriteEffects.None, layerDepth: 0); @@ -666,7 +668,7 @@ namespace Barotrauma foreach (GUIComponent element in spriteList.Content.Children) { if (!(element.UserData is Sprite sprite)) { continue; } - if (sprite.FullPath != selectedTexture.FullPath) { continue; } + if (sprite.FullPath != selectedTexturePath) { continue; } Rectangle sourceRect = new Rectangle( textureRect.X + (int)(sprite.SourceRect.X * zoom), @@ -874,13 +876,13 @@ namespace Barotrauma public void SelectSprite(Sprite sprite) { + lastSprite = sprite; if (!loadedSprites.Contains(sprite)) { loadedSprites.Add(sprite); RefreshLists(); } - - if (selectedSprites.Any(s => s.FullPath != selectedTexture.FullPath)) + if (selectedSprites.Any(s => s.FullPath != selectedTexturePath)) { ResetWidgets(); } @@ -902,9 +904,9 @@ namespace Barotrauma selectedSprites.Add(sprite); dirtySprites.Add(sprite); } - if (selectedTexture?.FullPath != sprite.FullPath) + if (sprite.FullPath != selectedTexturePath) { - textureList.Select(sprite.Texture, autoScroll: false); + textureList.Select(sprite.FullPath, autoScroll: false); UpdateScrollBar(textureList); } xmlPathText.Text = string.Empty; @@ -926,7 +928,6 @@ namespace Barotrauma public void RefreshLists() { - //selectedTexture = null; selectedSprites.Clear(); textureList.ClearChildren(); spriteList.ClearChildren(); @@ -936,7 +937,7 @@ namespace Barotrauma foreach (Sprite sprite in loadedSprites.OrderBy(s => Path.GetFileNameWithoutExtension(s.FilePath.Value))) { //ignore sprites that don't have a file path (e.g. submarine pics) - if (sprite.FilePath.IsNullOrEmpty()) continue; + if (sprite.FilePath.IsNullOrEmpty()) { continue; } string normalizedFilePath = sprite.FilePath.FullPath; if (!textures.Contains(normalizedFilePath)) { @@ -944,7 +945,7 @@ namespace Barotrauma Path.GetFileName(sprite.FilePath.Value)) { ToolTip = sprite.FilePath.Value, - UserData = sprite + UserData = sprite.FullPath }; textures.Add(normalizedFilePath); } @@ -965,10 +966,10 @@ namespace Barotrauma public void ResetZoom() { - if (selectedTexture == null) { return; } + if (SelectedTexture == null) { return; } var viewArea = GetViewArea; - float width = viewArea.Width / (float)selectedTexture.Texture.Width; - float height = viewArea.Height / (float)selectedTexture.Texture.Height; + float width = viewArea.Width / (float)SelectedTexture.Width; + float height = viewArea.Height / (float)SelectedTexture.Height; zoom = Math.Min(1, Math.Min(width, height)); zoomBar.BarScroll = GetBarScrollValue(); viewAreaOffset = Point.Zero; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 09ab544ff..14d7493a5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -5,12 +5,10 @@ using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Xml.Linq; using Microsoft.Xna.Framework.Input; -using System.Threading.Tasks; #if DEBUG using System.IO; #else @@ -21,6 +19,12 @@ namespace Barotrauma { class SubEditorScreen : EditorScreen { + public const int MaxStructures = 2000; + public const int MaxWalls = 500; + public const int MaxItems = 5000; + public const int MaxLights = 300; + public const int MaxShadowCastingLights = 60; + private static Submarine MainSub { get => Submarine.MainSub; @@ -83,7 +87,11 @@ namespace Barotrauma NoCargoSpawnpoints, NoBallastTag, NonLinkedGaps, - TooManyLights + StructureCount, + WallCount, + ItemCount, + LightCount, + ShadowCastingLightCount } public static Vector2 MouseDragStart = Vector2.Zero; @@ -102,7 +110,7 @@ namespace Barotrauma private bool wasSelectedBefore; public GUIComponent TopPanel; - private GUIComponent showEntitiesPanel, entityCountPanel; + public GUIComponent showEntitiesPanel, entityCountPanel; private readonly List showEntitiesTickBoxes = new List(); private readonly Dictionary hiddenSubCategories = new Dictionary(); @@ -809,7 +817,7 @@ namespace Barotrauma var itemCount = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), itemCountText.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight); itemCount.TextGetter = () => { - itemCount.TextColor = ToolBox.GradientLerp(Item.ItemList.Count / 5000.0f, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); + itemCount.TextColor = Item.ItemList.Count > MaxItems ? GUIStyle.Red : Color.Lerp(GUIStyle.Green, GUIStyle.Orange, Item.ItemList.Count / (float)MaxItems); return Item.ItemList.Count.ToString(); }; @@ -818,8 +826,8 @@ namespace Barotrauma var structureCount = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), structureCountText.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight); structureCount.TextGetter = () => { - int count = (MapEntity.mapEntityList.Count - Item.ItemList.Count - Hull.HullList.Count - WayPoint.WayPointList.Count - Gap.GapList.Count); - structureCount.TextColor = ToolBox.GradientLerp(count / 1000.0f, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); + int count = MapEntity.mapEntityList.Count - Item.ItemList.Count - Hull.HullList.Count - WayPoint.WayPointList.Count - Gap.GapList.Count; + structureCount.TextColor = count > MaxStructures ? GUIStyle.Red : Color.Lerp(GUIStyle.Green, GUIStyle.Orange, count / (float)MaxStructures); return count.ToString(); }; @@ -828,7 +836,7 @@ namespace Barotrauma var wallCount = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), wallCountText.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight); wallCount.TextGetter = () => { - wallCount.TextColor = ToolBox.GradientLerp(Structure.WallList.Count / 500.0f, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); + wallCount.TextColor = Structure.WallList.Count > MaxWalls ? GUIStyle.Red : Color.Lerp(GUIStyle.Green, GUIStyle.Orange, Structure.WallList.Count / (float)MaxWalls); return Structure.WallList.Count.ToString(); }; @@ -843,7 +851,7 @@ namespace Barotrauma if (item.ParentInventory != null) { continue; } lightCount += item.GetComponents().Count(); } - lightCountText.TextColor = ToolBox.GradientLerp(lightCount / 250.0f, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); + lightCountText.TextColor = lightCount > MaxLights ? GUIStyle.Red : Color.Lerp(GUIStyle.Green, GUIStyle.Orange, lightCount / (float)MaxLights); return lightCount.ToString(); }; var shadowCastingLightCountLabel = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("SubEditorShadowCastingLights"), @@ -857,7 +865,7 @@ namespace Barotrauma if (item.ParentInventory != null) { continue; } lightCount += item.GetComponents().Count(l => l.CastShadows); } - shadowCastingLightCountText.TextColor = ToolBox.GradientLerp(lightCount / 60.0f, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); + shadowCastingLightCountText.TextColor = lightCount > MaxShadowCastingLights ? GUIStyle.Red : Color.Lerp(GUIStyle.Green, GUIStyle.Orange, lightCount / (float)MaxShadowCastingLights); return lightCount.ToString(); }; entityCountPanel.RectTransform.NonScaledSize = @@ -1448,7 +1456,7 @@ namespace Barotrauma case ".jpeg": if (saveFrame == null) { break; } - Texture2D texture = Sprite.LoadTexture(filePath); + Texture2D texture = Sprite.LoadTexture(filePath, compress: false); previewImage.Sprite = new Sprite(texture, null, null); if (MainSub != null) { @@ -1548,7 +1556,7 @@ namespace Barotrauma { foreach (GUIColorPicker colorPicker in msgBox.GetAllChildren()) { - colorPicker.DisposeTextures(); + colorPicker.Dispose(); } msgBox.Close(); @@ -1827,6 +1835,21 @@ namespace Barotrauma modProject.Save(packagePath); } + if (!GameMain.DebugDraw) + { + if (Submarine.GetLightCount() > MaxLights) + { + new GUIMessageBox(TextManager.Get("error"), TextManager.GetWithVariable("subeditor.lightcounterror", "[max]", MaxShadowCastingLights.ToString())); + return false; + } + + if (Submarine.GetShadowCastingLightCount() > MaxShadowCastingLights) + { + new GUIMessageBox(TextManager.Get("error"), TextManager.GetWithVariable("subeditor.shadowcastinglightcounterror", "[max]", MaxShadowCastingLights.ToString())); + return false; + } + } + if (string.IsNullOrWhiteSpace(name)) { GUI.AddMessage(TextManager.Get("SubNameMissingWarning"), GUIStyle.Red); @@ -3634,7 +3657,7 @@ namespace Barotrauma closeButton.OnClicked = (button, o) => { - colorPicker.DisposeTextures(); + colorPicker.Dispose(); msgBox.Close(); Color newColor = SetColor(null); @@ -3678,7 +3701,7 @@ namespace Barotrauma cancelButton.OnClicked = (button, o) => { - colorPicker.DisposeTextures(); + colorPicker.Dispose(); msgBox.Close(); foreach (var (e, color, prop) in entities) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs index c6d3deeb0..3d95b1bbb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs @@ -21,16 +21,16 @@ namespace Barotrauma private Item? miniMapItem; private Submarine? submarine; - private Character? dummyCharacter; - public static Effect BlueprintEffect = null!; - private GUIFrame container = null!; + public static Character? dummyCharacter; + public static Effect? BlueprintEffect; + private GUIFrame? container; private TabMenu? tabMenu; public TestScreen() { Cam = new Camera(); - BlueprintEffect = GameMain.GameScreen.BlueprintEffect!; + BlueprintEffect = GameMain.GameScreen.BlueprintEffect; new GUIButton(new RectTransform(new Point(256, 256), Frame.RectTransform), "Reload shader") { @@ -38,7 +38,7 @@ namespace Barotrauma { BlueprintEffect.Dispose(); GameMain.Instance.Content.Unload(); - BlueprintEffect = GameMain.Instance.Content.Load("Effects/blueprintshader_opengl")!; + BlueprintEffect = GameMain.Instance.Content.Load("Effects/blueprintshader_opengl"); GameMain.GameScreen.BlueprintEffect = BlueprintEffect; return true; } @@ -47,21 +47,19 @@ namespace Barotrauma } public override void Select() - { + { base.Select(); container = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: "InnerGlow", color: Color.Black); var tab = new GUIFrame(new RectTransform(Vector2.One, container.RectTransform), color: Color.Black * 0.9f); - MedicalClinicUI clinic = new MedicalClinicUI(new MedicalClinic(null!), tab); - clinic.RequestLatestPending(); if (dummyCharacter is { Removed: false }) { dummyCharacter?.Remove(); } - // dummyCharacter = Character.Create(CharacterPrefab.HumanSpeciesName, Vector2.Zero, "", id: Entity.DummyID, hasAi: false); - // dummyCharacter.Info.Job = new Job(JobPrefab.Prefabs.Where(jp => TalentTree.JobTalentTrees.ContainsKey(jp.Identifier)).GetRandom()); - // dummyCharacter.Info.Name = "Galldren"; - // dummyCharacter.Inventory.CreateSlots(); + dummyCharacter = Character.Create(CharacterPrefab.HumanSpeciesName, Vector2.Zero, "", id: Entity.DummyID, hasAi: false); + dummyCharacter.Info.Job = new Job(JobPrefab.Prefabs.Where(jp => TalentTree.JobTalentTrees.ContainsKey(jp.Identifier)).GetRandom(Rand.RandSync.Unsynced)); + dummyCharacter.Info.Name = "Galldren"; + dummyCharacter.Inventory.CreateSlots(); Character.Controlled = dummyCharacter; GameMain.World.ProcessChanges(); @@ -71,7 +69,8 @@ namespace Barotrauma public override void AddToGUIUpdateList() { Frame.AddToGUIUpdateList(); - container.AddToGUIUpdateList(); + container?.AddToGUIUpdateList(); + tabMenu?.AddToGUIUpdateList(); // CharacterHUD.AddToGUIUpdateList(dummyCharacter); // dummyCharacter?.SelectedConstruction?.AddToGUIUpdateList(); } @@ -79,15 +78,13 @@ namespace Barotrauma public override void Update(double deltaTime) { base.Update(deltaTime); - tabMenu!.Update(); if (dummyCharacter is { } dummy) { dummy.ControlLocalPlayer((float)deltaTime, Cam, false); dummy.Control((float)deltaTime, Cam); } - - GUI.Update((float)deltaTime); + tabMenu?.Update((float)deltaTime); } public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index 0b2da87b7..f8186ff57 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -342,9 +342,9 @@ namespace Barotrauma if (property.PropertyType == typeof(string) && value == null) { value = ""; - } + } - Identifier propertyTag = $"{entity.GetType().Name}.{property.PropertyInfo.Name}".ToIdentifier(); + Identifier propertyTag = $"{property.PropertyInfo.DeclaringType.Name}.{property.PropertyInfo.Name}".ToIdentifier(); Identifier fallbackTag = property.PropertyInfo.Name.ToIdentifier(); LocalizedString displayName = TextManager.Get(propertyTag, $"sp.{propertyTag}.name".ToIdentifier()); @@ -365,7 +365,7 @@ namespace Barotrauma { displayName = property.Name.FormatCamelCaseWithSpaces(); #if DEBUG - Editable editable = property.GetAttribute(); + InGameEditable editable = property.GetAttribute(); if (editable != null) { if (!MissingLocalizations.Contains($"sp.{propertyTag}.name|{displayName}")) @@ -378,7 +378,11 @@ namespace Barotrauma #endif } - LocalizedString toolTip = TextManager.Get($"sp.{propertyTag}.description", $"sp.{fallbackTag}.description"); + LocalizedString toolTip = TextManager.Get($"sp.{propertyTag}.description"); + if (toolTip.IsNullOrEmpty()) + { + toolTip = TextManager.Get($"{propertyTag}.description", $"sp.{fallbackTag}.description"); + } if (toolTip == null) { @@ -1312,12 +1316,9 @@ namespace Barotrauma entity = e.Item; } - if (GameMain.Client != null) + if (GameMain.Client != null && entity is Item item) { - if (entity is IClientSerializable clientSerializable) - { - GameMain.Client.CreateEntityEvent(clientSerializable, new object[] { NetEntityEvent.Type.ChangeProperty, property }); - } + GameMain.Client.CreateEntityEvent(item, new Item.ChangePropertyEventData(property)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/SpriteDeformation.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/SpriteDeformation.cs index d15eb2729..8abba88a0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/SpriteDeformation.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/SpriteDeformation.cs @@ -21,8 +21,8 @@ namespace Barotrauma.SpriteDeformations private set; } - [Serialize("", IsPropertySaveable.Yes)] - public string TypeName + [Serialize("", IsPropertySaveable.No)] + public string Type { get; set; @@ -35,7 +35,7 @@ namespace Barotrauma.SpriteDeformations set; } - public string Name => $"Deformation ({TypeName})"; + public string Name => $"Deformation ({Type})"; [Serialize(1.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2, ValueStep = 0.01f)] public float Strength { get; private set; } @@ -85,11 +85,11 @@ namespace Barotrauma.SpriteDeformations public SpriteDeformationParams(XElement element) { - if (element != null) - { - TypeName = element.GetAttributeString("type", "").ToLowerInvariant(); - } SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + if (element != null && string.IsNullOrEmpty(Type)) + { + Type = element.GetAttributeString("typename", string.Empty); + } } } @@ -120,7 +120,7 @@ namespace Barotrauma.SpriteDeformations set { SetResolution(value); } } - public string TypeName => Params.TypeName; + public string TypeName => Params.Type; public int Sync => Params.Sync; @@ -177,7 +177,7 @@ namespace Barotrauma.SpriteDeformations if (newDeformation != null) { - newDeformation.Params.TypeName = typeName; + newDeformation.Params.Type = typeName; } return newDeformation; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/ItemList.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/ItemList.cs index 9c3cfb54b..c94e33092 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/ItemList.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/ItemList.cs @@ -522,7 +522,7 @@ namespace Barotrauma.Steam UserData = tag }; tagBtn.RectTransform.NonScaledSize - = tagBtn.Font.MeasureString(tagBtn.Text).ToPoint() + new Point(GUI.IntScale(5)); + = tagBtn.Font.MeasureString(tagBtn.Text).ToPoint() + new Point(GUI.IntScale(15), GUI.IntScale(5)); tagBtn.RectTransform.IsFixedSize = true; tagBtn.ClampMouseRectToParent = false; } @@ -625,7 +625,7 @@ namespace Barotrauma.Steam #region Stats box var statsHorizontalLayout = new GUILayoutGroup(new RectTransform(Vector2.One, statsBox.RectTransform), isHorizontal: true); var statsVertical0 - = new GUILayoutGroup(new RectTransform((1.0f, 1.0f), statsHorizontalLayout.RectTransform)); + = new GUILayoutGroup(new RectTransform((1.0f, 1.0f), statsHorizontalLayout.RectTransform), childAnchor: Anchor.TopCenter); statFrame("", ""); //padding @@ -680,7 +680,7 @@ namespace Barotrauma.Steam var tagsLabel = new GUITextBlock(new RectTransform((1.0f, 0.12f), statsVertical0.RectTransform), TextManager.Get("WorkshopItemTags"), font: GUIStyle.SubHeadingFont); - CreateTagsList(workshopItem.Tags.ToIdentifiers(), new RectTransform((1.0f, 0.3f), statsVertical0.RectTransform), canBeFocused: false); + CreateTagsList(workshopItem.Tags.ToIdentifiers(), new RectTransform((0.97f, 0.3f), statsVertical0.RectTransform), canBeFocused: false); #endregion var descriptionListBox = new GUIListBox(new RectTransform((1.0f, 0.38f), verticalLayout.RectTransform)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs index b67cab661..d7a3f7b70 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs @@ -71,6 +71,7 @@ namespace Barotrauma.Steam if (GameMain.Client == null) { LeaveLobby(); + return; } if (lobbyState == LobbyState.NotConnected) @@ -83,7 +84,7 @@ namespace Barotrauma.Steam return; } - var contentPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerIncompatibleContent); + var contentPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent); currentLobby?.SetData("name", serverSettings.ServerName); currentLobby?.SetData("playercount", (GameMain.Client?.ConnectedClients?.Count ?? 0).ToString()); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/PublishTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/PublishTab.cs index e34fabd34..45fa95f50 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/PublishTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/PublishTab.cs @@ -357,21 +357,31 @@ namespace Barotrauma.Steam private IEnumerable MessageBoxCoroutine(Func> subcoroutine) { - var messageBox = new GUIMessageBox("", "", relativeSize: (0.4f, 0.4f), buttons: new [] { TextManager.Get("Cancel") }); + var messageBox = new GUIMessageBox("", "...", buttons: new [] { TextManager.Get("Cancel") }); messageBox.Buttons[0].OnClicked = (button, o) => { messageBox.Close(); return false; }; - var currentStepText = new GUITextBlock(new RectTransform((1.0f, 0.8f), messageBox.InnerFrame.RectTransform), - "...", font: GUIStyle.Font) - { - CanBeFocused = false - }; - - foreach (var status in subcoroutine(currentStepText, messageBox)) + var coroutineEval = subcoroutine(messageBox.Text, messageBox); + while (true) { + bool moveNext = true; + try + { + moveNext = coroutineEval.GetEnumerator().MoveNext(); + } + catch (Exception e) + { + DebugConsole.ThrowError($"{e.Message} {e.StackTrace.CleanupStackTrace()}"); + messageBox.Close(); + } + if (!moveNext) + { + messageBox.Close(); + } + var status = coroutineEval.GetEnumerator().Current; if (messageBox.Closed) { yield return CoroutineStatus.Success; @@ -410,7 +420,7 @@ namespace Barotrauma.Steam { SteamManager.Workshop.ForceRedownload(workshopItem); } - currentStepText.Text = $"Downloading {Percentage(workshopItem.DownloadAmount)}"; + currentStepText.Text = TextManager.GetWithVariable("PublishPopupDownload", "[percentage]", Percentage(workshopItem.DownloadAmount)); yield return new WaitForSeconds(0.5f); } } @@ -426,7 +436,7 @@ namespace Barotrauma.Steam }); while (!ContentPackageManager.WorkshopPackages.Any(p => p.SteamWorkshopId == workshopItem.Id)) { - currentStepText.Text = $"Installing"; + currentStepText.Text = TextManager.Get("PublishPopupInstall"); yield return new WaitForSeconds(0.5f); } @@ -444,7 +454,7 @@ namespace Barotrauma.Steam }); while (!localCopyMade) { - currentStepText.Text = $"Creating local copy"; + currentStepText.Text = TextManager.Get("PublishPopupCreateLocal"); yield return new WaitForSeconds(0.5f); } @@ -457,47 +467,62 @@ namespace Barotrauma.Steam GUITextBlock currentStepText, GUIMessageBox messageBox, string modVersion, Steamworks.Ugc.Editor editor, ContentPackage localPackage) { + if (!SteamManager.IsInitialized) + { + yield return CoroutineStatus.Failure; + } + bool stagingReady = false; + Exception? stagingException = null; TaskPool.Add("CreatePublishStagingCopy", SteamManager.Workshop.CreatePublishStagingCopy(modVersion, localPackage), (t) => { - Exception? exception = t.Exception?.InnerException ?? t.Exception; - if (exception != null) - { - throw new Exception($"Failed to create staging copy: {exception.Message} {exception.StackTrace}"); - } stagingReady = true; + stagingException = t.Exception?.GetInnermost(); }); - currentStepText.Text = "Copying item to staging folder..."; + currentStepText.Text = TextManager.Get("PublishPopupStaging"); while (!stagingReady) { yield return new WaitForSeconds(0.5f); } + if (stagingException != null) + { + throw new Exception($"Failed to create staging copy: {stagingException.Message} {stagingException.StackTrace.CleanupStackTrace()}"); + } + editor = editor .WithContent(SteamManager.Workshop.PublishStagingDir) .ForAppId(SteamManager.AppID); messageBox.Buttons[0].Enabled = false; Steamworks.Ugc.PublishResult? result = null; + Exception? resultException = null; TaskPool.Add($"Publishing {localPackage.Name} ({localPackage.SteamWorkshopId})", editor.SubmitAsync(), - (t) => + t => { - result = ((Task)t).Result; + t.TryGetResult(out result); + resultException = t.Exception?.GetInnermost(); }); - currentStepText.Text = "Submitting item to the Workshop..."; - while (!result.HasValue) { yield return new WaitForSeconds(0.5f); } + currentStepText.Text = TextManager.Get("PublishPopupSubmit"); + while (!result.HasValue && resultException is null) { yield return new WaitForSeconds(0.5f); } - if (result.Value.Success) + if (result is { Success: true }) { var resultId = result.Value.FileId; Steamworks.Ugc.Item resultItem = new Steamworks.Ugc.Item(resultId); - SteamManager.Workshop.ForceRedownload(resultItem); - while (!resultItem.IsInstalled) + Task downloadTask = SteamManager.Workshop.ForceRedownload(resultItem); + while (!resultItem.IsInstalled && !downloadTask.IsCompleted) { - currentStepText.Text = $"Downloading {Percentage(resultItem.DownloadAmount)}"; + currentStepText.Text = TextManager.GetWithVariable("PublishPopupDownload", "[percentage]", Percentage(resultItem.DownloadAmount)); yield return new WaitForSeconds(0.5f); } + if (!resultItem.IsInstalled) + { + throw new Exception($"Failed to install item: download task ended with status {downloadTask.Status}, " + + $"exception was {downloadTask.Exception?.GetInnermost()?.ToString().CleanupStackTrace() ?? "[NULL]"}"); + } + bool installed = false; TaskPool.Add( "InstallNewlyPublished", @@ -508,7 +533,7 @@ namespace Barotrauma.Steam }); while (!installed) { - currentStepText.Text = $"Installing"; + currentStepText.Text = TextManager.Get("PublishPopupInstall"); yield return new WaitForSeconds(0.5f); } @@ -524,8 +549,19 @@ namespace Barotrauma.Steam { SteamManager.OverlayCustomURL(resultItem.Url); } + new GUIMessageBox(string.Empty, TextManager.GetWithVariable("workshopitempublished", "[itemname]", localPackage.Name)); } + else if (resultException != null) + { + throw new Exception($"Failed to publish item: {resultException.Message} {resultException.StackTrace.CleanupStackTrace()}"); + } + else + { + new GUIMessageBox(TextManager.Get("error"), TextManager.GetWithVariable("workshopitempublishfailed", "[itemname]", localPackage.Name)); + } + SteamManager.Workshop.DeletePublishStagingCopy(); + messageBox.Close(); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/UiUtil.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/UiUtil.cs index 75372311c..9293df567 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/UiUtil.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/UiUtil.cs @@ -89,7 +89,7 @@ namespace Barotrauma.Steam } private static int Round(float v) => (int)MathF.Round(v); - private static string Percentage(float v) => $"{Round(v * 100)}%"; + private static string Percentage(float v) => $"{Round(v * 100)}"; private struct ActionCarrier { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs index 25d4144aa..fe499a4d9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs @@ -1,14 +1,13 @@ #nullable enable +using Barotrauma.IO; using Microsoft.Xna.Framework.Graphics; using RestSharp; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; -using Barotrauma.IO; namespace Barotrauma.Steam { @@ -180,8 +179,10 @@ namespace Barotrauma.Steam await CopyDirectory(contentPackage.Dir, contentPackage.Name, Path.GetDirectoryName(contentPackage.Path)!, PublishStagingDir); //Load filelist.xml and write the hash into it so anyone downloading this mod knows what it should be - ModProject modProject = new ModProject(contentPackage); - modProject.ModVersion = modVersion; + ModProject modProject = new ModProject(contentPackage) + { + ModVersion = modVersion + }; modProject.Save(Path.Combine(PublishStagingDir, ContentPackage.FileListFileName)); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu.cs index c921a71aa..feab2338c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu.cs @@ -1,16 +1,11 @@ #nullable enable -using System; using Barotrauma.Extensions; using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Text.RegularExpressions; -using System.Threading.Tasks; using System.Threading; -using System.Xml.Linq; -using Barotrauma.IO; -using Microsoft.Xna.Framework.Graphics; using ItemOrPackage = Barotrauma.Either; namespace Barotrauma.Steam @@ -25,12 +20,12 @@ namespace Barotrauma.Steam Publish } - private GUILayoutGroup tabber; - private Dictionary tabContents; + private readonly GUILayoutGroup tabber; + private readonly Dictionary tabContents; - private GUIFrame contentFrame; + private readonly GUIFrame contentFrame; - private CorePackage enabledCorePackage => enabledCoreDropdown.SelectedData as CorePackage ?? throw new Exception("Valid core package not selected"); + private CorePackage EnabledCorePackage => enabledCoreDropdown.SelectedData as CorePackage ?? throw new Exception("Valid core package not selected"); private readonly GUIDropDown enabledCoreDropdown; private readonly GUIListBox enabledRegularModsList; @@ -173,7 +168,7 @@ namespace Barotrauma.Steam to.DraggedElement = draggedElement; - to.BarScroll = to.BarScroll * (oldCount / newCount); + to.BarScroll *= (oldCount / newCount); } } @@ -367,7 +362,8 @@ namespace Barotrauma.Steam { ToolBox.OpenFileWithShell(mod.Dir); return false; - } + }, + ToolTip = TextManager.Get("OpenLocalModInExplorer") }; } else if (ContentPackageManager.WorkshopPackages.Contains(mod)) @@ -386,8 +382,13 @@ namespace Barotrauma.Steam onInstalledInfoButtonHit(item.Value); }); return false; - } + }, + ToolTip = TextManager.Get("ViewModDetails") }; + if (!SteamManager.IsInitialized) + { + infoButton.Enabled = false; + } TaskPool.Add( $"DetermineUpdateRequired{mod.SteamWorkshopId}", mod.IsUpToDate(), @@ -398,6 +399,7 @@ namespace Barotrauma.Steam if (!isUpToDate) { infoButton.ApplyStyle(GUIStyle.ComponentStyles["WorkshopMenu.InfoButtonUpdate"]); + infoButton.ToolTip = TextManager.Get("ViewModDetailsUpdateAvailable"); } }); } @@ -421,20 +423,26 @@ namespace Barotrauma.Steam private void CreatePopularModsTab(out GUIListBox popularModsList) { GUIFrame content = CreateNewContentFrame(Tab.PopularMods); - + if (!SteamManager.IsInitialized) + { + tabContents[Tab.PopularMods].Button.Enabled = false; + } CreateWorkshopItemList(content, out _, out popularModsList, onSelected: PopulateFrameWithItemInfo); } private void CreatePublishTab(out GUIListBox selfModsList) { GUIFrame content = CreateNewContentFrame(Tab.Publish); - + if (!SteamManager.IsInitialized) + { + tabContents[Tab.Publish].Button.Enabled = false; + } CreateWorkshopItemOrPackageList(content, out _, out selfModsList, onSelected: PopulatePublishTab); } public void Apply() { - ContentPackageManager.EnabledPackages.SetCore(enabledCorePackage); + ContentPackageManager.EnabledPackages.SetCore(EnabledCorePackage); ContentPackageManager.EnabledPackages.SetRegular(enabledRegularModsList.Content.Children .Where(c => c.UserData is RegularPackage).Select(c => (RegularPackage)c.UserData).ToArray()); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs index c6eb0b6b4..397c71876 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; -using System.Text; using Color = Microsoft.Xna.Framework.Color; namespace Barotrauma @@ -127,7 +126,7 @@ namespace Barotrauma Vector2 newPoint = new Vector2(point.X, point.Y); foreach (Vector2 otherPoint in toCheck.Concat(newPoints)) { - float diffX = Math.Abs(newPoint.X - otherPoint.X), + float diffX = Math.Abs(newPoint.X - otherPoint.X), diffY = Math.Abs(newPoint.Y - otherPoint.Y); if (diffX <= treshold) @@ -142,7 +141,7 @@ namespace Barotrauma } newPoints.Add(newPoint); } - + return newPoints; } @@ -275,7 +274,7 @@ namespace Barotrauma return pts; } } - + // Convert an RGB value into an HLS value. public static Vector3 RgbToHLS(this Color color) { @@ -320,7 +319,7 @@ namespace Barotrauma if (hue < 240) return q1 + (q2 - q1) * (240 - hue) / 60; return q1; } - + /// /// Convert a RGB value into a HSV value. /// @@ -329,7 +328,7 @@ namespace Barotrauma /// /// Vector3 where X is the hue (0-360 or NaN) /// Y is the saturation (0-1) - /// Z is the value (0-1) + /// Z is the value (0-1) /// public static Vector3 RGBToHSV(Color color) { @@ -478,7 +477,7 @@ namespace Barotrauma if (b.Build < a.Build) { return false; } return false; } - + public static void OpenFileWithShell(string filename) { ProcessStartInfo startInfo = new ProcessStartInfo() @@ -488,5 +487,24 @@ namespace Barotrauma }; Process.Start(startInfo); } + + public static Vector2 PaddingSizeParentRelative(RectTransform parent, float padding) + { + var (sizeX, sizeY) = parent.NonScaledSize.ToVector2(); + + float higher = sizeX, + lower = sizeY; + bool swap = lower > higher; + if (swap) { (higher, lower) = (lower, higher); } + + float diffY = lower - lower * padding; + + float paddingX = (higher - diffY) / higher, + paddingY = padding; + + if (swap) { (paddingX, paddingY) = (paddingY, paddingX); } + + return new Vector2(paddingX, paddingY); + } } } diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 31debdb9c..5b77eb340 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.0.0 + 0.17.1.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 6f1f04661..48d9ebdc3 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.0.0 + 0.17.1.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index ca8ab114b..7ee9ab78d 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.0.0 + 0.17.1.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 996817458..b77b4c435 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.0.0 + 0.17.1.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 296dc0707..d22aef776 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.0.0 + 0.17.1.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs index 4a9e5f918..49405004e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs @@ -56,7 +56,7 @@ namespace Barotrauma partial void OnMoneyChanged(int prevAmount, int newAmount) { - GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.UpdateMoney }); + GameMain.NetworkMember.CreateEntityEvent(this, new UpdateMoneyEventData()); } partial void OnTalentGiven(TalentPrefab talentPrefab) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs index c3d14550f..096e49741 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs @@ -19,7 +19,7 @@ namespace Barotrauma } if (Math.Abs(prevSentSkill[skillIdentifier] - newLevel) > 0.01f) { - GameMain.NetworkMember.CreateEntityEvent(Character, new object[] { NetEntityEvent.Type.UpdateSkills }); + GameMain.NetworkMember.CreateEntityEvent(Character, new Character.UpdateSkillsEventData()); prevSentSkill[skillIdentifier] = newLevel; } } @@ -30,14 +30,14 @@ namespace Barotrauma if (prevAmount != newAmount) { GameServer.Log($"{GameServer.CharacterLogName(Character)} has gained {newAmount - prevAmount} experience ({prevAmount} -> {newAmount})", ServerLog.MessageType.Talent); - GameMain.NetworkMember.CreateEntityEvent(Character, new object[] { NetEntityEvent.Type.UpdateExperience }); + GameMain.NetworkMember.CreateEntityEvent(Character, new Character.UpdateExperienceEventData()); } } partial void OnPermanentStatChanged(StatTypes statType) { if (Character == null || Character.Removed) { return; } - GameMain.NetworkMember.CreateEntityEvent(Character, new object[] { NetEntityEvent.Type.UpdatePermanentStats, statType }); + GameMain.NetworkMember.CreateEntityEvent(Character, new Character.UpdatePermanentStatsEventData()); } public void ServerWrite(IWriteMessage msg) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index a7c694e5d..7d8e7791e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -153,14 +153,94 @@ namespace Barotrauma } } - public virtual void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerReadInput(IReadMessage msg, Client c) { - if (GameMain.Server == null) return; - - switch (type) + if (c.Character != this) { - case ClientNetObject.CHARACTER_INPUT: +#if DEBUG + DebugConsole.Log("Received a character update message from a client who's not controlling the character"); +#endif + return; + } + UInt16 networkUpdateID = msg.ReadUInt16(); + byte inputCount = msg.ReadByte(); + + if (AllowInput) { Enabled = true; } + + for (int i = 0; i < inputCount; i++) + { + InputNetFlags newInput = (InputNetFlags)msg.ReadRangedInteger(0, (int)InputNetFlags.MaxVal); + UInt16 newAim = 0; + UInt16 newInteract = 0; + + if (newInput != InputNetFlags.None && newInput != InputNetFlags.FacingLeft) + { + c.KickAFKTimer = 0.0f; + } + else if (AnimController.Dir < 0.0f != newInput.HasFlag(InputNetFlags.FacingLeft)) + { + //character changed the direction they're facing + c.KickAFKTimer = 0.0f; + } + + newAim = msg.ReadUInt16(); + if (newInput.HasFlag(InputNetFlags.Select) || + newInput.HasFlag(InputNetFlags.Deselect) || + newInput.HasFlag(InputNetFlags.Use) || + newInput.HasFlag(InputNetFlags.Health) || + newInput.HasFlag(InputNetFlags.Grab)) + { + newInteract = msg.ReadUInt16(); + } + + if (NetIdUtils.IdMoreRecent((ushort)(networkUpdateID - i), LastNetworkUpdateID) && (i < 60)) + { + if ((i > 0 && memInput[i - 1].intAim != newAim)) + { + c.KickAFKTimer = 0.0f; + } + NetInputMem newMem = new NetInputMem + { + states = newInput, + intAim = newAim, + interact = newInteract, + networkUpdateID = (ushort)(networkUpdateID - i) + }; + memInput.Insert(i, newMem); + LastInputTime = Timing.TotalTime; + } + } + + if (NetIdUtils.IdMoreRecent(networkUpdateID, LastNetworkUpdateID)) + { + LastNetworkUpdateID = networkUpdateID; + } + else if (NetIdUtils.Difference(networkUpdateID, LastNetworkUpdateID) > 500) + { +#if DEBUG || UNSTABLE + DebugConsole.AddWarning($"Large disrepancy between a client character's network update ID server-side and client-side (client: {networkUpdateID}, server: {LastNetworkUpdateID}). Resetting the ID."); +#endif + LastNetworkUpdateID = networkUpdateID; + } + if (memInput.Count > 60) + { + //deleting inputs from the queue here means the server is way behind and data needs to be dropped + //we'll make the server drop down to 30 inputs for good measure + memInput.RemoveRange(30, memInput.Count - 30); + } + } + + public virtual void ServerEventRead(IReadMessage msg, Client c) + { + EventType eventType = (EventType)msg.ReadRangedInteger((int)EventType.MinValue, (int)EventType.MaxValue); + switch (eventType) + { + case EventType.InventoryState: + Inventory.ServerEventRead(msg, c); + break; + case EventType.Treatment: + bool doingCPR = msg.ReadBoolean(); if (c.Character != this) { #if DEBUG @@ -169,415 +249,315 @@ namespace Barotrauma return; } - UInt16 networkUpdateID = msg.ReadUInt16(); - byte inputCount = msg.ReadByte(); - - if (AllowInput) { Enabled = true; } - - for (int i = 0; i < inputCount; i++) + AnimController.Anim = doingCPR ? AnimController.Animation.CPR : AnimController.Animation.None; + break; + case EventType.Status: + if (c.Character != this) { - InputNetFlags newInput = (InputNetFlags)msg.ReadRangedInteger(0, (int)InputNetFlags.MaxVal); - UInt16 newAim = 0; - UInt16 newInteract = 0; - - if (newInput != InputNetFlags.None && newInput != InputNetFlags.FacingLeft) - { - c.KickAFKTimer = 0.0f; - } - else if (AnimController.Dir < 0.0f != newInput.HasFlag(InputNetFlags.FacingLeft)) - { - //character changed the direction they're facing - c.KickAFKTimer = 0.0f; - } - - newAim = msg.ReadUInt16(); - if (newInput.HasFlag(InputNetFlags.Select) || - newInput.HasFlag(InputNetFlags.Deselect) || - newInput.HasFlag(InputNetFlags.Use) || - newInput.HasFlag(InputNetFlags.Health) || - newInput.HasFlag(InputNetFlags.Grab)) - { - newInteract = msg.ReadUInt16(); - } - - if (NetIdUtils.IdMoreRecent((ushort)(networkUpdateID - i), LastNetworkUpdateID) && (i < 60)) - { - if ((i > 0 && memInput[i - 1].intAim != newAim)) - { - c.KickAFKTimer = 0.0f; - } - NetInputMem newMem = new NetInputMem - { - states = newInput, - intAim = newAim, - interact = newInteract, - networkUpdateID = (ushort)(networkUpdateID - i) - }; - memInput.Insert(i, newMem); - LastInputTime = Timing.TotalTime; - } - } - - if (NetIdUtils.IdMoreRecent(networkUpdateID, LastNetworkUpdateID)) - { - LastNetworkUpdateID = networkUpdateID; - } - else if (NetIdUtils.Difference(networkUpdateID, LastNetworkUpdateID) > 500) - { -#if DEBUG || UNSTABLE - DebugConsole.AddWarning($"Large disrepancy between a client character's network update ID server-side and client-side (client: {networkUpdateID}, server: {LastNetworkUpdateID}). Resetting the ID."); +#if DEBUG + DebugConsole.Log("Received a character update message from a client who's not controlling the character"); #endif - LastNetworkUpdateID = networkUpdateID; + return; } - if (memInput.Count > 60) + + if (IsIncapacitated) { - //deleting inputs from the queue here means the server is way behind and data needs to be dropped - //we'll make the server drop down to 30 inputs for good measure - memInput.RemoveRange(30, memInput.Count - 30); + var causeOfDeath = CharacterHealth.GetCauseOfDeath(); + Kill(causeOfDeath.type, causeOfDeath.affliction); } break; - - case ClientNetObject.ENTITY_STATE: - int eventType = msg.ReadRangedInteger(0, 4); - switch (eventType) + case EventType.UpdateTalents: + if (c.Character != this) { - case 0: - Inventory.ServerRead(type, msg, c); - break; - case 1: - bool doingCPR = msg.ReadBoolean(); - if (c.Character != this) - { #if DEBUG - DebugConsole.Log("Received a character update message from a client who's not controlling the character"); + DebugConsole.Log("Received a character update message from a client who's not controlling the character"); #endif - return; - } + return; + } - AnimController.Anim = doingCPR ? AnimController.Animation.CPR : AnimController.Animation.None; - break; - case 2: - if (c.Character != this) - { -#if DEBUG - DebugConsole.Log("Received a character update message from a client who's not controlling the character"); -#endif - return; - } + // get the full list of talents from the player, only give the ones + // that are not already given (or otherwise not viable) + ushort talentCount = msg.ReadUInt16(); + List talentSelection = new List(); + for (int i = 0; i < talentCount; i++) + { + UInt32 talentIdentifier = msg.ReadUInt32(); + var prefab = TalentPrefab.TalentPrefabs.Find(p => p.UintIdentifier == talentIdentifier); + if (prefab == null) { continue; } - if (IsIncapacitated) - { - var causeOfDeath = CharacterHealth.GetCauseOfDeath(); - Kill(causeOfDeath.type, causeOfDeath.affliction); - } - break; - case 3: // NetEntityEvent.Type.UpdateTalents - if (c.Character != this) - { -#if DEBUG - DebugConsole.Log("Received a character update message from a client who's not controlling the character"); -#endif - return; - } - - // get the full list of talents from the player, only give the ones - // that are not already given (or otherwise not viable) - ushort talentCount = msg.ReadUInt16(); - List talentSelection = new List(); - for (int i = 0; i < talentCount; i++) - { - UInt32 talentIdentifier = msg.ReadUInt32(); - var prefab = TalentPrefab.TalentPrefabs.Find(p => p.UintIdentifier == talentIdentifier); - if (prefab == null) { continue; } - - if (TalentTree.IsViableTalentForCharacter(this, prefab.Identifier, talentSelection)) - { - GiveTalent(prefab.Identifier); - talentSelection.Add(prefab.Identifier); - } - } - if (talentSelection.Count != talentCount) - { - DebugConsole.AddWarning($"Failed to unlock talents: the amount of unlocked talents doesn't match (client: {talentCount}, server: {talentSelection.Count})"); - } - break; + if (TalentTree.IsViableTalentForCharacter(this, prefab.Identifier, talentSelection)) + { + GiveTalent(prefab.Identifier); + talentSelection.Add(prefab.Identifier); + } + } + if (talentSelection.Count != talentCount) + { + DebugConsole.AddWarning($"Failed to unlock talents: the amount of unlocked talents doesn't match (client: {talentCount}, server: {talentSelection.Count})"); } break; } - msg.ReadPadBits(); } - public virtual void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerWritePosition(IWriteMessage msg, Client c) { - if (GameMain.Server == null) return; + msg.Write(ID); - if (extraData != null) + IWriteMessage tempBuffer = new WriteOnlyMessage(); + + if (this == c.Character) { - const int min = 0, max = 13; - switch ((NetEntityEvent.Type)extraData[0]) + tempBuffer.Write(true); + if (LastNetworkUpdateID < memInput.Count + 1) { - case NetEntityEvent.Type.InventoryState: - msg.WriteRangedInteger(0, min, max); - msg.Write(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0); - Inventory.ServerWrite(msg, c); - break; - case NetEntityEvent.Type.Control: - msg.WriteRangedInteger(1, min, max); - Client owner = (Client)extraData[1]; - msg.Write(owner == c && owner.Character == this); - msg.Write(owner != null && owner.Character == this && GameMain.Server.ConnectedClients.Contains(owner) ? owner.ID : (byte)0); - break; - case NetEntityEvent.Type.Status: - msg.WriteRangedInteger(2, min, max); - WriteStatus(msg); - break; - case NetEntityEvent.Type.UpdateSkills: - msg.WriteRangedInteger(3, min, max); - if (Info?.Job == null) - { - msg.Write((byte)0); - } - else - { - msg.Write((byte)Info.Job.Skills.Count); - foreach (Skill skill in Info.Job.Skills) - { - msg.Write(skill.Identifier); - msg.Write(skill.Level); - } - } - break; - case NetEntityEvent.Type.SetAttackTarget: - case NetEntityEvent.Type.ExecuteAttack: - Limb attackLimb = extraData[1] as Limb; - UInt16 targetEntityID = (UInt16)extraData[2]; - int targetLimbIndex = extraData.Length > 3 ? (int)extraData[3] : 0; - msg.WriteRangedInteger(extraData[0] is NetEntityEvent.Type.SetAttackTarget ? 4 : 5, min, max); - msg.Write((byte)(Removed ? 255 : Array.IndexOf(AnimController.Limbs, attackLimb))); - msg.Write(targetEntityID); - msg.Write((byte)targetLimbIndex); - msg.Write(extraData.Length > 4 ? (float)extraData[4] : 0); - msg.Write(extraData.Length > 5 ? (float)extraData[5] : 0); - break; - case NetEntityEvent.Type.AssignCampaignInteraction: - msg.WriteRangedInteger(6, min, max); - msg.Write((byte)CampaignInteractionType); - msg.Write(RequireConsciousnessForCustomInteract); - break; - case NetEntityEvent.Type.ObjectiveManagerState: - msg.WriteRangedInteger(7, min, max); - int type = (extraData[1] as string) switch - { - "order" => 1, - "objective" => 2, - _ => 0 - }; - msg.WriteRangedInteger(type, 0, 2); - if (!(AIController is HumanAIController controller)) - { - msg.Write(false); - break; - } - if (type == 1) - { - var currentOrderInfo = controller.ObjectiveManager.GetCurrentOrderInfo(); - bool validOrder = currentOrderInfo != null; - msg.Write(validOrder); - if (!validOrder) { break; } - var orderPrefab = currentOrderInfo.Prefab; - msg.Write(orderPrefab.UintIdentifier); - if (!orderPrefab.HasOptions) { break; } - int optionIndex = orderPrefab.AllOptions.IndexOf(currentOrderInfo.Option); - if (optionIndex == -1) - { - DebugConsole.AddWarning($"Error while writing order data. Order option \"{currentOrderInfo.Option}\" not found in the order prefab \"{orderPrefab.Name}\"."); - } - msg.WriteRangedInteger(optionIndex, -1, orderPrefab.AllOptions.Length); - } - else if (type == 2) - { - var objective = controller.ObjectiveManager.CurrentObjective; - bool validObjective = objective != null && objective.Identifier != Identifier.Empty; - msg.Write(validObjective); - if (!validObjective) { break; } - msg.Write(objective.Identifier); - msg.Write(objective.Option); - UInt16 targetEntityId = 0; - if (objective is AIObjectiveOperateItem operateObjective && operateObjective.OperateTarget != null) - { - targetEntityId = operateObjective.OperateTarget.ID; - } - msg.Write(targetEntityId); - } - break; - case NetEntityEvent.Type.TeamChange: - msg.WriteRangedInteger(8, min, max); - msg.Write((byte)TeamID); - break; - case NetEntityEvent.Type.AddToCrew: - msg.WriteRangedInteger(9, min, max); - msg.Write((byte)(CharacterTeamType)extraData[1]); // team id - ushort[] inventoryItemIDs = (ushort[])extraData[2]; - msg.Write((ushort)inventoryItemIDs.Length); - for (int i = 0; i < inventoryItemIDs.Length; i++) - { - msg.Write(inventoryItemIDs[i]); - } - break; - case NetEntityEvent.Type.UpdateExperience: - msg.WriteRangedInteger(10, min, max); - msg.Write(Info.ExperiencePoints); - break; - case NetEntityEvent.Type.UpdateTalents: - msg.WriteRangedInteger(11, min, max); - msg.Write((ushort)characterTalents.Count); - foreach (var unlockedTalent in characterTalents) - { - msg.Write(unlockedTalent.AddedThisRound); - msg.Write(unlockedTalent.Prefab.UintIdentifier); - } - break; - case NetEntityEvent.Type.UpdateMoney: - msg.WriteRangedInteger(12, min, max); - msg.Write(GameMain.GameSession.Campaign.Money); - break; - case NetEntityEvent.Type.UpdatePermanentStats: - msg.WriteRangedInteger(13, min, max); - if (Info == null || extraData.Length < 2 || !(extraData[1] is StatTypes statType)) - { - msg.Write((byte)0); - msg.Write((byte)0); - } - else if (!Info.SavedStatValues.ContainsKey(statType)) - { - msg.Write((byte)0); - msg.Write((byte)statType); - } - else - { - msg.Write((byte)Info.SavedStatValues[statType].Count); - msg.Write((byte)statType); - foreach (var savedStatValue in Info.SavedStatValues[statType]) - { - msg.Write(savedStatValue.StatIdentifier); - msg.Write(savedStatValue.StatValue); - msg.Write(savedStatValue.RemoveOnDeath); - } - } - break; - default: - DebugConsole.ThrowError("Invalid NetworkEvent type for entity " + ToString() + " (" + (NetEntityEvent.Type)extraData[0] + ")"); - break; + tempBuffer.Write((UInt16)0); + } + else + { + tempBuffer.Write((UInt16)(LastNetworkUpdateID - memInput.Count - 1)); } - msg.WritePadBits(); } else { - msg.Write(ID); + tempBuffer.Write(false); - IWriteMessage tempBuffer = new WriteOnlyMessage(); + bool aiming = false; + bool use = false; + bool attack = false; + bool shoot = false; - if (this == c.Character) + if (IsRemotePlayer) { - tempBuffer.Write(true); - if (LastNetworkUpdateID < memInput.Count + 1) + aiming = dequeuedInput.HasFlag(InputNetFlags.Aim); + use = dequeuedInput.HasFlag(InputNetFlags.Use); + attack = dequeuedInput.HasFlag(InputNetFlags.Attack); + shoot = dequeuedInput.HasFlag(InputNetFlags.Shoot); + } + else if (keys != null) + { + aiming = keys[(int)InputType.Aim].GetHeldQueue; + use = keys[(int)InputType.Use].GetHeldQueue; + attack = keys[(int)InputType.Attack].GetHeldQueue; + shoot = keys[(int)InputType.Shoot].GetHeldQueue; + networkUpdateSent = true; + } + + tempBuffer.Write(aiming); + tempBuffer.Write(shoot); + tempBuffer.Write(use); + if (AnimController is HumanoidAnimController) + { + tempBuffer.Write(((HumanoidAnimController)AnimController).Crouching); + } + tempBuffer.Write(attack); + + Vector2 relativeCursorPos = cursorPosition - AimRefPosition; + tempBuffer.Write((UInt16)(65535.0 * Math.Atan2(relativeCursorPos.Y, relativeCursorPos.X) / (2.0 * Math.PI))); + + tempBuffer.Write(IsRagdolled || Stun > 0.0f || IsDead || IsIncapacitated); + + tempBuffer.Write(AnimController.Dir > 0.0f); + } + + if (SelectedCharacter != null || SelectedConstruction != null) + { + tempBuffer.Write(true); + tempBuffer.Write(SelectedCharacter != null ? SelectedCharacter.ID : NullEntityID); + tempBuffer.Write(SelectedConstruction != null ? SelectedConstruction.ID : NullEntityID); + if (SelectedCharacter != null) + { + tempBuffer.Write(AnimController.Anim == AnimController.Animation.CPR); + } + } + else + { + tempBuffer.Write(false); + } + + tempBuffer.Write(SimPosition.X); + tempBuffer.Write(SimPosition.Y); + float MaxVel = NetConfig.MaxPhysicsBodyVelocity; + AnimController.Collider.LinearVelocity = new Vector2( + MathHelper.Clamp(AnimController.Collider.LinearVelocity.X, -MaxVel, MaxVel), + MathHelper.Clamp(AnimController.Collider.LinearVelocity.Y, -MaxVel, MaxVel)); + tempBuffer.WriteRangedSingle(AnimController.Collider.LinearVelocity.X, -MaxVel, MaxVel, 12); + tempBuffer.WriteRangedSingle(AnimController.Collider.LinearVelocity.Y, -MaxVel, MaxVel, 12); + + bool fixedRotation = AnimController.Collider.FarseerBody.FixedRotation || !AnimController.Collider.PhysEnabled; + tempBuffer.Write(fixedRotation); + if (!fixedRotation) + { + tempBuffer.Write(AnimController.Collider.Rotation); + float MaxAngularVel = NetConfig.MaxPhysicsBodyAngularVelocity; + AnimController.Collider.AngularVelocity = NetConfig.Quantize(AnimController.Collider.AngularVelocity, -MaxAngularVel, MaxAngularVel, 8); + tempBuffer.WriteRangedSingle(MathHelper.Clamp(AnimController.Collider.AngularVelocity, -MaxAngularVel, MaxAngularVel), -MaxAngularVel, MaxAngularVel, 8); + } + + bool writeStatus = healthUpdateTimer <= 0.0f; + tempBuffer.Write(writeStatus); + if (writeStatus) + { + WriteStatus(tempBuffer); + AIController?.ServerWrite(tempBuffer); + HealthUpdatePending = false; + } + + tempBuffer.WritePadBits(); + + msg.WriteVariableUInt32((uint)tempBuffer.LengthBytes); + msg.Write(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); + } + + public virtual void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) + { + if (!(extraData is IEventData eventData)) { throw new Exception($"Malformed character event: expected {nameof(Character)}.{nameof(IEventData)}, got {extraData?.GetType().Name ?? "[NULL]"}"); } + + msg.WriteRangedInteger((int)eventData.EventType, (int)EventType.MinValue, (int)EventType.MaxValue); + switch (eventData) + { + case InventoryStateEventData _: + msg.Write(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0); + Inventory.ServerEventWrite(msg, c); + break; + case ControlEventData controlEventData: + Client owner = controlEventData.Owner; + msg.Write(owner == c && owner.Character == this); + msg.Write(owner != null && owner.Character == this && GameMain.Server.ConnectedClients.Contains(owner) ? owner.ID : (byte)0); + break; + case StatusEventData _: + WriteStatus(msg); + break; + case UpdateSkillsEventData _: + if (Info?.Job == null) { - tempBuffer.Write((UInt16)0); + msg.Write((byte)0); } else { - tempBuffer.Write((UInt16)(LastNetworkUpdateID - memInput.Count - 1)); + msg.Write((byte)Info.Job.Skills.Count); + foreach (Skill skill in Info.Job.Skills) + { + msg.Write(skill.Identifier); + msg.Write(skill.Level); + } } - } - else - { - tempBuffer.Write(false); - - bool aiming = false; - bool use = false; - bool attack = false; - bool shoot = false; - - if (IsRemotePlayer) + break; + case IAttackEventData attackEventData: { - aiming = dequeuedInput.HasFlag(InputNetFlags.Aim); - use = dequeuedInput.HasFlag(InputNetFlags.Use); - attack = dequeuedInput.HasFlag(InputNetFlags.Attack); - shoot = dequeuedInput.HasFlag(InputNetFlags.Shoot); + int attackLimbIndex = Removed ? -1 : Array.IndexOf(AnimController.Limbs, attackEventData.AttackLimb); + ushort targetEntityId = 0; + int targetLimbIndex = -1; + if (attackEventData.TargetEntity is Entity { Removed: false } targetEntity) + { + targetEntityId = targetEntity.ID; + if (targetEntity is Character { AnimController: { Limbs: var targetLimbsArray } }) + { + targetLimbIndex = targetLimbsArray.IndexOf(attackEventData.TargetLimb); + } + } + msg.Write((byte)(attackLimbIndex < 0 ? 255 : attackLimbIndex)); + msg.Write((ushort)targetEntityId); + msg.Write((byte)(targetLimbIndex < 0 ? 255 : targetLimbIndex)); + msg.Write(attackEventData.TargetSimPos.X); + msg.Write(attackEventData.TargetSimPos.Y); } - else if (keys != null) + break; + case AssignCampaignInteractionEventData _: + msg.Write((byte)CampaignInteractionType); + msg.Write(RequireConsciousnessForCustomInteract); + break; + case ObjectiveManagerStateEventData objectiveManagerStateEventData: + AIObjectiveManager.ObjectiveType type = objectiveManagerStateEventData.ObjectiveType; + msg.WriteRangedInteger((int)type, (int)AIObjectiveManager.ObjectiveType.MinValue, (int)AIObjectiveManager.ObjectiveType.MaxValue); + if (!(AIController is HumanAIController controller)) { - aiming = keys[(int)InputType.Aim].GetHeldQueue; - use = keys[(int)InputType.Use].GetHeldQueue; - attack = keys[(int)InputType.Attack].GetHeldQueue; - shoot = keys[(int)InputType.Shoot].GetHeldQueue; - networkUpdateSent = true; + msg.Write(false); + break; } - - tempBuffer.Write(aiming); - tempBuffer.Write(shoot); - tempBuffer.Write(use); - if (AnimController is HumanoidAnimController) + if (type == AIObjectiveManager.ObjectiveType.Order) { - tempBuffer.Write(((HumanoidAnimController)AnimController).Crouching); + var currentOrderInfo = controller.ObjectiveManager.GetCurrentOrderInfo(); + bool validOrder = currentOrderInfo != null; + msg.Write(validOrder); + if (!validOrder) { break; } + var orderPrefab = currentOrderInfo.Prefab; + msg.Write(orderPrefab.UintIdentifier); + if (!orderPrefab.HasOptions) { break; } + int optionIndex = orderPrefab.AllOptions.IndexOf(currentOrderInfo.Option); + if (optionIndex == -1) + { + DebugConsole.AddWarning($"Error while writing order data. Order option \"{currentOrderInfo.Option}\" not found in the order prefab \"{orderPrefab.Name}\"."); + } + msg.WriteRangedInteger(optionIndex, -1, orderPrefab.AllOptions.Length); } - tempBuffer.Write(attack); - - Vector2 relativeCursorPos = cursorPosition - AimRefPosition; - tempBuffer.Write((UInt16)(65535.0 * Math.Atan2(relativeCursorPos.Y, relativeCursorPos.X) / (2.0 * Math.PI))); - - tempBuffer.Write(IsRagdolled || Stun > 0.0f || IsDead || IsIncapacitated); - - tempBuffer.Write(AnimController.Dir > 0.0f); - } - - if (SelectedCharacter != null || SelectedConstruction != null) - { - tempBuffer.Write(true); - tempBuffer.Write(SelectedCharacter != null ? SelectedCharacter.ID : NullEntityID); - tempBuffer.Write(SelectedConstruction != null ? SelectedConstruction.ID : NullEntityID); - if (SelectedCharacter != null) + else if (type == AIObjectiveManager.ObjectiveType.Objective) { - tempBuffer.Write(AnimController.Anim == AnimController.Animation.CPR); + var objective = controller.ObjectiveManager.CurrentObjective; + bool validObjective = objective?.Identifier is { IsEmpty: false }; + msg.Write(validObjective); + if (!validObjective) { break; } + msg.Write(objective.Identifier); + msg.Write(objective.Option); + UInt16 targetEntityId = 0; + if (objective is AIObjectiveOperateItem operateObjective && operateObjective.OperateTarget != null) + { + targetEntityId = operateObjective.OperateTarget.ID; + } + msg.Write(targetEntityId); } - } - else - { - tempBuffer.Write(false); - } - - tempBuffer.Write(SimPosition.X); - tempBuffer.Write(SimPosition.Y); - float MaxVel = NetConfig.MaxPhysicsBodyVelocity; - AnimController.Collider.LinearVelocity = new Vector2( - MathHelper.Clamp(AnimController.Collider.LinearVelocity.X, -MaxVel, MaxVel), - MathHelper.Clamp(AnimController.Collider.LinearVelocity.Y, -MaxVel, MaxVel)); - tempBuffer.WriteRangedSingle(AnimController.Collider.LinearVelocity.X, -MaxVel, MaxVel, 12); - tempBuffer.WriteRangedSingle(AnimController.Collider.LinearVelocity.Y, -MaxVel, MaxVel, 12); - - bool fixedRotation = AnimController.Collider.FarseerBody.FixedRotation || !AnimController.Collider.PhysEnabled; - tempBuffer.Write(fixedRotation); - if (!fixedRotation) - { - tempBuffer.Write(AnimController.Collider.Rotation); - float MaxAngularVel = NetConfig.MaxPhysicsBodyAngularVelocity; - AnimController.Collider.AngularVelocity = NetConfig.Quantize(AnimController.Collider.AngularVelocity, -MaxAngularVel, MaxAngularVel, 8); - tempBuffer.WriteRangedSingle(MathHelper.Clamp(AnimController.Collider.AngularVelocity, -MaxAngularVel, MaxAngularVel), -MaxAngularVel, MaxAngularVel, 8); - } - - bool writeStatus = healthUpdateTimer <= 0.0f; - tempBuffer.Write(writeStatus); - if (writeStatus) - { - WriteStatus(tempBuffer); - AIController?.ServerWrite(tempBuffer); - HealthUpdatePending = false; - } - - tempBuffer.WritePadBits(); - - msg.WriteVariableUInt32((uint)tempBuffer.LengthBytes); - msg.Write(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); + break; + case TeamChangeEventData _: + msg.Write((byte)TeamID); + break; + case AddToCrewEventData addToCrewEventData: + msg.Write((byte)addToCrewEventData.TeamType); // team id + ushort[] inventoryItemIDs = addToCrewEventData.InventoryItems.Select(item => item.ID).ToArray(); + msg.Write((ushort)inventoryItemIDs.Length); + for (int i = 0; i < inventoryItemIDs.Length; i++) + { + msg.Write(inventoryItemIDs[i]); + } + break; + case UpdateExperienceEventData _: + msg.Write(Info.ExperiencePoints); + break; + case UpdateTalentsEventData _: + msg.Write((ushort)characterTalents.Count); + foreach (var unlockedTalent in characterTalents) + { + msg.Write(unlockedTalent.AddedThisRound); + msg.Write(unlockedTalent.Prefab.UintIdentifier); + } + break; + case UpdateMoneyEventData _: + msg.Write(GameMain.GameSession.Campaign.GetWallet(c).Balance); + break; + case UpdatePermanentStatsEventData updatePermanentStatsEventData: + StatTypes statType = updatePermanentStatsEventData.StatType; + if (Info == null) + { + msg.Write((byte)0); + msg.Write((byte)0); + } + else if (!Info.SavedStatValues.ContainsKey(statType)) + { + msg.Write((byte)0); + msg.Write((byte)statType); + } + else + { + msg.Write((byte)Info.SavedStatValues[statType].Count); + msg.Write((byte)statType); + foreach (var savedStatValue in Info.SavedStatValues[statType]) + { + msg.Write(savedStatValue.StatIdentifier); + msg.Write(savedStatValue.StatValue); + msg.Write(savedStatValue.RemoveOnDeath); + } + } + break; + default: + throw new Exception($"Malformed character event: did not expect {eventData.GetType().Name}"); } } @@ -656,6 +636,8 @@ namespace Barotrauma { msg.Write(true); msg.Write(ownerClient.ID); + msg.Write(Wallet.Balance); + msg.WriteRangedInteger(Wallet.RewardDistribution, 0, 100); } else if (GameMain.Server.Character == this) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index c3ad6d244..c1a0eff87 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -1,16 +1,12 @@ -using Barotrauma.Networking; +using Barotrauma.Items.Components; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; -using System.Linq; using System.Collections.Generic; -using System.ComponentModel; -using FarseerPhysics; -using Barotrauma.Items.Components; -using System.Threading; -using Barotrauma.IO; -using System.Text; using System.Diagnostics; using System.Globalization; +using System.Linq; +using System.Text; namespace Barotrauma { @@ -1187,7 +1183,7 @@ namespace Barotrauma NewMessage("*****************", Color.Lime); GameServer.Log("Console command \"restart\" executed: closing the server...", ServerLog.MessageType.ServerMessage); GameMain.Instance.CloseServer(); - GameMain.Instance.TryStartChildServerRelay(); + Program.TryStartChildServerRelay(GameMain.Instance.CommandLineArgs); GameMain.Instance.StartServer(); })); @@ -1400,17 +1396,71 @@ namespace Barotrauma commands.Add(new Command("eventdata", "", (string[] args) => { if (args.Length == 0) { return; } - if (!UInt16.TryParse(args[0], NumberStyles.Any, CultureInfo.InvariantCulture, out ushort eventId)) { return; } - ServerEntityEvent ev = GameMain.Server.EntityEventManager.Events.Find(ev => ev.ID == eventId); - if (ev != null) + + string indentStr(string s) + => string.Join('\n', s.Split('\n').Select(sub => $" {sub}")); + + string eventDataRip(object data) { + if (data is null) { return "[NULL]"; } + var type = data.GetType(); + + string retVal = $"{type.FullName} "; + + if (type.IsPrimitive + || type.IsEnum + || type.IsClass) + { + retVal += data.ToString(); + return retVal; + } + + retVal += "{\n"; + var fields = data.GetType().GetFields(); + foreach (var field in fields) + { + retVal += indentStr($"{field.Name}: {eventDataRip(field.GetValue(data))}")+"\n"; + } + + retVal += "}"; + retVal = retVal.Replace("{\n}", "{ }"); + return retVal; + } + + string eventDebugStr(ServerEntityEvent ev) + { + ushort eventId = ev.ID; + string entityData = ""; if (ev.Entity is { ID: var entityId, Removed: var removed, IdFreed: var idFreed }) { - entityData = $"Entity ID: {entityId}; Entity removed: {removed}; Entity ID freed: {idFreed}"; + entityData = $"Entity ID: {entityId}\n" + + $"Entity type {ev.Entity.GetType().Name}\n" + + $"Entity removed: {removed}\n" + + $"Entity ID freed: {idFreed}\n" + + $"Event data: {eventDataRip(ev.Data)}\n"; } - NewMessage($"EventData {eventId}\n{entityData}", Color.Lime); - //NewMessage(ev.StackTrace.CleanupStackTrace(), Color.Lime); + + return $"EventData {eventId}\n{indentStr(entityData)}"; + } + + IReadOnlyList events = GameMain.Server.EntityEventManager.Events; + ushort? eventId = null; + if (args[0].Equals("latest", StringComparison.OrdinalIgnoreCase)) + { + eventId = events.Max(e => e.ID); + } + else if (UInt16.TryParse(args[0], NumberStyles.Any, CultureInfo.InvariantCulture, out ushort id)) + { + eventId = id; + } + IEnumerable matchedEvents = GameMain.Server.EntityEventManager.Events.Where(ev + => eventId.HasValue + ? ev.ID == eventId + : eventDebugStr(ev).Contains(args[0], StringComparison.OrdinalIgnoreCase)); + foreach (var ev in matchedEvents) + { + NewMessage(eventDebugStr(ev), Color.Lime); } })); @@ -1665,28 +1715,27 @@ namespace Barotrauma (Client client, Vector2 cursorWorldPos, string[] args) => { if (args.Length < 2) return; - - AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a => - a.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase) || - a.Identifier == args[0]); + string affliction = args[0]; + AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a => a.Identifier == affliction); if (afflictionPrefab == null) { - GameMain.Server.SendConsoleMessage("Affliction \"" + args[0] + "\" not found.", client, Color.Red); + afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a => a.Name.Equals(affliction, StringComparison.OrdinalIgnoreCase)); + } + if (afflictionPrefab == null) + { + GameMain.Server.SendConsoleMessage("Affliction \"" + affliction + "\" not found.", client, Color.Red); return; } - if (!float.TryParse(args[1], out float afflictionStrength)) { GameMain.Server.SendConsoleMessage("\"" + args[1] + "\" is not a valid affliction strength.", client, Color.Red); return; } - bool relativeStrength = false; if (args.Length > 4) { bool.TryParse(args[4], out relativeStrength); } - Character targetCharacter = (args.Length <= 2) ? client.Character : FindMatchingCharacter(args.Skip(2).ToArray()); if (targetCharacter != null) { @@ -2217,18 +2266,50 @@ namespace Barotrauma GameMain.Server.SendConsoleMessage("No campaign active!", senderClient, Color.Red); return; } + + Character targetCharacter = null; + + if (args.Length >= 2) + { + targetCharacter = FindMatchingCharacter(args.Skip(1).ToArray()); + } + if (int.TryParse(args[0], out int money)) { - campaign.Money += money; + Wallet wallet = targetCharacter is null ? campaign.Bank : targetCharacter.Wallet; + wallet.Give(money); GameAnalyticsManager.AddMoneyGainedEvent(money, GameAnalyticsManager.MoneySource.Cheat, "console"); campaign.LastUpdateID++; } else { GameMain.Server.SendConsoleMessage($"\"{args[0]}\" is not a valid numeric value.", senderClient, Color.Red); - } + } } ); + + AssignOnClientRequestExecute( + "showmoney", + (Client senderClient, Vector2 cursorWorldPos, string[] args) => + { + if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign)) + { + GameMain.Server.SendConsoleMessage("No campaign active!", senderClient, Color.Red); + return; + } + + StringBuilder sb = new StringBuilder(); + sb.Append($"Bank: {campaign.Bank.Balance}"); + foreach (Client client in GameMain.Server.ConnectedClients) + { + if (client.Character is null) { continue; } + sb.Append(Environment.NewLine); + sb.Append($"{client.Name}: {client.Character.Wallet.Balance}"); + } + GameMain.Server.SendConsoleMessage(sb.ToString(), senderClient); + } + ); + AssignOnClientRequestExecute( "campaigndestination|setcampaigndestination", (Client senderClient, Vector2 cursorWorldPos, string[] args) => @@ -2327,7 +2408,7 @@ namespace Barotrauma GameMain.Server.SendConsoleMessage($"Set {character.Name}'s {skillIdentifier} level to {level}", senderClient); } - GameMain.NetworkMember.CreateEntityEvent(character, new object[] { NetEntityEvent.Type.UpdateSkills }); + GameMain.NetworkMember.CreateEntityEvent(character, new Character.UpdateSkillsEventData()); } else { diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index 236257000..532d46b51 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -141,20 +141,6 @@ namespace Barotrauma }*/ } - public bool TryStartChildServerRelay() - { - for (int i = 0; i < CommandLineArgs.Length; i++) - { - switch (CommandLineArgs[i].Trim()) - { - case "-pipes": - ChildServerRelay.Start(CommandLineArgs[i + 2], CommandLineArgs[i + 1]); - return true; - } - } - return false; - } - public void StartServer() { string name = "Server"; @@ -299,7 +285,6 @@ namespace Barotrauma Hyper.ComponentModel.HyperTypeDescriptionProvider.Add(typeof(Items.Components.ItemComponent)); Hyper.ComponentModel.HyperTypeDescriptionProvider.Add(typeof(Hull)); - TryStartChildServerRelay(); Init(); StartServer(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs index b888b2139..00190b2fd 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs @@ -1,12 +1,13 @@ using Barotrauma.Extensions; using System.Collections.Generic; using System.Linq; +using Barotrauma.Networking; namespace Barotrauma { partial class CargoManager { - public void SellBackPurchasedItems(List itemsToSell) + public void SellBackPurchasedItems(List itemsToSell, Client client = null) { // Check all the prices before starting the transaction // to make sure the modifiers stay the same for the whole transaction @@ -15,12 +16,12 @@ namespace Barotrauma { var itemValue = item.Quantity * buyValues[item.ItemPrefab]; Location.StoreCurrentBalance -= itemValue; - campaign.Money += itemValue; + campaign.GetWallet(client).Give(itemValue); PurchasedItems.Remove(item); } } - public void BuyBackSoldItems(List itemsToBuy) + public void BuyBackSoldItems(List itemsToBuy, Client client) { // Check all the prices before starting the transaction // to make sure the modifiers stay the same for the whole transaction @@ -30,12 +31,12 @@ namespace Barotrauma int itemValue = sellValues[item.ItemPrefab]; if (Location.StoreCurrentBalance < itemValue || item.Removed) { continue; } Location.StoreCurrentBalance += itemValue; - campaign.Money -= itemValue; + campaign.Bank.TryDeduct(itemValue); SoldItems.Remove(item); } } - public void SellItems(List itemsToSell) + public void SellItems(List itemsToSell, Client client) { bool canAddToRemoveQueue = (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) && Entity.Spawner != null; IEnumerable sellableItemsInSub = Enumerable.Empty(); @@ -67,7 +68,7 @@ namespace Barotrauma } SoldItems.Add(item); Location.StoreCurrentBalance -= itemValue; - campaign.Money += itemValue; + campaign.Bank.Give(itemValue); GameAnalyticsManager.AddMoneyGainedEvent(itemValue, GameAnalyticsManager.MoneySource.Store, item.ItemPrefab.Identifier.Value); } OnSoldItemsChanged?.Invoke(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/Data/Wallet.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/Data/Wallet.cs new file mode 100644 index 000000000..7c0767001 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/Data/Wallet.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; + +namespace Barotrauma +{ + internal partial class Wallet + { + private readonly Queue transactions = new Queue(); + + partial void SettingsChanged(Option balanceChanged, Option rewardChanged) + { + transactions.Enqueue(new WalletChangedData + { + BalanceChanged = balanceChanged, + RewardDistributionChanged = rewardChanged + }); + } + + public bool HasTransactions() => transactions.Count > 0; + + public NetWalletTransaction DequeueAndMergeTransactions(ushort id) + { + Option targetCharacterID = id == Entity.NullEntityID ? Option.None() : Option.Some(id); + + WalletChangedData changedData = new WalletChangedData + { + BalanceChanged = Option.None(), + RewardDistributionChanged = Option.None() + }; + + while (transactions.TryDequeue(out WalletChangedData transactionOut)) + { + changedData = changedData.MergeInto(transactionOut); + } + + return new NetWalletTransaction + { + CharacterID = targetCharacterID, + ChangedData = changedData, + Info = CreateWalletInfo() + }; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs index f8c39e187..98015ddf0 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using System.Globalization; using System.Xml.Linq; namespace Barotrauma @@ -32,8 +31,26 @@ namespace Barotrauma { CharacterInfo.SaveOrderData(client.CharacterInfo, OrderData); } + + if (client.Character?.Wallet.Save() is { } walletSave) + { + WalletData = walletSave; + } } + public void Refresh(Character character) + { + healthData = new XElement("health"); + character.CharacterHealth.Save(healthData); + if (character.Inventory != null) + { + itemData = new XElement("inventory"); + Character.SaveInventory(character.Inventory, itemData); + } + OrderData = new XElement("orders"); + CharacterInfo.SaveOrderData(character.Info, OrderData); + WalletData = character.Wallet.Save(); + } public CharacterCampaignData(XElement element) { @@ -63,6 +80,9 @@ namespace Barotrauma case "orders": OrderData = subElement; break; + case Wallet.LowerCaseSaveElementName: + WalletData = subElement; + break; } } } @@ -98,7 +118,7 @@ namespace Barotrauma } public void ApplyHealthData(Character character) - { + { CharacterInfo.ApplyHealthData(character, healthData); } @@ -106,5 +126,26 @@ namespace Barotrauma { CharacterInfo.ApplyOrderData(character, OrderData); } + + public void ApplyWalletData(Character character) + { + character.Wallet = new Wallet(WalletData); + } + + public XElement Save() + { + XElement element = new XElement("CharacterCampaignData", + new XAttribute("name", Name), + new XAttribute("endpoint", ClientEndPoint), + new XAttribute("steamid", SteamID)); + + CharacterInfo?.Save(element); + if (itemData != null) { element.Add(itemData); } + if (healthData != null) { element.Add(healthData); } + if (OrderData != null) { element.Add(OrderData); } + if (WalletData != null) { element.Add(WalletData); } + + return element; + } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index f736290f3..fbd7d030e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -12,6 +12,22 @@ namespace Barotrauma partial class MultiPlayerCampaign : CampaignMode { private readonly List characterData = new List(); + private readonly Dictionary walletsToCheck = new Dictionary(); + private readonly HashSet transactions = new HashSet(); + private const float clientCheckInterval = 10; + private float clientCheckTimer = clientCheckInterval; + + public override Wallet GetWallet(Client client = null) + { + if (client is null) { throw new ArgumentNullException(nameof(client), "Client should not be null in multiplayer"); } + + if (client.Character is { } character) + { + return character.Wallet; + } + + return Wallet.Invalid; + } private bool forceMapUI; public bool ForceMapUI @@ -229,8 +245,7 @@ namespace Barotrauma characterInfo.CauseOfDeath = null; } c.CharacterInfo = characterInfo; - characterData.RemoveAll(cd => cd.MatchesClient(c)); - characterData.Add(new CharacterCampaignData(c)); + SetClientCharacterData(c); } //refresh the character data of clients who aren't in the server anymore @@ -412,8 +427,8 @@ namespace Barotrauma LastSaveID++; } - public bool CanPurchaseSub(SubmarineInfo info) - => info.Price <= Money && GetCampaignSubs().Contains(info); + public bool CanPurchaseSub(SubmarineInfo info, Client client) + => GetWallet(client).CanAfford(info.Price) && GetCampaignSubs().Contains(info); public void DiscardClientCharacterData(Client client) { @@ -487,6 +502,54 @@ namespace Barotrauma KeepCharactersCloseToOutpost(deltaTime); } } + + UpdateClientsToCheck(deltaTime); + UpdateWallets(); + } + + private void UpdateClientsToCheck(float deltaTime) + { + if (clientCheckTimer < clientCheckInterval) + { + clientCheckTimer += deltaTime; + return; + } + + clientCheckTimer = 0; + walletsToCheck.Clear(); + walletsToCheck.Add(0, Bank); + + foreach (Character character in Mission.GetSalaryEligibleCrew()) + { + walletsToCheck.Add(character.ID, character.Wallet); + } + } + + private void UpdateWallets() + { + foreach (var (id, wallet) in walletsToCheck) + { + if (wallet.HasTransactions()) + { + transactions.Add(wallet.DequeueAndMergeTransactions(id)); + } + } + + if (transactions.Count == 0) { return; } + + NetWalletUpdate walletUpdate = new NetWalletUpdate + { + Transactions = transactions.ToArray() + }; + + transactions.Clear(); + + foreach (Client client in GameMain.Server.ConnectedClients) + { + IWriteMessage msg = new WriteOnlyMessage().WithHeader(ServerPacketHeader.MONEY); + ((INetSerializableStruct)walletUpdate).Write(msg); + GameMain.Server?.ServerPeer?.Send(msg, client.Connection, DeliveryMethod.Reliable); + } } public override void End(TransitionType transitionType = TransitionType.None) @@ -530,7 +593,6 @@ namespace Barotrauma msg.Write(ForceMapUI); - msg.Write(Money); msg.Write(PurchasedHullRepairs); msg.Write(PurchasedItemRepairs); msg.Write(PurchasedLostShuttles); @@ -644,7 +706,7 @@ namespace Barotrauma { string itemPrefabIdentifier = msg.ReadString(); int itemQuantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity); - buyCrateItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity)); + buyCrateItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity, sender)); } UInt16 subSellCrateItemCount = msg.ReadUInt16(); @@ -653,7 +715,7 @@ namespace Barotrauma { string itemPrefabIdentifier = msg.ReadString(); int itemQuantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity); - subSellCrateItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity)); + subSellCrateItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity, sender)); } UInt16 purchasedItemCount = msg.ReadUInt16(); @@ -662,7 +724,7 @@ namespace Barotrauma { string itemPrefabIdentifier = msg.ReadString(); int itemQuantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity); - purchasedItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity)); + purchasedItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity, sender)); } UInt16 soldItemCount = msg.ReadUInt16(); @@ -711,57 +773,63 @@ namespace Barotrauma int hullRepairCost = location?.GetAdjustedMechanicalCost(HullRepairCost) ?? HullRepairCost; int itemRepairCost = location?.GetAdjustedMechanicalCost(ItemRepairCost) ?? ItemRepairCost; int shuttleRetrieveCost = location?.GetAdjustedMechanicalCost(ShuttleReplaceCost) ?? ShuttleReplaceCost; - if (purchasedHullRepairs != this.PurchasedHullRepairs) + Wallet personalWallet = GetWallet(sender); + + if (purchasedHullRepairs != PurchasedHullRepairs) { - if (purchasedHullRepairs && Money >= hullRepairCost) + switch (purchasedHullRepairs) { - this.PurchasedHullRepairs = true; - Money -= hullRepairCost; - GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "hullrepairs"); - } - else if (!purchasedHullRepairs) - { - this.PurchasedHullRepairs = false; - Money += hullRepairCost; + case true when personalWallet.CanAfford(hullRepairCost): + personalWallet.Deduct(hullRepairCost); + PurchasedHullRepairs = true; + GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "hullrepairs"); + break; + case false: + PurchasedHullRepairs = false; + personalWallet.Refund(hullRepairCost); + break; } } - if (purchasedItemRepairs != this.PurchasedItemRepairs) + + if (purchasedItemRepairs != PurchasedItemRepairs) { - if (purchasedItemRepairs && Money >= itemRepairCost) + switch (purchasedItemRepairs) { - this.PurchasedItemRepairs = true; - Money -= itemRepairCost; - GameAnalyticsManager.AddMoneySpentEvent(itemRepairCost, GameAnalyticsManager.MoneySink.Service, "devicerepairs"); - } - else if (!purchasedItemRepairs) - { - this.PurchasedItemRepairs = false; - Money += itemRepairCost; + case true when personalWallet.CanAfford(itemRepairCost): + personalWallet.Deduct(itemRepairCost); + PurchasedItemRepairs = true; + GameAnalyticsManager.AddMoneySpentEvent(itemRepairCost, GameAnalyticsManager.MoneySink.Service, "devicerepairs"); + break; + case false: + PurchasedItemRepairs = false; + personalWallet.Refund(itemRepairCost); + break; } } - if (purchasedLostShuttles != this.PurchasedLostShuttles) + + if (purchasedLostShuttles != PurchasedLostShuttles) { - if (GameMain.GameSession?.SubmarineInfo != null && - GameMain.GameSession.SubmarineInfo.LeftBehindSubDockingPortOccupied) + if (GameMain.GameSession?.SubmarineInfo != null && GameMain.GameSession.SubmarineInfo.LeftBehindSubDockingPortOccupied) { GameMain.Server.SendDirectChatMessage(TextManager.FormatServerMessage("ReplaceShuttleDockingPortOccupied"), sender, ChatMessageType.MessageBox); } - else if (purchasedLostShuttles && Money >= shuttleRetrieveCost) + else if (purchasedLostShuttles && personalWallet.TryDeduct(shuttleRetrieveCost)) { - this.PurchasedLostShuttles = true; - Money -= shuttleRetrieveCost; + PurchasedLostShuttles = true; GameAnalyticsManager.AddMoneySpentEvent(shuttleRetrieveCost, GameAnalyticsManager.MoneySink.Service, "retrieveshuttle"); } else if (!purchasedItemRepairs) { - this.PurchasedLostShuttles = false; - Money += shuttleRetrieveCost; + PurchasedLostShuttles = false; + personalWallet.Refund(shuttleRetrieveCost); } } + if (currentLocIndex < Map.Locations.Count && Map.AllowDebugTeleport) { Map.SetLocation(currentLocIndex); } + Map.SelectLocation(selectedLocIndex == UInt16.MaxValue ? -1 : selectedLocIndex); if (Map.SelectedLocation == null) { Map.SelectRandomLocation(preferUndiscovered: true); } if (Map.SelectedConnection != null) { Map.SelectMission(selectedMissionIndices); } @@ -772,18 +840,18 @@ namespace Barotrauma if (allowedToManageCampaign || allowedToUseStore || AllowedToManageCampaign(sender, ClientPermissions.BuyItems)) { var currentBuyCrateItems = new List(CargoManager.ItemsInBuyCrate); - currentBuyCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInBuyCrate(i.ItemPrefab, -i.Quantity)); - buyCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInBuyCrate(i.ItemPrefab, i.Quantity)); + currentBuyCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInBuyCrate(i.ItemPrefab, -i.Quantity, sender)); + buyCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInBuyCrate(i.ItemPrefab, i.Quantity, sender)); CargoManager.SellBackPurchasedItems(new List(CargoManager.PurchasedItems)); - CargoManager.PurchaseItems(purchasedItems, false); + CargoManager.PurchaseItems(purchasedItems, false, sender); } bool allowedToSellSubItems = AllowedToManageCampaign(sender, ClientPermissions.SellSubItems); if (allowedToManageCampaign || allowedToUseStore || allowedToSellSubItems) { var currentSubSellCrateItems = new List(CargoManager.ItemsInSellFromSubCrate); - currentSubSellCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInSubSellCrate(i.ItemPrefab, -i.Quantity)); - subSellCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInSubSellCrate(i.ItemPrefab, i.Quantity)); + currentSubSellCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInSubSellCrate(i.ItemPrefab, -i.Quantity, sender)); + subSellCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInSubSellCrate(i.ItemPrefab, i.Quantity, sender)); } bool allowedToSellInventoryItems = AllowedToManageCampaign(sender, ClientPermissions.SellInventoryItems); @@ -791,29 +859,29 @@ namespace Barotrauma { // for some reason CargoManager.SoldItem is never cleared by the server, I've added a check to SellItems that ignores all // sold items that are removed so they should be discarded on the next message - CargoManager.BuyBackSoldItems(new List(CargoManager.SoldItems)); - CargoManager.SellItems(soldItems); + CargoManager.BuyBackSoldItems(new List(CargoManager.SoldItems), sender); + CargoManager.SellItems(soldItems, sender); } else if (allowedToSellInventoryItems || allowedToSellSubItems) { if (allowedToSellInventoryItems) { - CargoManager.BuyBackSoldItems(new List(CargoManager.SoldItems.Where(i => i.Origin == SoldItem.SellOrigin.Character))); + CargoManager.BuyBackSoldItems(new List(CargoManager.SoldItems.Where(i => i.Origin == SoldItem.SellOrigin.Character)), sender); soldItems.RemoveAll(i => i.Origin != SoldItem.SellOrigin.Character); } else { - CargoManager.BuyBackSoldItems(new List(CargoManager.SoldItems.Where(i => i.Origin == SoldItem.SellOrigin.Submarine))); + CargoManager.BuyBackSoldItems(new List(CargoManager.SoldItems.Where(i => i.Origin == SoldItem.SellOrigin.Submarine)), sender); soldItems.RemoveAll(i => i.Origin != SoldItem.SellOrigin.Submarine); } - CargoManager.SellItems(soldItems); + CargoManager.SellItems(soldItems, sender); } if (allowedToManageCampaign) { foreach (var (prefab, category, _) in purchasedUpgrades) { - UpgradeManager.PurchaseUpgrade(prefab, category); + UpgradeManager.PurchaseUpgrade(prefab, category, client: sender); // unstable logging int price = prefab.Price.GetBuyprice(UpgradeManager.GetUpgradeLevel(prefab, category), Map?.CurrentLocation); @@ -828,7 +896,7 @@ namespace Barotrauma } else { - UpgradeManager.PurchaseItemSwap(purchasedItemSwap.ItemToRemove, purchasedItemSwap.ItemToInstall); + UpgradeManager.PurchaseItemSwap(purchasedItemSwap.ItemToRemove, purchasedItemSwap.ItemToInstall, client: sender); } } foreach (Item item in Item.ItemList) @@ -842,6 +910,64 @@ namespace Barotrauma } } + public void ServerReadMoney(IReadMessage msg, Client sender) + { + NetWalletTransfer transfer = INetSerializableStruct.Read(msg); + + switch (transfer.Sender) + { + case Some { Value: var id }: + + if (id != sender.CharacterID && !AllowedToManageCampaign(sender)) { return; } + + Wallet wallet = GetWalletByID(id); + if (wallet is InvalidWallet) { return; } + + TransferMoney(wallet); + break; + + case None _: + if (!AllowedToManageCampaign(sender)) { return; } + + TransferMoney(Bank); + break; + } + + void TransferMoney(Wallet from) + { + if (!from.TryDeduct(transfer.Amount)) { return; } + + switch (transfer.Receiver) + { + case Some { Value: var id }: + Wallet wallet = GetWalletByID(id); + if (wallet is InvalidWallet) { return; } + + wallet.Give(transfer.Amount); + break; + case None _: + Bank.Give(transfer.Amount); + break; + } + } + + Wallet GetWalletByID(ushort id) + { + Character targetCharacter = Character.CharacterList.FirstOrDefault(c => c.ID == id); + return targetCharacter is null ? Wallet.Invalid : targetCharacter.Wallet; + } + } + + public void ServerReadRewardDistribution(IReadMessage msg, Client sender) + { + NetWalletSalaryUpdate update = INetSerializableStruct.Read(msg); + + if (!AllowedToManageCampaign(sender)) { return; } + + Character targetCharacter = Character.CharacterList.FirstOrDefault(c => c.ID == update.Target); + targetCharacter?.Wallet.SetRewardDistrubiton(update.NewRewardDistribution); + } + public void ServerReadCrew(IReadMessage msg, Client sender) { int[] pendingHires = null; @@ -928,7 +1054,7 @@ namespace Barotrauma { foreach (CharacterInfo hireInfo in location.HireManager.PendingHires) { - if (TryHireCharacter(location, hireInfo)) + if (TryHireCharacter(location, hireInfo, sender)) { hiredCharacters.Add(hireInfo); }; @@ -1045,7 +1171,6 @@ namespace Barotrauma { element.Add(new XAttribute("campaignid", CampaignID)); XElement modeElement = new XElement("MultiPlayerCampaign", - new XAttribute("money", Money), new XAttribute("purchasedlostshuttles", PurchasedLostShuttles), new XAttribute("purchasedhullrepairs", PurchasedHullRepairs), new XAttribute("purchaseditemrepairs", PurchasedItemRepairs), @@ -1053,6 +1178,7 @@ namespace Barotrauma modeElement.Add(Settings.Save()); modeElement.Add(SaveStats()); + modeElement.Add(Bank.Save()); CampaignMetadata?.Save(modeElement); Map.Save(modeElement); CargoManager?.SavePurchasedItems(modeElement); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs index 3e2afe845..6f19e2077 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs @@ -85,7 +85,7 @@ namespace Barotrauma { if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } - HealRequestResult result = HealAllPending(); + HealRequestResult result = HealAllPending(client: client); ServerSend(new NetHealRequest { Result = result }, NetworkHeader.HEAL_PENDING, DeliveryMethod.Reliable, reponseClient: client); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs index eb8aa8e58..aa7abb66a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs @@ -5,7 +5,7 @@ namespace Barotrauma.Items.Components { partial class DockingPort : ItemComponent, IDrawableComponent, IServerSerializable { - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write(docked); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs index afb05dbc6..3edb8ff4e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs @@ -6,6 +6,16 @@ namespace Barotrauma.Items.Components { partial class Door { + private readonly struct EventData : IEventData + { + public readonly bool ForcedOpen; + + public EventData(bool forcedOpen) + { + ForcedOpen = forcedOpen; + } + } + partial void SetState(bool open, bool isNetworkMessage, bool sendNetworkMessage, bool forcedOpen) { if (IsStuck || isOpen == open) @@ -19,17 +29,18 @@ namespace Barotrauma.Items.Components if (sendNetworkMessage) { - GameMain.Server.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ComponentState, item.GetComponentIndex(this), forcedOpen }); + item.CreateServerEvent(this, new EventData(forcedOpen)); } } - public override void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public override void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - base.ServerWrite(msg, c, extraData); + bool forcedOpen = TryExtractEventData(extraData, out var eventData) && eventData.ForcedOpen; + base.ServerEventWrite(msg, c, extraData); msg.Write(isOpen); msg.Write(isBroken); - msg.Write(extraData.Length == 3 ? (bool)extraData[2] : false); //forced open + msg.Write(forcedOpen); //forced open msg.Write(isStuck); msg.Write(isJammed); msg.WriteRangedSingle(stuck, 0.0f, 100.0f, 8); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/GeneticMaterial.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/GeneticMaterial.cs index 6548e6046..ff82cc9d8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/GeneticMaterial.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/GeneticMaterial.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { partial class GeneticMaterial : ItemComponent { - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write(tainted); if (tainted) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Growable.cs index cac426158..8fd930fc8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Growable.cs @@ -6,6 +6,16 @@ namespace Barotrauma.Items.Components { internal partial class Growable { + private readonly struct EventData : IEventData + { + public readonly int Offset; + + public EventData(int offset) + { + Offset = offset; + } + } + private const int serverHealthUpdateDelay = 10; private int serverHealthUpdateTimer; @@ -25,11 +35,12 @@ namespace Barotrauma.Items.Components } } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.WriteRangedSingle(Health, 0f, (float) MaxHealth, 8); - if (extraData != null && extraData.Length >= 3 && extraData[2] is int offset) + if (TryExtractEventData(extraData, out EventData eventData)) { + int offset = eventData.Offset; int amountToSend = Math.Min(Vines.Count - offset, VineChunkSize); msg.WriteRangedInteger(offset, -1, MaximumVines); msg.WriteRangedInteger(amountToSend, 0, VineChunkSize); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs index a30115996..0b8521700 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs @@ -5,9 +5,9 @@ namespace Barotrauma.Items.Components { partial class Holdable : Pickable, IServerSerializable, IClientSerializable { - public override void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public override void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - base.ServerWrite(msg, c, extraData); + base.ServerEventWrite(msg, c, extraData); bool writeAttachData = attachable && body != null; msg.Write(writeAttachData); @@ -19,7 +19,7 @@ namespace Barotrauma.Items.Components msg.Write(item.Submarine?.ID ?? Entity.NullEntityID); } - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerEventRead(IReadMessage msg, Client c) { Vector2 simPosition = new Vector2(msg.ReadSingle(), msg.ReadSingle()); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/LevelResource.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/LevelResource.cs index a1f9c67df..3d372c206 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/LevelResource.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/LevelResource.cs @@ -6,7 +6,7 @@ namespace Barotrauma.Items.Components { private float lastSentDeattachTimer; - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write(deattachTimer); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemComponent.cs index 25d01e567..9ff312bcf 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemComponent.cs @@ -1,4 +1,6 @@ -using System.Xml.Linq; +using System; +using System.Xml.Linq; +using Barotrauma.Networking; namespace Barotrauma.Items.Components { @@ -18,7 +20,7 @@ namespace Barotrauma.Items.Components return true; //element processed } - public virtual void ServerAppendExtraData(ref object[] extraData) { } + public virtual IEventData ServerGetEventData() => null; } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs index 912920409..063f23d7a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs @@ -76,7 +76,7 @@ namespace Barotrauma.Items.Components yield return CoroutineStatus.Success; } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write(Text); lastSentText = Text; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/LightComponent.cs index 53d331b9c..4396628d9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/LightComponent.cs @@ -36,7 +36,7 @@ namespace Barotrauma.Items.Components yield return CoroutineStatus.Success; } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write(IsActive); lastSentState = IsActive; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Controller.cs index 5de5d4360..dc87c0b4a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Controller.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { partial class Controller : ItemComponent, IServerSerializable { - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write(State); msg.Write(user == null ? (ushort)0 : user.ID); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Deconstructor.cs index 420b4c685..9897bc414 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Deconstructor.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { partial class Deconstructor : Powered, IServerSerializable, IClientSerializable { - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerEventRead(IReadMessage msg, Client c) { bool active = msg.ReadBoolean(); @@ -16,7 +16,7 @@ namespace Barotrauma.Items.Components } } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write(user?.ID ?? 0); msg.Write(IsActive); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Engine.cs index 3be3b0dc5..a1f431279 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Engine.cs @@ -5,14 +5,14 @@ namespace Barotrauma.Items.Components { partial class Engine : Powered, IServerSerializable, IClientSerializable { - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { //force can only be adjusted at 10% intervals -> no need for more accuracy than this msg.WriteRangedInteger((int)(targetForce / 10.0f), -10, 10); msg.Write(User == null ? Entity.NullEntityID : User.ID); } - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerEventRead(IReadMessage msg, Client c) { float newTargetForce = msg.ReadRangedInteger(-10, 10) * 10.0f; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs index baddfb0d8..8ffcf9742 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs @@ -9,7 +9,7 @@ namespace Barotrauma.Items.Components { partial class Fabricator : Powered, IServerSerializable, IClientSerializable { - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerEventRead(IReadMessage msg, Client c) { uint recipeHash = msg.ReadUInt32(); @@ -32,21 +32,33 @@ namespace Barotrauma.Items.Components } private ulong serverEventId = 0; - public override void ServerAppendExtraData(ref object[] extraData) - { - //ensuring the uniqueness of this event is - //required for the fabricator to sync correctly; - //otherwise, the event manager would incorrectly - //assume that the client actually has the latest state - Array.Resize(ref extraData, 4); - extraData[2] = serverEventId; - extraData[3] = State; - } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + private readonly struct EventData : IEventData { - FabricatorState stateAtEvent = (FabricatorState)extraData[3]; - msg.Write((byte)stateAtEvent); + public readonly ulong ServerEventId; + public readonly FabricatorState State; + + public EventData(ulong serverEventId, FabricatorState state) + { + //ensuring the uniqueness of this event is + //required for the fabricator to sync correctly; + //otherwise, the event manager would incorrectly + //assume that the client actually has the latest state + ServerEventId = serverEventId; + State = state; + } + } + + public override IEventData ServerGetEventData() + => new EventData(serverEventId, State); + + public override bool ValidateEventData(NetEntityEvent.IData data) + => TryExtractEventData(data, out _); + + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) + { + var componentData = ExtractEventData(extraData); + msg.Write((byte)componentData.State); msg.Write(timeUntilReady); uint recipeHash = fabricatedItem?.RecipeHash ?? 0; msg.Write(recipeHash); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/OutpostTerminal.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/OutpostTerminal.cs index 5944a1be1..266398e7e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/OutpostTerminal.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/OutpostTerminal.cs @@ -4,12 +4,12 @@ namespace Barotrauma.Items.Components { partial class OutpostTerminal : ItemComponent, IClientSerializable, IServerSerializable { - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerEventRead(IReadMessage msg, Client c) { } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs index 74ba0b7ce..f2249351b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs @@ -8,7 +8,7 @@ namespace Barotrauma.Items.Components { partial class Pump : Powered, IServerSerializable, IClientSerializable { - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerEventRead(IReadMessage msg, Client c) { float newFlowPercentage = msg.ReadRangedInteger(-10, 10) * 10.0f; bool newIsActive = msg.ReadBoolean(); @@ -36,7 +36,7 @@ namespace Barotrauma.Items.Components item.CreateServerEvent(this); } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { //flowpercentage can only be adjusted at 10% intervals -> no need for more accuracy than this msg.WriteRangedInteger((int)(flowPercentage / 10.0f), -10, 10); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Reactor.cs index 61dbbd7e5..91f9f02ad 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Reactor.cs @@ -12,7 +12,7 @@ namespace Barotrauma.Items.Components private float? nextServerLogWriteTime; private float lastServerLogWriteTime; - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerEventRead(IReadMessage msg, Client c) { bool autoTemp = msg.ReadBoolean(); bool powerOn = msg.ReadBoolean(); @@ -43,7 +43,7 @@ namespace Barotrauma.Items.Components unsentChanges = true; } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write(autoTemp); msg.Write(_powerOn); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs index c26c7cde2..d7223e2c8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs @@ -5,6 +5,16 @@ namespace Barotrauma.Items.Components { partial class Steering : Powered, IServerSerializable, IClientSerializable { + private readonly struct EventData : IEventData + { + public readonly bool DockingButtonClicked; + + public EventData(bool dockingButtonClicked) + { + DockingButtonClicked = dockingButtonClicked; + } + } + // TODO: an enumeration would be much cleaner public bool MaintainPos; public bool LevelStartSelected; @@ -23,7 +33,7 @@ namespace Barotrauma.Items.Components } - public void ServerRead(ClientNetObject type, IReadMessage msg, Barotrauma.Networking.Client c) + public void ServerEventRead(IReadMessage msg, Client c) { bool autoPilot = msg.ReadBoolean(); bool dockingButtonClicked = msg.ReadBoolean(); @@ -58,7 +68,7 @@ namespace Barotrauma.Items.Components if (dockingButtonClicked) { item.SendSignal(new Signal("1", sender: c.Character), "toggle_docking"); - GameMain.Server.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ComponentState, item.GetComponentIndex(this), true }); + item.CreateServerEvent(this, new EventData(dockingButtonClicked: true)); } if (!AutoPilot) @@ -88,10 +98,10 @@ namespace Barotrauma.Items.Components unsentChanges = true; } - public void ServerWrite(IWriteMessage msg, Barotrauma.Networking.Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Barotrauma.Networking.Client c, NetEntityEvent.IData extraData = null) { msg.Write(autoPilot); - msg.Write(extraData.Length > 2 && extraData[2] is bool && (bool)extraData[2]); + msg.Write(TryExtractEventData(extraData, out var eventData) && eventData.DockingButtonClicked); if (!autoPilot) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Power/PowerContainer.cs index 20ae5b275..e0cf10812 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Power/PowerContainer.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Items.Components { partial class PowerContainer : Powered, IDrawableComponent, IServerSerializable, IClientSerializable { - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerEventRead(IReadMessage msg, Client c) { float newRechargeSpeed = msg.ReadRangedInteger(0, 10) / 10.0f * maxRechargeSpeed; @@ -20,7 +20,7 @@ namespace Barotrauma.Items.Components item.CreateServerEvent(this); } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.WriteRangedInteger((int)(rechargeSpeed / MaxRechargeSpeed * 10), 0, 10); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs index 7d2c4055f..2310120a0 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs @@ -5,11 +5,26 @@ namespace Barotrauma.Items.Components { partial class Projectile : ItemComponent { + private readonly struct EventData : IEventData + { + public readonly bool Launch; + + public EventData(bool launch) + { + Launch = launch; + } + } + private float launchRot; - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public override bool ValidateEventData(NetEntityEvent.IData data) + => TryExtractEventData(data, out _); + + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - bool launch = extraData.Length > 2 && (bool)extraData[2]; + var eventData = ExtractEventData(extraData); + bool launch = eventData.Launch; + msg.Write(launch); if (launch) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs index 673f53972..41d2f5c1d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs @@ -7,13 +7,13 @@ namespace Barotrauma.Items.Components private Character prevLoggedFixer; private FixActions prevLoggedFixAction; - partial void InitProjSpecific(ContentXElement _) + public override void OnMapLoaded() { //let the clients know the initial deterioration delay item.CreateServerEvent(this); } - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerEventRead(IReadMessage msg, Client c) { if (c.Character == null) { return; } var requestedFixAction = (FixActions)msg.ReadRangedInteger(0, 2); @@ -42,7 +42,7 @@ namespace Barotrauma.Items.Components } } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write(deteriorationTimer); msg.Write(deteriorateAlwaysResetTimer); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Rope.cs index e56702fc3..81b46fe2a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Rope.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { partial class Rope : ItemComponent { - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write(Snapped); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Scanner.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Scanner.cs index 337b721b0..2af1acfea 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Scanner.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Scanner.cs @@ -6,7 +6,7 @@ namespace Barotrauma.Items.Components { private float LastSentScanTimer { get; set; } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write(scanTimer); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ButtonTerminal.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ButtonTerminal.cs index 4cdc0ec1b..0dbd4486b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ButtonTerminal.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ButtonTerminal.cs @@ -4,16 +4,16 @@ namespace Barotrauma.Items.Components { partial class ButtonTerminal : ItemComponent, IClientSerializable, IServerSerializable { - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerEventRead(IReadMessage msg, Client c) { int signalIndex = msg.ReadRangedInteger(0, Signals.Length - 1); if (!item.CanClientAccess(c)) { return; } if (!SendSignal(signalIndex, c.Character)) { return; } GameServer.Log($"{GameServer.CharacterLogName(c.Character)} sent a signal \"{Signals[signalIndex]}\" from {item.Name}", ServerLog.MessageType.ItemInteraction); - item.CreateServerEvent(this, new object[] { signalIndex }); + item.CreateServerEvent(this, new EventData(signalIndex)); } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { Write(msg, extraData); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs index aa3466eb1..a92cc70d9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Items.Components { partial class ConnectionPanel : ItemComponent, IServerSerializable, IClientSerializable { - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerEventRead(IReadMessage msg, Client c) { List[] wires = new List[Connections.Count]; @@ -84,7 +84,7 @@ namespace Barotrauma.Items.Components if (!selectedWire.Item.Removed) { selectedWire.CreateNetworkEvent(); } }, 1.0f); } - GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFailure, this, c.Character.ID }); + GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnFailure, this, c.Character)); return; } @@ -210,10 +210,10 @@ namespace Barotrauma.Items.Components } } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write(user == null ? (ushort)0 : user.ID); - ClientWrite(msg, extraData); + ClientEventWrite(msg, extraData); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs index ae6f5b6ef..7c2bc102a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs @@ -5,7 +5,7 @@ namespace Barotrauma.Items.Components { partial class CustomInterface : ItemComponent, IClientSerializable, IServerSerializable { - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerEventRead(IReadMessage msg, Client c) { bool[] elementStates = new bool[customInterfaceElementList.Count]; string[] elementValues = new string[customInterfaceElementList.Count]; @@ -51,17 +51,12 @@ namespace Barotrauma.Items.Components } //notify all clients of the new state - GameMain.Server.CreateEntityEvent(item, new object[] - { - NetEntityEvent.Type.ComponentState, - item.GetComponentIndex(this), - clickedButton - }); + item.CreateServerEvent(this, new EventData(clickedButton)); item.CreateServerEvent(this); } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { //extradata contains an array of buttons clicked by a client (or nothing if nothing was clicked) for (int i = 0; i < customInterfaceElementList.Count; i++) @@ -76,7 +71,7 @@ namespace Barotrauma.Items.Components } else { - msg.Write(extraData != null && extraData.Any(d => d as CustomInterfaceElement == customInterfaceElementList[i])); + msg.Write(extraData is Item.ComponentStateEventData { ComponentData: EventData eventData } && eventData.BtnElement == customInterfaceElementList[i]); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/MemoryComponent.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/MemoryComponent.cs index 8a093a7d5..d99732838 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/MemoryComponent.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/MemoryComponent.cs @@ -36,7 +36,7 @@ namespace Barotrauma.Items.Components yield return CoroutineStatus.Success; } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write(Value); lastSentValue = Value; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs index cbd3638ca..9fdd21863 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs @@ -7,7 +7,19 @@ namespace Barotrauma.Items.Components { partial class Terminal : ItemComponent, IClientSerializable, IServerSerializable { - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + private readonly struct ServerEventData : IEventData + { + public readonly int MsgIndex; + public readonly string MsgToSend; + + public ServerEventData(int msgIndex, string msgToSend) + { + MsgIndex = msgIndex; + MsgToSend = msgToSend; + } + } + + public void ServerEventRead(IReadMessage msg, Client c) { string newOutputValue = msg.ReadString(); @@ -47,7 +59,7 @@ namespace Barotrauma.Items.Components string msgToSend = str; if (string.IsNullOrEmpty(msgToSend)) { - item.CreateServerEvent(this, new object[] { msgIndex, msgToSend }); + item.CreateServerEvent(this, new ServerEventData(msgIndex, msgToSend)); msgIndex++; continue; } @@ -73,23 +85,23 @@ namespace Barotrauma.Items.Components if (!splitMessage.Any()) { break; } tempMsg += " "; } while (tempMsg.Length + splitMessage[0].Length < MaxMessageLength); - item.CreateServerEvent(this, new object[] { msgIndex, tempMsg }); + item.CreateServerEvent(this, new ServerEventData(msgIndex, tempMsg)); msgToSend = msgToSend.Remove(0, tempMsg.Length); } } if (!string.IsNullOrEmpty(msgToSend)) { - item.CreateServerEvent(this, new object[] { msgIndex, msgToSend }); + item.CreateServerEvent(this, new ServerEventData(msgIndex, msgToSend)); } msgIndex++; } } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - if (extraData.Length > 3 && extraData[3] is string str) + if (TryExtractEventData(extraData, out ServerEventData eventData)) { - msg.Write(str); + msg.Write(eventData.MsgToSend); } else { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/WifiComponent.cs index 333028165..a42adf967 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/WifiComponent.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { partial class WifiComponent { - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.WriteRangedInteger(Channel, MinChannel, MaxChannel); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Wire.cs index 7586f86c0..51a0fa393 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Wire.cs @@ -6,6 +6,16 @@ namespace Barotrauma.Items.Components { partial class Wire : ItemComponent, IDrawableComponent, IServerSerializable { + private readonly struct ServerEventData : IEventData + { + public readonly int EventIndex; + + public ServerEventData(int eventIndex) + { + EventIndex = eventIndex; + } + } + public void CreateNetworkEvent() { if (GameMain.Server == null) return; @@ -13,13 +23,17 @@ namespace Barotrauma.Items.Components int eventCount = Math.Max((int)Math.Ceiling(nodes.Count / (float)MaxNodesPerNetworkEvent), 1); for (int i = 0; i < eventCount; i++) { - GameMain.Server.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ComponentState, item.GetComponentIndex(this), i }); + item.CreateServerEvent(this, new ServerEventData(i)); } } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public override bool ValidateEventData(NetEntityEvent.IData data) + => TryExtractEventData(data, out _); + + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - int eventIndex = (int)extraData[2]; + var eventData = ExtractEventData(extraData); + int eventIndex = eventData.EventIndex; int nodeStartIndex = eventIndex * MaxNodesPerNetworkEvent; int nodeCount = MathHelper.Clamp(nodes.Count - nodeStartIndex, 0, MaxNodesPerNetworkEvent); @@ -32,7 +46,7 @@ namespace Barotrauma.Items.Components } } - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerEventRead(IReadMessage msg, Client c) { int nodeCount = msg.ReadByte(); Vector2 lastNodePos = Vector2.Zero; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/TriggerComponent.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/TriggerComponent.cs index 2ebea696e..4526dd037 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/TriggerComponent.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/TriggerComponent.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { partial class TriggerComponent : ItemComponent, IServerSerializable { - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.WriteRangedSingle(CurrentForceFluctuation, 0.0f, 1.0f, 8); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs index ccf34fa50..14901a971 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs @@ -8,21 +8,11 @@ namespace Barotrauma { partial class Inventory : IServerSerializable, IClientSerializable { - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerEventRead(IReadMessage msg, Client c) { List prevItems = new List(AllItems.Distinct()); - byte slotCount = msg.ReadByte(); - List[] newItemIDs = new List[slotCount]; - for (int i = 0; i < slotCount; i++) - { - newItemIDs[i] = new List(); - int itemCount = msg.ReadRangedInteger(0, MaxStackSize); - for (int j = 0; j < itemCount; j++) - { - newItemIDs[i].Add(msg.ReadUInt16()); - } - } + SharedRead(msg, out var newItemIDs); if (c == null || c.Character == null) { return; } @@ -175,7 +165,7 @@ namespace Barotrauma } } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { SharedWrite(msg, extraData); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index e229c8ed9..8f1783707 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -20,103 +20,73 @@ namespace Barotrauma partial void AssignCampaignInteractionTypeProjSpecific(CampaignMode.InteractionType interactionType) { - GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.AssignCampaignInteraction }); + GameMain.NetworkMember.CreateEntityEvent(this, new AssignCampaignInteractionEventData()); } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - string errorMsg = ""; - if (extraData == null || extraData.Length == 0 || !(extraData[0] is NetEntityEvent.Type)) + Exception error(string reason) { - if (extraData == null) - { - errorMsg = "Failed to write a network event for the item \"" + Name + "\" - event data was null."; - } - else if (extraData.Length == 0) - { - errorMsg = "Failed to write a network event for the item \"" + Name + "\" - event data was empty."; - } - else - { - errorMsg = "Failed to write a network event for the item \"" + Name + "\" - event type not set."; - } - msg.WriteRangedInteger((int)NetEntityEvent.Type.Invalid, 0, Enum.GetValues(typeof(NetEntityEvent.Type)).Length - 1); - DebugConsole.Log(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("Item.ServerWrite:InvalidData" + Name, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); - return; + string errorMsg = $"Failed to write a network event for the item \"{Name}\" - {reason}"; + GameAnalyticsManager.AddErrorEventOnce($"Item.ServerWrite:{Name}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + return new Exception(errorMsg); } + + if (extraData is null) { throw error("event data was null"); } + if (!(extraData is IEventData itemEventData)) { throw error($"event data was of the wrong type (\"{extraData.GetType().Name}\")"); } - int initialWritePos = msg.LengthBits; - - NetEntityEvent.Type eventType = (NetEntityEvent.Type)extraData[0]; - msg.WriteRangedInteger((int)eventType, 0, Enum.GetValues(typeof(NetEntityEvent.Type)).Length - 1); - switch (eventType) + msg.WriteRangedInteger((int)itemEventData.EventType, (int)EventType.MinValue, (int)EventType.MaxValue); + switch (itemEventData) { - case NetEntityEvent.Type.ComponentState: - if (extraData.Length < 2 || !(extraData[1] is int)) + case ComponentStateEventData componentStateEventData: + int componentIndex = components.IndexOf(componentStateEventData.Component); + if (componentIndex < 0) { - errorMsg = "Failed to write a component state event for the item \"" + Name + "\" - component index not given."; - break; + throw error($"component index out of range ({componentIndex})"); } - int componentIndex = (int)extraData[1]; - if (componentIndex < 0 || componentIndex >= components.Count) + if (!(components[componentIndex] is IServerSerializable serializableComponent)) { - errorMsg = "Failed to write a component state event for the item \"" + Name + "\" - component index out of range (" + componentIndex + ")."; - break; - } - else if (!(components[componentIndex] is IServerSerializable)) - { - errorMsg = "Failed to write a component state event for the item \"" + Name + "\" - component \"" + components[componentIndex] + "\" is not server serializable."; - break; + throw error($"component \"{components[componentIndex]}\" is not server serializable"); } msg.WriteRangedInteger(componentIndex, 0, components.Count - 1); - (components[componentIndex] as IServerSerializable).ServerWrite(msg, c, extraData); + serializableComponent.ServerEventWrite(msg, c, extraData); break; - case NetEntityEvent.Type.InventoryState: - if (extraData.Length < 2 || !(extraData[1] is int)) + case InventoryStateEventData inventoryStateEventData: + int containerIndex = components.IndexOf(inventoryStateEventData.Component); + if (containerIndex < 0) { - errorMsg = "Failed to write an inventory state event for the item \"" + Name + "\" - component index not given."; + throw error($"container index out of range ({containerIndex})"); break; } - int containerIndex = (int)extraData[1]; - if (containerIndex < 0 || containerIndex >= components.Count) + if (!(components[containerIndex] is ItemContainer itemContainer)) { - errorMsg = "Failed to write an inventory state event for the item \"" + Name + "\" - container index out of range (" + containerIndex + ")."; - break; - } - else if (!(components[containerIndex] is ItemContainer)) - { - errorMsg = "Failed to write an inventory state event for the item \"" + Name + "\" - component \"" + components[containerIndex] + "\" is not server serializable."; - break; + throw error("component \"" + components[containerIndex] + "\" is not server serializable"); } msg.WriteRangedInteger(containerIndex, 0, components.Count - 1); msg.Write(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0); - (components[containerIndex] as ItemContainer).Inventory.ServerWrite(msg, c); + itemContainer.Inventory.ServerEventWrite(msg, c); break; - case NetEntityEvent.Type.Status: + case StatusEventData _: msg.Write(condition); break; - case NetEntityEvent.Type.AssignCampaignInteraction: + case AssignCampaignInteractionEventData _: msg.Write((byte)CampaignInteractionType); break; - case NetEntityEvent.Type.ApplyStatusEffect: + case ApplyStatusEffectEventData applyStatusEffectEventData: { - ActionType actionType = (ActionType)extraData[1]; - ItemComponent targetComponent = extraData.Length > 2 ? (ItemComponent)extraData[2] : null; - ushort characterID = extraData.Length > 3 ? (ushort)extraData[3] : (ushort)0; - Limb targetLimb = extraData.Length > 4 ? (Limb)extraData[4] : null; - ushort useTargetID = extraData.Length > 5 ? (ushort)extraData[5] : (ushort)0; - Vector2? worldPosition = null; - if (extraData.Length > 6) { worldPosition = (Vector2)extraData[6]; } + ActionType actionType = applyStatusEffectEventData.ActionType; + ItemComponent targetComponent = applyStatusEffectEventData.TargetItemComponent; + Limb targetLimb = applyStatusEffectEventData.TargetLimb; + Vector2? worldPosition = applyStatusEffectEventData.WorldPosition; - Character targetCharacter = FindEntityByID(characterID) as Character; + Character targetCharacter = applyStatusEffectEventData.TargetCharacter; byte targetLimbIndex = targetLimb != null && targetCharacter != null ? (byte)Array.IndexOf(targetCharacter.AnimController.Limbs, targetLimb) : (byte)255; msg.WriteRangedInteger((int)actionType, 0, Enum.GetValues(typeof(ActionType)).Length - 1); msg.Write((byte)(targetComponent == null ? 255 : components.IndexOf(targetComponent))); - msg.Write(characterID); + msg.Write(applyStatusEffectEventData.TargetCharacter?.ID ?? (ushort)0); msg.Write(targetLimbIndex); - msg.Write(useTargetID); + msg.Write(applyStatusEffectEventData.UseTarget?.ID ?? (ushort)0); msg.Write(worldPosition.HasValue); if (worldPosition.HasValue) { @@ -125,74 +95,56 @@ namespace Barotrauma } } break; - case NetEntityEvent.Type.ChangeProperty: + case ChangePropertyEventData changePropertyEventData: try { - WritePropertyChange(msg, extraData, inGameEditableOnly: !GameMain.NetworkMember.IsServer); + WritePropertyChange(msg, changePropertyEventData, inGameEditableOnly: !GameMain.NetworkMember.IsServer); } catch (Exception e) { - errorMsg = "Failed to write a ChangeProperty network event for the item \"" + Name + "\" (" + e.Message + ")"; + throw new Exception( + $"Failed to write a ChangeProperty network event for the item \"{Name}\" ({e.Message})"); } break; - case NetEntityEvent.Type.Upgrade: - if (extraData.Length > 0 && extraData[1] is Upgrade upgrade) + case UpgradeEventData upgradeEventData: + var upgrade = upgradeEventData.Upgrade; + var upgradeTargets = upgrade.TargetComponents; + msg.Write(upgrade.Identifier); + msg.Write((byte)upgrade.Level); + msg.Write((byte)upgradeTargets.Count); + foreach (var (_, value) in upgrade.TargetComponents) { - var upgradeTargets = upgrade.TargetComponents; - msg.Write(upgrade.Identifier); - msg.Write((byte)upgrade.Level); - msg.Write((byte)upgradeTargets.Count); - foreach (var (_, value) in upgrade.TargetComponents) + msg.Write((byte)value.Length); + foreach (var propertyReference in value) { - msg.Write((byte)value.Length); - foreach (var propertyReference in value) - { - object originalValue = propertyReference.OriginalValue; - msg.Write((float)(originalValue ?? -1)); - } + object originalValue = propertyReference.OriginalValue; + msg.Write((float)(originalValue ?? -1)); } } - else - { - errorMsg = extraData.Length > 0 - ? $"Failed to write a network event for the item \"{Name}\" - \"{extraData[1].GetType()}\" is not a valid upgrade." - : $"Failed to write a network event for the item \"{Name}\". No upgrade specified."; - } break; default: - errorMsg = "Failed to write a network event for the item \"" + Name + "\" - \"" + eventType + "\" is not a valid entity event type for items."; - break; - } - - if (!string.IsNullOrEmpty(errorMsg)) - { - //something went wrong - rewind the write position and write invalid event type to prevent creating an unreadable event - msg.BitPosition = initialWritePos; - msg.LengthBits = initialWritePos; - msg.WriteRangedInteger((int)NetEntityEvent.Type.Invalid, 0, Enum.GetValues(typeof(NetEntityEvent.Type)).Length - 1); - DebugConsole.Log(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("Item.ServerWrite:" + errorMsg, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + throw error($"Unsupported event type {itemEventData.GetType().Name}"); } } - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerEventRead(IReadMessage msg, Client c) { - NetEntityEvent.Type eventType = - (NetEntityEvent.Type)msg.ReadRangedInteger(0, Enum.GetValues(typeof(NetEntityEvent.Type)).Length - 1); + EventType eventType = + (EventType)msg.ReadRangedInteger((int)EventType.MinValue, (int)EventType.MaxValue); c.KickAFKTimer = 0.0f; switch (eventType) { - case NetEntityEvent.Type.ComponentState: + case EventType.ComponentState: int componentIndex = msg.ReadRangedInteger(0, components.Count - 1); - (components[componentIndex] as IClientSerializable).ServerRead(type, msg, c); + (components[componentIndex] as IClientSerializable).ServerEventRead(msg, c); break; - case NetEntityEvent.Type.InventoryState: + case EventType.InventoryState: int containerIndex = msg.ReadRangedInteger(0, components.Count - 1); - (components[containerIndex] as ItemContainer).Inventory.ServerRead(type, msg, c); + (components[containerIndex] as ItemContainer).Inventory.ServerEventRead(msg, c); break; - case NetEntityEvent.Type.Treatment: + case EventType.Treatment: if (c.Character == null || !c.Character.CanInteractWith(this)) return; UInt16 characterID = msg.ReadUInt16(); @@ -218,10 +170,10 @@ namespace Barotrauma ApplyTreatment(c.Character, targetCharacter, targetLimb); break; - case NetEntityEvent.Type.ChangeProperty: + case EventType.ChangeProperty: ReadPropertyChange(msg, inGameEditableOnly: GameMain.NetworkMember.IsServer, sender: c); break; - case NetEntityEvent.Type.Combine: + case EventType.Combine: UInt16 combineTargetID = msg.ReadUInt16(); Item combineTarget = FindEntityByID(combineTargetID) as Item; if (combineTarget == null || !c.Character.CanInteractWith(this) || !c.Character.CanInteractWith(combineTarget)) @@ -269,6 +221,7 @@ namespace Barotrauma msg.WriteRangedInteger(Quality, 0, Items.Components.Quality.MaxQuality); byte teamID = 0; + IdCard idCardComponent = null; foreach (WifiComponent wifiComponent in GetComponents()) { teamID = (byte)wifiComponent.TeamID; @@ -279,11 +232,31 @@ namespace Barotrauma foreach (IdCard idCard in GetComponents()) { teamID = (byte)idCard.TeamID; + idCardComponent = idCard; break; } } msg.Write(teamID); + + bool hasIdCard = idCardComponent != null; + msg.Write(hasIdCard); + if (hasIdCard) + { + msg.Write(idCardComponent.OwnerName); + msg.Write(idCardComponent.OwnerTags); + msg.Write((byte)Math.Max(0, idCardComponent.OwnerBeardIndex+1)); + msg.Write((byte)Math.Max(0, idCardComponent.OwnerHairIndex+1)); + msg.Write((byte)Math.Max(0, idCardComponent.OwnerMoustacheIndex+1)); + msg.Write((byte)Math.Max(0, idCardComponent.OwnerFaceAttachmentIndex+1)); + msg.WriteColorR8G8B8(idCardComponent.OwnerHairColor); + msg.WriteColorR8G8B8(idCardComponent.OwnerFacialHairColor); + msg.WriteColorR8G8B8(idCardComponent.OwnerSkinColor); + msg.Write(idCardComponent.OwnerJobId); + msg.Write((byte)idCardComponent.OwnerSheetIndex.X); + msg.Write((byte)idCardComponent.OwnerSheetIndex.Y); + } + bool tagsChanged = tags.Count != base.Prefab.Tags.Count || !tags.All(t => base.Prefab.Tags.Contains(t)); msg.Write(tagsChanged); if (tagsChanged) @@ -367,39 +340,21 @@ namespace Barotrauma } } - public void ServerWritePosition(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerWritePosition(IWriteMessage msg, Client c) { msg.Write(ID); IWriteMessage tempBuffer = new WriteOnlyMessage(); - body.ServerWrite(tempBuffer, c, extraData); + body.ServerWrite(tempBuffer); msg.WriteVariableUInt32((uint)tempBuffer.LengthBytes); msg.Write(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); msg.WritePadBits(); } public void CreateServerEvent(T ic) where T : ItemComponent, IServerSerializable - { - if (GameMain.Server == null) { return; } + => CreateServerEvent(ic, ic.ServerGetEventData()); - if (!ItemList.Contains(this)) - { - string errorMsg = "Attempted to create a network event for an item (" + Name + ") that hasn't been fully initialized yet.\n" + Environment.StackTrace.CleanupStackTrace(); - DebugConsole.AddWarning(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("Item.CreateServerEvent:EventForUninitializedItem" + Name + ID, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); - return; - } - - int index = components.IndexOf(ic); - if (index == -1) { return; } - - object[] extraData = new object[] { NetEntityEvent.Type.ComponentState, index }; - ic.ServerAppendExtraData(ref extraData); - - GameMain.Server.CreateEntityEvent(this, extraData); - } - - public void CreateServerEvent(T ic, object[] extraData) where T : ItemComponent, IServerSerializable + public void CreateServerEvent(T ic, ItemComponent.IEventData extraData) where T : ItemComponent, IServerSerializable { if (GameMain.Server == null) { return; } @@ -411,11 +366,12 @@ namespace Barotrauma return; } - int index = components.IndexOf(ic); - if (index == -1) { return; } + #warning TODO: this should throw an exception + if (!components.Contains(ic)) { return; } - object[] data = new object[] { NetEntityEvent.Type.ComponentState, index }.Concat(extraData).ToArray(); - GameMain.Server.CreateEntityEvent(this, data); + var eventData = new ComponentStateEventData(ic, extraData); + if (!ic.ValidateEventData(eventData)) { throw new Exception($"Component event creation failed: {typeof(T).Name}.{nameof(ItemComponent.ValidateEventData)} returned false"); } + GameMain.Server.CreateEntityEvent(this, eventData); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Levels/Level.cs b/Barotrauma/BarotraumaServer/ServerSource/Levels/Level.cs new file mode 100644 index 000000000..cdbd0c952 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Levels/Level.cs @@ -0,0 +1,58 @@ +using System; +using Barotrauma.Networking; +using FarseerPhysics; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + partial class Level : Entity, IServerSerializable + { + public interface IEventData : NetEntityEvent.IData + { + public EventType EventType { get; } + } + + public readonly struct SingleLevelWallEventData : IEventData + { + public EventType EventType => EventType.SingleDestructibleWall; + public readonly DestructibleLevelWall Wall; + + public SingleLevelWallEventData(DestructibleLevelWall wall) + { + Wall = wall; + } + } + + public readonly struct GlobalLevelWallEventData : IEventData + { + public EventType EventType => EventType.GlobalDestructibleWall; + } + + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) + { + if (!(extraData is IEventData eventData)) { throw new Exception($"Malformed level event: expected {nameof(Level)}.{nameof(IEventData)}"); } + + msg.Write((byte)eventData.EventType); + switch (eventData) + { + case SingleLevelWallEventData { Wall: var destructibleWall }: + int index = ExtraWalls.IndexOf(destructibleWall); + msg.Write((ushort)(index == -1 ? ushort.MaxValue : index)); + //write health using one byte + msg.Write((byte)MathHelper.Clamp((int)(MathUtils.InverseLerp(0.0f, destructibleWall.MaxHealth, destructibleWall.Damage) * 255.0f), 0, 255)); + break; + case GlobalLevelWallEventData _: + foreach (LevelWall levelWall in ExtraWalls) + { + if (levelWall.Body.BodyType == BodyType.Static) { continue; } + msg.Write(levelWall.Body.Position.X); + msg.Write(levelWall.Body.Position.Y); + msg.WriteRangedSingle(levelWall.MoveState, 0.0f, MathHelper.TwoPi, 16); + } + break; + default: + throw new Exception($"Malformed level event: did not expect {eventData.GetType().Name}"); + } + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs index 65beaf5ba..0707bd526 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs @@ -31,32 +31,43 @@ namespace Barotrauma.MapCreatures.Behavior } } - - partial void UpdateDamage(float deltaTime) + public void ServerWrite(IWriteMessage msg, IEventData eventData) { - if (damageUpdateTimer <= 0) + msg.Write((byte)eventData.NetworkHeader); + + switch (eventData) { - foreach (BallastFloraBranch branch in Branches) - { - if (Math.Abs(branch.AccumulatedDamage) > 1.0f) - { - SendNetworkMessage(this, NetworkHeader.BranchDamage, branch); - branch.AccumulatedDamage = 0f; - } - } - damageUpdateTimer = 1f; + case SpawnEventData spawnEventData: + ServerWriteSpawn(msg); + break; + case KillEventData killEventData: + //do nothing + break; + case BranchCreateEventData branchCreateEventData: + ServerWriteBranchGrowth(msg, branchCreateEventData.NewBranch, branchCreateEventData.Parent.ID); + break; + case BranchDamageEventData branchDamageEventData: + ServerWriteBranchDamage(msg, branchDamageEventData.Branch); + break; + case InfectEventData infectEventData: + ServerWriteInfect(msg, infectEventData.Item.ID, infectEventData.Infect, infectEventData.Infector); + break; + case BranchRemoveEventData branchRemoveEventData: + ServerWriteBranchRemove(msg, branchRemoveEventData.Branch); + break; } - damageUpdateTimer -= deltaTime; + + msg.Write(PowerConsumptionTimer); } - public void ServerWriteSpawn(IWriteMessage msg) + private void ServerWriteSpawn(IWriteMessage msg) { msg.Write(Prefab.Identifier); msg.Write(Offset.X); msg.Write(Offset.Y); } - public void ServerWriteBranchGrowth(IWriteMessage msg, BallastFloraBranch branch, int parentId = -1) + private void ServerWriteBranchGrowth(IWriteMessage msg, BallastFloraBranch branch, int parentId = -1) { var (x, y) = branch.Position; msg.Write(parentId); @@ -71,30 +82,30 @@ namespace Barotrauma.MapCreatures.Behavior msg.Write(branch.ParentBranch == null ? -1 : Branches.IndexOf(branch.ParentBranch)); } - public void ServerWriteBranchDamage(IWriteMessage msg, BallastFloraBranch branch) + private void ServerWriteBranchDamage(IWriteMessage msg, BallastFloraBranch branch) { msg.Write((int)branch.ID); msg.Write(branch.Health); } - public void ServerWriteInfect(IWriteMessage msg, UInt16 itemID, bool infect, BallastFloraBranch infector = null) + private void ServerWriteInfect(IWriteMessage msg, UInt16 itemID, InfectEventData.InfectState infect, BallastFloraBranch infector = null) { msg.Write(itemID); - msg.Write(infect); - if (infect) + msg.Write(infect == InfectEventData.InfectState.Yes); + if (infect == InfectEventData.InfectState.Yes) { msg.Write(infector?.ID ?? -1); } } - public void ServerWriteBranchRemove(IWriteMessage msg, BallastFloraBranch branch) + private void ServerWriteBranchRemove(IWriteMessage msg, BallastFloraBranch branch) { msg.Write(branch.ID); } - public void SendNetworkMessage(params object[] extraData) + public void SendNetworkMessage(IEventData extraData) { - GameMain.Server.CreateEntityEvent(Parent, extraData); + GameMain.Server.CreateEntityEvent(Parent, new Hull.BallastFloraEventData(this, extraData)); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs index 861494dc6..024b68518 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs @@ -4,14 +4,20 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; +using Barotrauma.Extensions; using Barotrauma.MapCreatures.Behavior; namespace Barotrauma { partial class Hull : MapEntity, ISerializableEntity, IServerSerializable, IClientSerializable { - private float lastSentVolume, lastSentOxygen, lastSentFireCount; - private float sendUpdateTimer; + private float lastSentVolume; + private float lastSentOxygen; + private int lastSentFireCount; + + private float statusUpdateTimer; + private float decalUpdateTimer; + private float backgroundSectionUpdateTimer; private bool decalUpdatePending; @@ -33,225 +39,163 @@ namespace Barotrauma return; } - sendUpdateTimer -= deltaTime; + statusUpdateTimer -= deltaTime; + decalUpdateTimer -= deltaTime; + backgroundSectionUpdateTimer -= deltaTime; + //update client hulls if the amount of water has changed by >10% //or if oxygen percentage has changed by 5% - if (Math.Abs(lastSentVolume - waterVolume) > Volume * 0.1f || Math.Abs(lastSentOxygen - OxygenPercentage) > 5f || - lastSentFireCount != FireSources.Count || FireSources.Count > 0 || - pendingSectionUpdates.Count > 0 || - sendUpdateTimer < -NetConfig.SparseHullUpdateInterval || - decalUpdatePending) - { - if (sendUpdateTimer < 0.0f) - { - if (decalUpdatePending) - { - GameMain.NetworkMember.CreateEntityEvent(this, new object[] { false }); - } - if (pendingSectionUpdates.Count > 0) - { - foreach (int pendingSectionUpdate in pendingSectionUpdates) - { - GameMain.NetworkMember.CreateEntityEvent(this, new object[] { true, pendingSectionUpdate } ); - } - pendingSectionUpdates.Clear(); - } - else - { - GameMain.NetworkMember.CreateEntityEvent(this); - } + bool shouldSendStatusUpdate = + (Math.Abs(lastSentVolume - waterVolume) > Volume * 0.1f + || Math.Abs(lastSentOxygen - OxygenPercentage) > 5f + || lastSentFireCount != FireSources.Count) + && statusUpdateTimer <= 0.0f; - lastSentVolume = waterVolume; - lastSentOxygen = OxygenPercentage; - lastSentFireCount = FireSources.Count; - sendUpdateTimer = NetConfig.HullUpdateInterval; + if (shouldSendStatusUpdate) + { + GameMain.NetworkMember.CreateEntityEvent(this, new StatusEventData()); + + lastSentVolume = waterVolume; + lastSentOxygen = OxygenPercentage; + lastSentFireCount = FireSources.Count; + + statusUpdateTimer = NetConfig.SparseHullUpdateInterval; + } + if (decalUpdatePending && decalUpdateTimer <= 0.0f) + { + GameMain.NetworkMember.CreateEntityEvent(this, new DecalEventData()); + + decalUpdateTimer = NetConfig.HullUpdateInterval; + decalUpdatePending = false; + } + if (pendingSectionUpdates.Count > 0 && backgroundSectionUpdateTimer <= 0.0f) + { + foreach (int pendingSectionUpdate in pendingSectionUpdates) + { + GameMain.NetworkMember.CreateEntityEvent(this, new BackgroundSectionsEventData(pendingSectionUpdate)); } + + backgroundSectionUpdateTimer = NetConfig.HullUpdateInterval; + pendingSectionUpdates.Clear(); } } - public void ServerWrite(IWriteMessage message, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - if (extraData != null && extraData.Length >= 2 && extraData[0] is BallastFloraBehavior behavior && extraData[1] is BallastFloraBehavior.NetworkHeader header) + if (!(extraData is IEventData eventData)) { throw new Exception($"Malformed hull event: expected {nameof(Hull)}.{nameof(IEventData)}"); } + msg.WriteRangedInteger((int)eventData.EventType, (int)EventType.MinValue, (int)EventType.MaxValue); + + switch (eventData) { - message.Write(true); - message.Write((byte)header); - - switch (header) - { - case BallastFloraBehavior.NetworkHeader.Spawn: - behavior.ServerWriteSpawn(message); - break; - case BallastFloraBehavior.NetworkHeader.Kill: - case BallastFloraBehavior.NetworkHeader.Remove: - break; - case BallastFloraBehavior.NetworkHeader.BranchCreate when extraData.Length >= 4 && extraData[2] is BallastFloraBranch branch && extraData[3] is int parentId: - behavior.ServerWriteBranchGrowth(message, branch, parentId); - break; - case BallastFloraBehavior.NetworkHeader.BranchDamage when extraData.Length >= 4 && extraData[2] is BallastFloraBranch branch: - behavior.ServerWriteBranchDamage(message, branch); - break; - case BallastFloraBehavior.NetworkHeader.BranchRemove when extraData.Length >= 3 && extraData[2] is BallastFloraBranch branch: - behavior.ServerWriteBranchRemove(message, branch); - break; - case BallastFloraBehavior.NetworkHeader.Infect when extraData.Length >= 4 && extraData[2] is UInt16 itemID && extraData[3] is bool infect: - BallastFloraBranch infector = null; - if (extraData.Length >= 5 && extraData[4] is BallastFloraBranch b) { infector = b; } - behavior.ServerWriteInfect(message, itemID, infect, infector); - break; - } - - message.Write(behavior.PowerConsumptionTimer); - return; - } - - message.Write(false); //not a ballast flora update - message.WriteRangedSingle(MathHelper.Clamp(waterVolume / Volume, 0.0f, 1.5f), 0.0f, 1.5f, 8); - message.WriteRangedSingle(MathHelper.Clamp(OxygenPercentage, 0.0f, 100.0f), 0.0f, 100.0f, 8); - - message.Write(FireSources.Count > 0); - if (FireSources.Count > 0) - { - message.WriteRangedInteger(Math.Min(FireSources.Count, 16), 0, 16); - for (int i = 0; i < Math.Min(FireSources.Count, 16); i++) - { - var fireSource = FireSources[i]; - Vector2 normalizedPos = new Vector2( - (fireSource.Position.X - rect.X) / rect.Width, - (fireSource.Position.Y - (rect.Y - rect.Height)) / rect.Height); - - message.WriteRangedSingle(MathHelper.Clamp(normalizedPos.X, 0.0f, 1.0f), 0.0f, 1.0f, 8); - message.WriteRangedSingle(MathHelper.Clamp(normalizedPos.Y, 0.0f, 1.0f), 0.0f, 1.0f, 8); - message.WriteRangedSingle(MathHelper.Clamp(fireSource.Size.X / rect.Width, 0.0f, 1.0f), 0, 1.0f, 8); - } - } - - message.Write(extraData != null); - if (extraData != null) - { - message.Write((bool)extraData[0]); - - // Section update - if ((bool)extraData[0]) - { - int sectorToUpdate = (int)extraData[1]; - int start = sectorToUpdate * BackgroundSectionsPerNetworkEvent; - int end = Math.Min((sectorToUpdate + 1) * BackgroundSectionsPerNetworkEvent, BackgroundSections.Count - 1); - message.WriteRangedInteger(sectorToUpdate, 0, BackgroundSections.Count - 1); - for (int i = start; i < end; i++) - { - message.WriteRangedSingle(BackgroundSections[i].ColorStrength, 0.0f, 1.0f, 8); - message.Write(BackgroundSections[i].Color.PackedValue); - } - } - else // Decal update - { - message.WriteRangedInteger(decals.Count, 0, MaxDecalsPerHull); + case StatusEventData statusEventData: + msg.WriteRangedSingle(MathHelper.Clamp(OxygenPercentage, 0.0f, 100.0f), 0.0f, 100.0f, 8); + SharedStatusWrite(msg); + break; + case BackgroundSectionsEventData backgroundSectionsEventData: + SharedBackgroundSectionsWrite(msg, backgroundSectionsEventData); + break; + case DecalEventData decalEventData: + msg.WriteRangedInteger(decals.Count, 0, MaxDecalsPerHull); foreach (Decal decal in decals) { - message.Write(decal.Prefab.UintIdentifier); - message.Write((byte)decal.SpriteIndex); + msg.Write(decal.Prefab.UintIdentifier); + msg.Write((byte)decal.SpriteIndex); float normalizedXPos = MathHelper.Clamp(MathUtils.InverseLerp(0.0f, rect.Width, decal.CenterPosition.X), 0.0f, 1.0f); float normalizedYPos = MathHelper.Clamp(MathUtils.InverseLerp(-rect.Height, 0.0f, decal.CenterPosition.Y), 0.0f, 1.0f); - message.WriteRangedSingle(normalizedXPos, 0.0f, 1.0f, 8); - message.WriteRangedSingle(normalizedYPos, 0.0f, 1.0f, 8); - message.WriteRangedSingle(decal.Scale, 0f, 2f, 12); + msg.WriteRangedSingle(normalizedXPos, 0.0f, 1.0f, 8); + msg.WriteRangedSingle(normalizedYPos, 0.0f, 1.0f, 8); + msg.WriteRangedSingle(decal.Scale, 0f, 2f, 12); } - } + break; + case BallastFloraEventData ballastFloraEventData: + ballastFloraEventData.Behavior.ServerWrite(msg, ballastFloraEventData.SubEventData); + break; + default: + throw new Exception($"Malformed hull event: did not expect {eventData.GetType().Name}"); } } //used when clients use the water/fire console commands or section / decal updates are received - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerEventRead(IReadMessage msg, Client c) { - int messageType = msg.ReadRangedInteger(0, 2); - if (messageType == 0) + EventType eventType = (EventType)msg.ReadRangedInteger((int)EventType.MinValue, (int)EventType.MaxValue); + switch (eventType) { - float newWaterVolume = msg.ReadRangedSingle(0.0f, 1.5f, 8) * Volume; + case EventType.Status: + SharedStatusRead( + msg, + out float newWaterVolume, + out NetworkFireSource[] newFireSources); - bool hasFireSources = msg.ReadBoolean(); - int fireSourceCount = 0; - List newFireSources = new List(); - if (hasFireSources) - { - fireSourceCount = msg.ReadRangedInteger(0, 16); - for (int i = 0; i < fireSourceCount; i++) + if (!c.HasPermission(ClientPermissions.ConsoleCommands) || + !c.PermittedConsoleCommands.Any(command => command.names.Contains("fire") || command.names.Contains("editfire"))) { - newFireSources.Add(new Vector3( - MathHelper.Clamp(msg.ReadRangedSingle(0.0f, 1.0f, 8), 0.05f, 0.95f), - MathHelper.Clamp(msg.ReadRangedSingle(0.0f, 1.0f, 8), 0.05f, 0.95f), - msg.ReadRangedSingle(0.0f, 1.0f, 8))); + return; } - } - if (!c.HasPermission(ClientPermissions.ConsoleCommands) || - !c.PermittedConsoleCommands.Any(command => command.names.Contains("fire") || command.names.Contains("editfire"))) - { - return; - } + WaterVolume = newWaterVolume; - WaterVolume = newWaterVolume; - - for (int i = 0; i < fireSourceCount; i++) - { - Vector2 pos = new Vector2( - rect.X + rect.Width * newFireSources[i].X, - rect.Y - rect.Height + (rect.Height * newFireSources[i].Y)); - float size = newFireSources[i].Z * rect.Width; - - var newFire = i < FireSources.Count ? - FireSources[i] : - new FireSource(Submarine == null ? pos : pos + Submarine.Position, null, true); - newFire.Position = pos; - newFire.Size = new Vector2(size, newFire.Size.Y); - - //ignore if the fire wasn't added to this room (invalid position)? - if (!FireSources.Contains(newFire)) + for (int i = 0; i < newFireSources.Length; i++) { - newFire.Remove(); - continue; - } - } + Vector2 pos = newFireSources[i].Position; + float size = newFireSources[i].Size; - for (int i = FireSources.Count - 1; i >= fireSourceCount; i--) - { - FireSources[i].Remove(); - if (i < FireSources.Count) + var newFire = i < FireSources.Count ? + FireSources[i] : + new FireSource(Submarine == null ? pos : pos + Submarine.Position, null, true); + newFire.Position = pos; + newFire.Size = new Vector2(size, newFire.Size.Y); + + //ignore if the fire wasn't added to this room (invalid position)? + if (!FireSources.Contains(newFire)) + { + newFire.Remove(); + continue; + } + } + + for (int i = FireSources.Count - 1; i >= newFireSources.Length; i--) { - FireSources.RemoveAt(i); + FireSources[i].Remove(); + if (i < FireSources.Count) + { + FireSources.RemoveAt(i); + } } - } - } - else if (messageType == 1) - { - byte decalIndex = msg.ReadByte(); - float decalAlpha = msg.ReadRangedSingle(0.0f, 1.0f, 255); - if (decalIndex < 0 || decalIndex >= decals.Count) { return; } - if (c.Character != null && c.Character.AllowInput && c.Character.HeldItems.Any(it => it.GetComponent() != null)) - { - decals[decalIndex].BaseAlpha = decalAlpha; - } - decalUpdatePending = true; - } - else - { - int sectorToUpdate = msg.ReadRangedInteger(0, BackgroundSections.Count - 1); - int start = sectorToUpdate * BackgroundSectionsPerNetworkEvent; - int end = Math.Min((sectorToUpdate + 1) * BackgroundSectionsPerNetworkEvent, BackgroundSections.Count - 1); - for (int i = start; i < end; i++) - { - float colorStrength = msg.ReadRangedSingle(0.0f, 1.0f, 8); - Color color = new Color(msg.ReadUInt32()); + break; + case EventType.BackgroundSections: + SharedBackgroundSectionRead( + msg, + bsnu => + { + int i = bsnu.SectionIndex; + Color color = bsnu.Color; + float colorStrength = bsnu.ColorStrength; - //TODO: verify the client is close enough to this hull to paint it, that the sprayer is functional and that the color matches + #warning TODO: verify the client is close enough to this hull to paint it, that the sprayer is functional and that the color matches + if (!(c.Character is { AllowInput: true })) { return; } + if (c.Character.HeldItems.All(it => it.GetComponent() == null)) { return; } + + BackgroundSections[i].SetColorStrength(colorStrength); + BackgroundSections[i].SetColor(color); + }, + out int sectorToUpdate); + //add to pending updates to notify other clients as well + pendingSectionUpdates.Add(sectorToUpdate); + break; + case EventType.Decal: + byte decalIndex = msg.ReadByte(); + float decalAlpha = msg.ReadRangedSingle(0.0f, 1.0f, 255); + if (decalIndex < 0 || decalIndex >= decals.Count) { return; } if (c.Character != null && c.Character.AllowInput && c.Character.HeldItems.Any(it => it.GetComponent() != null)) { - BackgroundSections[i].SetColorStrength(colorStrength); - BackgroundSections[i].SetColor(color); + decals[decalIndex].BaseAlpha = decalAlpha; } - } - //add to pending updates to notify other clients as well - pendingSectionUpdates.Add(sectorToUpdate); - } + decalUpdatePending = true; + break; + default: + throw new Exception($"Malformed incoming hull event: {eventType} is not a supported event type"); + } } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Structure.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Structure.cs index a9cfdc9f7..1f6f58360 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Structure.cs @@ -9,7 +9,7 @@ namespace Barotrauma GameMain.Server.KarmaManager.OnStructureHealthChanged(this, attacker, damageAmount); } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write((byte)Sections.Length); for (int i = 0; i < Sections.Length; i++) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Submarine.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Submarine.cs index 4acc60be1..9f634bd1d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Submarine.cs @@ -1,17 +1,23 @@ -using Barotrauma.Networking; +using System; +using Barotrauma.Networking; namespace Barotrauma { partial class Submarine { - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerWritePosition(IWriteMessage msg, Client c) { msg.Write(ID); IWriteMessage tempBuffer = new WriteOnlyMessage(); - subBody.Body.ServerWrite(tempBuffer, c, extraData); + subBody.Body.ServerWrite(tempBuffer); msg.Write((byte)tempBuffer.LengthBytes); msg.Write(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); msg.WritePadBits(); } + + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) + { + throw new Exception($"Error while writing a network event for the submarine \"{Info.Name} ({ID})\". Submarines are not even supposed to send events!"); + } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs index edd4690ca..9d64d3876 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs @@ -1,5 +1,4 @@ -using Microsoft.Xna.Framework; -using System; +using System; using System.Text; namespace Barotrauma.Networking @@ -17,10 +16,10 @@ namespace Barotrauma.Networking Character orderTargetCharacter = null; Entity orderTargetEntity = null; OrderChatMessage orderMsg = null; - OrderTarget orderTargetPosition = null; Order.OrderTargetType orderTargetType = Order.OrderTargetType.Entity; int? wallSectionIndex = null; Order order = null; + bool isNewOrder = false; if (type == ChatMessageType.Order) { var orderMessageInfo = OrderChatMessage.ReadOrder(msg); @@ -30,9 +29,10 @@ namespace Barotrauma.Networking if (NetIdUtils.IdMoreRecent(ID, c.LastSentChatMsgID)) { c.LastSentChatMsgID = ID; } return; } + isNewOrder = orderMessageInfo.IsNewOrder; orderTargetCharacter = orderMessageInfo.TargetCharacter; orderTargetEntity = orderMessageInfo.TargetEntity; - orderTargetPosition = orderMessageInfo.TargetPosition; + OrderTarget orderTargetPosition = orderMessageInfo.TargetPosition; orderTargetType = orderMessageInfo.TargetType; wallSectionIndex = orderMessageInfo.WallSectionIndex; var orderPrefab = orderMessageInfo.OrderPrefab ?? OrderPrefab.Prefabs[orderMessageInfo.OrderIdentifier]; @@ -165,7 +165,7 @@ namespace Barotrauma.Networking } else if (orderTargetCharacter != null) { - orderTargetCharacter.SetOrder(order); + orderTargetCharacter.SetOrder(order, isNewOrder); } } GameMain.Server.SendOrderChatMessage(orderMsg); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChildServerRelay.cs index 584342950..64e1ced26 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChildServerRelay.cs @@ -1,4 +1,7 @@ -using System.IO.Pipes; +using System; +using System.IO.Pipes; +using System.Text; +using System.Threading; namespace Barotrauma.Networking { @@ -14,6 +17,12 @@ namespace Barotrauma.Networking PrivateStart(); } + public static void NotifyCrash(string msg) + { + errorsToWrite.Enqueue(msg); + Thread.Sleep(1000); + } + public static void ShutDown() { PrivateShutDown(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/EntitySpawner.cs index fc56ade32..266600615 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/EntitySpawner.cs @@ -1,52 +1,55 @@ -using Barotrauma.Networking; +using System; +using Barotrauma.Networking; namespace Barotrauma { partial class EntitySpawner : Entity, IServerSerializable { - public void CreateNetworkEvent(Entity entity, bool remove) + public void CreateNetworkEvent(SpawnOrRemove spawnOrRemove) { - CreateNetworkEventProjSpecific(entity, remove); + CreateNetworkEventProjSpecific(spawnOrRemove); } - partial void CreateNetworkEventProjSpecific(Entity entity, bool remove) + partial void CreateNetworkEventProjSpecific(SpawnOrRemove spawnOrRemove) { - if (GameMain.Server == null || entity == null) { return; } - - GameMain.Server.CreateEntityEvent(this, new object[] { new SpawnOrRemove(entity, remove) }); - if (entity is Character character && character.Info != null) + if (GameMain.Server == null || spawnOrRemove?.Entity == null) { return; } + + GameMain.Server.CreateEntityEvent(this, spawnOrRemove); + if (spawnOrRemove.Entity is Character { Info: { } } character) { foreach (var statKey in character.Info.SavedStatValues.Keys) { - GameMain.NetworkMember.CreateEntityEvent(character, new object[] { NetEntityEvent.Type.UpdatePermanentStats, statKey }); - } - } + GameMain.NetworkMember.CreateEntityEvent(character, new Character.UpdatePermanentStatsEventData(statKey)); + } + } } - public void ServerWrite(IWriteMessage message, Client client, object[] extraData = null) + public void ServerEventWrite(IWriteMessage message, Client client, NetEntityEvent.IData extraData = null) { - if (GameMain.Server == null) { return; } + if (GameMain.Server is null) { return; } + if (!(extraData is SpawnOrRemove entities)) { throw new Exception($"Malformed {nameof(EntitySpawner)} event: expected {nameof(SpawnOrRemove)}"); } - SpawnOrRemove entities = (SpawnOrRemove)extraData[0]; - - message.Write(entities.Remove); - if (entities.Remove) + message.Write(entities is RemoveEntity); + if (entities is RemoveEntity) { - message.Write(entities.OriginalID); + message.Write(entities.ID); } else { - if (entities.Entity is Item item) + switch (entities.Entity) { - message.Write((byte)SpawnableType.Item); - DebugConsole.Log("Writing item spawn data " + entities.Entity.ToString() + " (original ID: " + entities.OriginalID + ", current ID: " + entities.Entity.ID + ")"); - item.WriteSpawnData(message, entities.OriginalID, entities.OriginalInventoryID, entities.OriginalItemContainerIndex, entities.OriginalSlotIndex); - } - else if (entities.Entity is Character character) - { - message.Write((byte)SpawnableType.Character); - DebugConsole.Log("Writing character spawn data: " + entities.Entity.ToString() + " (original ID: " + entities.OriginalID + ", current ID: " + entities.Entity.ID + ")"); - character.WriteSpawnData(message, entities.OriginalID, restrictMessageSize: true); + case Item item: + message.Write((byte)SpawnableType.Item); + DebugConsole.Log( + $"Writing item spawn data {item} (ID: {entities.ID})"); + item.WriteSpawnData(message, entities.ID, entities.InventoryID, entities.ItemContainerIndex, entities.SlotIndex); + break; + case Character character: + message.Write((byte)SpawnableType.Character); + DebugConsole.Log( + $"Writing character spawn data: {character} (ID: {entities.ID})"); + character.WriteSpawnData(message, entities.ID, restrictMessageSize: true); + break; } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs index 9d32305d1..7a8d742eb 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs @@ -20,7 +20,7 @@ namespace Barotrauma.Networking "ModSender", Task.WhenAll( ContentPackageManager.EnabledPackages.All - .Where(p => p != ContentPackageManager.VanillaCorePackage && p.HasMultiplayerIncompatibleContent) + .Where(p => p != ContentPackageManager.VanillaCorePackage && p.HasMultiplayerSyncedContent) .Select(CompressMod)), (t) => Ready = true); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index ebd958713..804c11d40 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -814,6 +814,12 @@ namespace Barotrauma.Networking case ClientPacketHeader.CREW: ReadCrewMessage(inc, connectedClient); break; + case ClientPacketHeader.MONEY: + ReadMoneyMessage(inc, connectedClient); + break; + case ClientPacketHeader.REWARD_DISTRIBUTION: + ReadRewardDistributionMessage(inc, connectedClient); + break; case ClientPacketHeader.MEDICAL: ReadMedicalMessage(inc, connectedClient); break; @@ -977,12 +983,12 @@ namespace Barotrauma.Networking { if (entityEvent.Entity is EntitySpawner) { - var spawnData = entityEvent.Data[0] as EntitySpawner.SpawnOrRemove; + var spawnData = entityEvent.Data as EntitySpawner.SpawnOrRemove; errorLines.Add( entityEvent.ID + ": " + - (spawnData.Remove ? "Remove " : "Create ") + + (spawnData is EntitySpawner.RemoveEntity ? "Remove " : "Create ") + spawnData.Entity.ToString() + - " (" + spawnData.OriginalID + ", " + spawnData.Entity.ID + ")"); + " (" + spawnData.ID + ", " + spawnData.Entity.ID + ")"); } } @@ -996,7 +1002,7 @@ namespace Barotrauma.Networking File.WriteAllLines(filePath, errorLines); } - public override void CreateEntityEvent(INetSerializable entity, object[] extraData = null) + public override void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData = null) { if (!(entity is IServerSerializable serverSerializable)) { @@ -1203,7 +1209,7 @@ namespace Barotrauma.Networking case ClientNetObject.CHARACTER_INPUT: if (c.Character != null) { - c.Character.ServerRead(objHeader, inc, c); + c.Character.ServerReadInput(inc, c); } else { @@ -1246,6 +1252,22 @@ namespace Barotrauma.Networking } } + private void ReadMoneyMessage(IReadMessage inc, Client sender) + { + if (GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign) + { + mpCampaign.ServerReadMoney(inc, sender); + } + } + + private void ReadRewardDistributionMessage(IReadMessage inc, Client sender) + { + if (GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign) + { + mpCampaign.ServerReadRewardDistribution(inc, sender); + } + } + private void ReadMedicalMessage(IReadMessage inc, Client sender) { if (GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign) @@ -1714,7 +1736,8 @@ namespace Barotrauma.Networking while (!c.NeedsMidRoundSync && c.PendingPositionUpdates.Count > 0) { var entity = c.PendingPositionUpdates.Peek(); - if (entity == null || entity.Removed || + if (!(entity is IServerPositionSync entityPositionSync) || + entity.Removed || (entity is Item item && float.IsInfinity(item.PositionUpdateInterval))) { c.PendingPositionUpdates.Dequeue(); @@ -1724,14 +1747,7 @@ namespace Barotrauma.Networking IWriteMessage tempBuffer = new ReadWriteMessage(); tempBuffer.Write(entity is Item); tempBuffer.WritePadBits(); tempBuffer.Write(entity is MapEntity me ? me.Prefab.UintIdentifier : (UInt32)0); - if (entity is Item) - { - ((Item)entity).ServerWritePosition(tempBuffer, c); - } - else - { - ((IServerSerializable)entity).ServerWrite(tempBuffer, c); - } + entityPositionSync.ServerWritePosition(tempBuffer, c); //no more room in this packet if (outmsg.LengthBytes + tempBuffer.LengthBytes > MsgConstants.MTU - 100) @@ -1879,7 +1895,7 @@ namespace Barotrauma.Networking } outmsg.Write(GameMain.NetLobbyScreen.SelectedSub.Name); outmsg.Write(GameMain.NetLobbyScreen.SelectedSub.MD5Hash.ToString()); - outmsg.Write(serverSettings.UseRespawnShuttle || (gameStarted && respawnManager.UsingShuttle)); + outmsg.Write(IsUsingRespawnShuttle()); var selectedShuttle = gameStarted && respawnManager.UsingShuttle ? respawnManager.RespawnShuttle.Info : GameMain.NetLobbyScreen.SelectedShuttle; outmsg.Write(selectedShuttle.Name); outmsg.Write(selectedShuttle.MD5Hash.ToString()); @@ -2061,7 +2077,7 @@ namespace Barotrauma.Networking msg.Write(selectedSub.Name); msg.Write(selectedSub.MD5Hash.StringRepresentation); - msg.Write(serverSettings.UseRespawnShuttle || (gameStarted && respawnManager.UsingShuttle)); + msg.Write(IsUsingRespawnShuttle()); msg.Write(selectedShuttle.Name); msg.Write(selectedShuttle.MD5Hash.StringRepresentation); @@ -2218,8 +2234,6 @@ namespace Barotrauma.Networking CrewManager crewManager = campaign?.CrewManager; - entityEventManager.RefreshEntityIDs(); - bool hadBots = true; //assign jobs and spawnpoints separately for each team @@ -2366,6 +2380,7 @@ namespace Barotrauma.Networking } characterData.ApplyHealthData(spawnedCharacter); characterData.ApplyOrderData(spawnedCharacter); + characterData.ApplyWalletData(spawnedCharacter); spawnedCharacter.GiveIdCardTags(mainSubWaypoints[i]); spawnedCharacter.LoadTalents(); @@ -2419,7 +2434,7 @@ namespace Barotrauma.Networking List spawnList = new List(); foreach (KeyValuePair kvp in serverSettings.ExtraCargo) { - spawnList.Add(new PurchasedItem(kvp.Key, kvp.Value)); + spawnList.Add(new PurchasedItem(kvp.Key, kvp.Value, buyer: null)); } CargoManager.CreateItems(spawnList, sub); @@ -2486,7 +2501,7 @@ namespace Barotrauma.Networking msg.Write(serverSettings.LockAllDefaultWires); msg.Write(serverSettings.AllowRagdollButton); msg.Write(serverSettings.AllowLinkingWifiToChat); - msg.Write(serverSettings.UseRespawnShuttle || (gameStarted && respawnManager.UsingShuttle)); + msg.Write(IsUsingRespawnShuttle()); msg.Write((byte)serverSettings.LosMode); msg.Write(includesFinalize); msg.WritePadBits(); @@ -2498,7 +2513,8 @@ namespace Barotrauma.Networking msg.Write(serverSettings.SelectedLevelDifficulty); msg.Write(gameSession.SubmarineInfo.Name); msg.Write(gameSession.SubmarineInfo.MD5Hash.StringRepresentation); - var selectedShuttle = gameStarted && respawnManager.UsingShuttle ? respawnManager.RespawnShuttle.Info : GameMain.NetLobbyScreen.SelectedShuttle; + var selectedShuttle = gameStarted && respawnManager != null && respawnManager.UsingShuttle ? + respawnManager.RespawnShuttle.Info : GameMain.NetLobbyScreen.SelectedShuttle; msg.Write(selectedShuttle.Name); msg.Write(selectedShuttle.MD5Hash.StringRepresentation); msg.Write((byte)GameMain.GameSession.GameMode.Missions.Count()); @@ -2527,6 +2543,11 @@ namespace Barotrauma.Networking serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } + private bool IsUsingRespawnShuttle() + { + return serverSettings.UseRespawnShuttle || (gameStarted && respawnManager != null && respawnManager.UsingShuttle); + } + private void SendRoundStartFinalize(Client client) { IWriteMessage msg = new WriteOnlyMessage(); @@ -3286,6 +3307,7 @@ namespace Barotrauma.Networking { SubmarineInfo targetSubmarine = Voting.SubVote.Sub; VoteType voteType = Voting.SubVote.VoteType; + Client starter = Voting.SubVote.VoteStarter; int deliveryFee = 0; switch (voteType) @@ -3293,7 +3315,7 @@ namespace Barotrauma.Networking case VoteType.PurchaseAndSwitchSub: case VoteType.PurchaseSub: // Pay for submarine - GameMain.GameSession.PurchaseSubmarine(targetSubmarine); + GameMain.GameSession.PurchaseSubmarine(targetSubmarine, starter); break; case VoteType.SwitchSub: deliveryFee = Voting.SubVote.DeliveryFee; @@ -3304,7 +3326,7 @@ namespace Barotrauma.Networking if (voteType != VoteType.PurchaseSub) { - SubmarineInfo newSub = GameMain.GameSession.SwitchSubmarine(targetSubmarine, deliveryFee); + SubmarineInfo newSub = GameMain.GameSession.SwitchSubmarine(targetSubmarine, deliveryFee, starter); } serverSettings.Voting.StopSubmarineVote(true); @@ -3461,7 +3483,7 @@ namespace Barotrauma.Networking { if (client.Character != null) //removing control of the current character { - CreateEntityEvent(client.Character, new object[] { NetEntityEvent.Type.Control, null }); + CreateEntityEvent(client.Character, new Character.ControlEventData(null)); client.Character = null; } } @@ -3485,7 +3507,7 @@ namespace Barotrauma.Networking newCharacter.IsRemotePlayer = true; newCharacter.Enabled = true; client.Character = newCharacter; - CreateEntityEvent(newCharacter, new object[] { NetEntityEvent.Type.Control, client }); + CreateEntityEvent(newCharacter, new Character.ControlEventData(client)); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs index b11c6bcf4..e587e882e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs @@ -38,7 +38,7 @@ namespace Barotrauma.Networking public void Write(IWriteMessage msg, Client recipient) { - serializable.ServerWrite(msg, recipient, Data); + serializable.ServerEventWrite(msg, recipient, Data); } } @@ -111,7 +111,7 @@ namespace Barotrauma.Networking lastWarningTime = -10.0; } - public void CreateEvent(IServerSerializable entity, object[] extraData = null) + public void CreateEvent(IServerSerializable entity, NetEntityEvent.IData extraData = null) { if (!ValidateEntity(entity)) { return; } @@ -291,12 +291,6 @@ namespace Barotrauma.Networking bufferedEvents.Add(bufferedEvent); } - public void RefreshEntityIDs() - { - events.ForEach(e => e.RefreshEntityID()); - uniqueEvents.ForEach(e => e.RefreshEntityID()); - } - /// /// Writes all the events that the client hasn't received yet into the outgoing message /// @@ -310,15 +304,7 @@ namespace Barotrauma.Networking /// public void Write(Client client, IWriteMessage msg, out List sentEvents) { - List eventsToSync = null; - if (client.NeedsMidRoundSync) - { - eventsToSync = GetEventsToSync(client); - } - else - { - eventsToSync = GetEventsToSync(client); - } + List eventsToSync = GetEventsToSync(client); if (eventsToSync.Count == 0) { @@ -460,6 +446,7 @@ namespace Barotrauma.Networking /// public void Read(IReadMessage msg, Client sender = null) { + msg.ReadPadBits(); UInt16 firstEventID = msg.ReadUInt16(); int eventCount = msg.ReadByte(); @@ -470,7 +457,6 @@ namespace Barotrauma.Networking if (entityID == Entity.NullEntityID) { - msg.ReadPadBits(); if (thisEventID == (UInt16)(sender.LastSentEntityEventID + 1)) sender.LastSentEntityEventID++; continue; } @@ -490,7 +476,7 @@ namespace Barotrauma.Networking } else if (entity == null) { - //entity not found -> consider the even read and skip over it + //entity not found -> consider the event read and skip over it //(can happen, for example, when a client uses a medical item repeatedly //and creates an event for it before receiving the event about it being removed) if (GameSettings.CurrentConfig.VerboseLogging) @@ -519,7 +505,6 @@ namespace Barotrauma.Networking sender.LastSentEntityEventID++; } - msg.ReadPadBits(); } } @@ -536,7 +521,7 @@ namespace Barotrauma.Networking var clientEntity = entity as IClientSerializable; if (clientEntity == null) return; - clientEntity.ServerRead(ClientNetObject.ENTITY_STATE, buffer, sender); + clientEntity.ServerEventRead(buffer, sender); } public void Clear() diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs index 1e2d3e6d5..bdd1fc790 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs @@ -246,7 +246,7 @@ namespace Barotrauma.Networking case ConnectionInitialization.ContentPackageOrder: outMsg.Write(GameMain.Server.ServerName); - var mpContentPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerIncompatibleContent).ToList(); + var mpContentPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent).ToList(); outMsg.WriteVariableUInt32((UInt32)mpContentPackages.Count); for (int i = 0; i < mpContentPackages.Count; i++) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index 38252b58f..429d8b164 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -447,11 +447,11 @@ namespace Barotrauma.Networking if (divingSuitPrefab != null && oxyPrefab != null) { var divingSuit = new Item(divingSuitPrefab, pos, respawnSub); - Spawner.CreateNetworkEvent(divingSuit, false); + Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(divingSuit)); respawnItems.Add(divingSuit); var oxyTank = new Item(oxyPrefab, pos, respawnSub); - Spawner.CreateNetworkEvent(oxyTank, false); + Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(oxyTank)); divingSuit.Combine(oxyTank, user: null); respawnItems.Add(oxyTank); } @@ -459,10 +459,10 @@ namespace Barotrauma.Networking if (scooterPrefab != null && batteryPrefab != null) { var scooter = new Item(scooterPrefab, pos, respawnSub); - Spawner.CreateNetworkEvent(scooter, false); + Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(scooter)); var battery = new Item(batteryPrefab, pos, respawnSub); - Spawner.CreateNetworkEvent(battery, false); + Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(battery)); scooter.Combine(battery, user: null); respawnItems.Add(scooter); @@ -528,7 +528,7 @@ namespace Barotrauma.Networking } } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.WriteRangedInteger((int)CurrentState, 0, Enum.GetNames(typeof(State)).Length); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs index 290833c32..f6d9cf671 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs @@ -131,7 +131,7 @@ namespace Barotrauma { string subName = inc.ReadString(); SubmarineInfo subInfo = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName); - if (GameMain.GameSession?.Campaign is MultiPlayerCampaign campaign && (campaign.CanPurchaseSub(subInfo) || GameMain.GameSession.IsSubmarineOwned(subInfo))) + if (GameMain.GameSession?.Campaign is MultiPlayerCampaign campaign && (campaign.CanPurchaseSub(subInfo, sender) || GameMain.GameSession.IsSubmarineOwned(subInfo))) { StartSubmarineVote(subInfo, voteType, sender); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaServer/ServerSource/Physics/PhysicsBody.cs index 561192ab6..0b03fe680 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Physics/PhysicsBody.cs @@ -6,7 +6,7 @@ namespace Barotrauma { partial class PhysicsBody { - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerWrite(IWriteMessage msg) { float MaxVel = NetConfig.MaxPhysicsBodyVelocity; float MaxAngularVel = NetConfig.MaxPhysicsBodyAngularVelocity; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Program.cs b/Barotrauma/BarotraumaServer/ServerSource/Program.cs index 0712c25b6..c2fccb428 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Program.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Program.cs @@ -5,6 +5,7 @@ using System; using Barotrauma.IO; using System.Linq; using System.Text; +using Barotrauma.Networking; #if LINUX using System.Runtime.InteropServices; #endif @@ -26,6 +27,20 @@ namespace Barotrauma private static extern void setLinuxEnv(); #endif + public static bool TryStartChildServerRelay(string[] commandLineArgs) + { + for (int i = 0; i < commandLineArgs.Length; i++) + { + switch (commandLineArgs[i].Trim()) + { + case "-pipes": + ChildServerRelay.Start(commandLineArgs[i + 2], commandLineArgs[i + 1]); + return true; + } + } + return false; + } + /// /// The main entry point for the application. /// @@ -36,6 +51,7 @@ namespace Barotrauma AppDomain currentDomain = AppDomain.CurrentDomain; currentDomain.UnhandledException += new UnhandledExceptionEventHandler(CrashHandler); #endif + TryStartChildServerRelay(args); #if LINUX setLinuxEnv(); @@ -62,22 +78,49 @@ namespace Barotrauma static GameMain Game; + private static void NotifyCrash(string reportFilePath, Exception e) + { + string errorMsg = $"{reportFilePath}||\n{e.Message} ({e.GetType().Name}) {e.StackTrace}"; + if (e.InnerException != null) + { + var innerMost = e.GetInnermost(); + errorMsg += $"\nInner exception: {innerMost.Message} ({innerMost.GetType().Name}) {e.StackTrace}"; + } + if (errorMsg.Length > ushort.MaxValue) { errorMsg = errorMsg[..ushort.MaxValue]; } + ChildServerRelay.NotifyCrash(errorMsg); + GameMain.Server?.NotifyCrash(); + } + private static void CrashHandler(object sender, UnhandledExceptionEventArgs args) { + void swallowExceptions(Action action) + { + try + { + action(); + } + catch + { + //discard exceptions and keep going + } + } + + string reportFilePath = ""; try { - Game?.Exit(); - CrashDump("servercrashreport.log", (Exception)args.ExceptionObject); - GameMain.Server?.NotifyCrash(); + reportFilePath = "servercrashreport.log"; + CrashDump(ref reportFilePath, (Exception)args.ExceptionObject); } catch { - //exception handler is broken, we have a serious problem here!! - return; + //fuck + reportFilePath = ""; } + swallowExceptions(() => NotifyCrash(reportFilePath, (Exception)args.ExceptionObject)); + swallowExceptions(() => Game?.Exit()); } - static void CrashDump(string filePath, Exception exception) + static void CrashDump(ref string filePath, Exception exception) { try { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs index 1b2c09f73..bd622322a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs @@ -42,7 +42,7 @@ namespace Barotrauma.Steam return false; } - var contentPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerIncompatibleContent); + var contentPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent); // These server state variables may be changed at any time. Note that there is no longer a mechanism // to send the player count. The player count is maintained by Steam and you should use the player diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs index ece1c941b..715ef5a3a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs @@ -16,7 +16,7 @@ namespace Barotrauma Role = role; Character = character; Character.IsTraitor = true; - GameMain.NetworkMember.CreateEntityEvent(Character, new object[] { NetEntityEvent.Type.Status }); + GameMain.NetworkMember.CreateEntityEvent(Character, new Character.StatusEventData()); } public delegate void MessageSender(string message); diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 299d02f04..ecbbeb3e3 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.0.0 + 0.17.1.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 14c4333de..48ef25dce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -388,7 +388,7 @@ namespace Barotrauma break; } } - if (targetingTag == null) + if (targetingTag.IsNullOrEmpty()) { if (targetItem.GetComponent() != null) { @@ -2100,15 +2100,11 @@ namespace Barotrauma if (!ActiveAttack.IsRunning) { #if SERVER - GameMain.NetworkMember.CreateEntityEvent(Character, new object[] - { - Networking.NetEntityEvent.Type.SetAttackTarget, + GameMain.NetworkMember.CreateEntityEvent(Character, new Character.SetAttackTargetEventData( attackingLimb, - (damageTarget as Entity)?.ID ?? Entity.NullEntityID, - damageTarget is Character character && targetLimb != null ? Array.IndexOf(character.AnimController.Limbs, targetLimb) : 0, - SimPosition.X, - SimPosition.Y - }); + damageTarget, + targetLimb, + SimPosition)); #else Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3); #endif @@ -2696,7 +2692,7 @@ namespace Barotrauma float target = targetParams.Threshold; if (targetParams.ThresholdMin > 0 && targetParams.ThresholdMax > 0) { - target = selectedTargetingParams == targetParams ? targetParams.ThresholdMax : targetParams.ThresholdMin; + target = selectedTargetingParams == targetParams && State == AIState.FleeTo ? targetParams.ThresholdMax : targetParams.ThresholdMin; } if (Character.HealthPercentage > target) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 79879e835..e3f0db038 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -569,7 +569,8 @@ namespace Barotrauma (Character.Submarine.TeamID != Character.TeamID && !Character.IsEscorted) || ObjectiveManager.CurrentOrders.Any(o => o.Objective.KeepDivingGearOnAlsoWhenInactive) || ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn) || - Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10; + Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10 || + Character.CurrentHull.IsWetRoom; bool IsOrderedToWait() => Character.IsOnPlayerTeam && ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character; bool removeDivingSuit = !shouldKeepTheGearOn && !IsOrderedToWait(); if (oxygenLow && Character.CurrentHull.Oxygen > 0 && (!isCurrentObjectiveFindSafety || Character.OxygenAvailable < 1)) @@ -1265,15 +1266,15 @@ namespace Barotrauma { if (!IsFriendly(attacker)) { - if (Character.Submarine == null) + if (c.Submarine == null) { // Outside return attacker.Submarine == null ? AIObjectiveCombat.CombatMode.Defensive : AIObjectiveCombat.CombatMode.Retreat; } - if (!Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine)) + if (!c.Submarine.GetConnectedSubs().Contains(attacker.Submarine)) { // Attacked from an unconnected submarine. - return Character.SelectedConstruction?.GetComponent() != null ? AIObjectiveCombat.CombatMode.None : AIObjectiveCombat.CombatMode.Retreat; + return c.SelectedConstruction?.GetComponent() != null ? AIObjectiveCombat.CombatMode.None : AIObjectiveCombat.CombatMode.Retreat; } return c.AIController is HumanAIController humanAI && (humanAI.ObjectiveManager.IsCurrentOrder() || humanAI.ObjectiveManager.Objectives.Any(o => o is AIObjectiveFightIntruders)) @@ -1285,18 +1286,22 @@ namespace Barotrauma { cumulativeDamage = 100; } - if (GameMain.IsSingleplayer && attacker.IsPlayer && Character.TeamID == attacker.TeamID) + if (attacker.IsPlayer && c.TeamID == attacker.TeamID) { - // Bots in the player team never act aggressively in single player when attacked by the player - return cumulativeDamage > minorDamageThreshold ? AIObjectiveCombat.CombatMode.Retreat : AIObjectiveCombat.CombatMode.None; + if (GameMain.IsSingleplayer || Character.TeamID != attacker.TeamID) + { + // Bots in the player team never act aggressively in single player when attacked by the player + // In multiplayer, they react only to players attacking them or other crew members + return Character == c && cumulativeDamage > minorDamageThreshold ? AIObjectiveCombat.CombatMode.Retreat : AIObjectiveCombat.CombatMode.None; + } } - if (Character.Submarine == null || !Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine)) + if (c.Submarine == null || !c.Submarine.GetConnectedSubs().Contains(attacker.Submarine)) { // Outside or attacked from an unconnected submarine -> don't react. return AIObjectiveCombat.CombatMode.None; } // If there are any enemies around, just ignore the friendly fire - if (Character.CharacterList.Any(ch => ch.Submarine == Character.Submarine && !ch.Removed && !ch.IsIncapacitated && !IsFriendly(ch) && VisibleHulls.Contains(ch.CurrentHull))) + if (Character.CharacterList.Any(ch => ch.Submarine == c.Submarine && !ch.Removed && !ch.IsIncapacitated && !IsFriendly(ch) && VisibleHulls.Contains(ch.CurrentHull))) { isAttackerFightingEnemy = true; return AIObjectiveCombat.CombatMode.None; @@ -1352,18 +1357,19 @@ namespace Barotrauma Character FindInstigator() { - if (Character.IsInstigator) + if (attacker.IsInstigator) { - return Character; + return attacker; } - else if (c.AIController is HumanAIController humanAi) + if (c.IsInstigator) + { + return c; + } + if (c.AIController is HumanAIController humanAi) { return Character.CharacterList.FirstOrDefault(ch => ch.Submarine == c.Submarine && !ch.Removed && !ch.IsIncapacitated && ch.IsInstigator && humanAi.VisibleHulls.Contains(ch.CurrentHull)); } - else - { - return null; - } + return null; } } } @@ -1497,7 +1503,7 @@ namespace Barotrauma if (hull == null || hull.WaterPercentage > 90 || hull.LethalPressure > 0 || - hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.5f)) + hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.9f)) { needsSuit = !Character.HasAbilityFlag(AbilityFlags.ImmuneToPressure); return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs index b76ebfaa3..e3e8af4cd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs @@ -30,6 +30,7 @@ namespace Barotrauma if (!character.Submarine.IsConnectedTo(item.Submarine)) { return false; } } if (item.ConditionPercentage <= 0) { return false; } + if (item.IsClaimedByBallastFlora) { return false; } if (Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; } if (IsReady(battery)) { return false; } return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs index 79732e006..c647b82d0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs @@ -83,7 +83,8 @@ namespace Barotrauma container.HasTag("allowcleanup") && container.ParentInventory == null && container.OwnInventory != null && container.OwnInventory.AllItems.Any() && container.GetComponent() != null && - IsItemInsideValidSubmarine(container, character); + IsItemInsideValidSubmarine(container, character) && + !container.IsClaimedByBallastFlora; public static bool IsValidTarget(Item item, Character character, bool checkInventory, bool allowUnloading = true) { @@ -100,6 +101,7 @@ namespace Barotrauma if (!IsValidContainer(item.Container, character, allowUnloading)) { return false; } } if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; } + if (item.HasBallastFloraInHull) { return false; } var pickable = item.GetComponent(); if (pickable == null) { return false; } if (pickable is Holdable h && h.Attachable && h.Attached) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index 4f89fe0ad..f93ededde 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -777,7 +777,7 @@ namespace Barotrauma } if (retreatTarget != null && character.CurrentHull != retreatTarget) { - TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager, false, true) + TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager) { UsePathingOutside = false }, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index 9e9a1be4f..4dae997fb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -162,7 +162,6 @@ namespace Barotrauma CloseEnough = reach, DialogueIdentifier = Leak.FlowTargetHull != null ? "dialogcannotreachleak".ToIdentifier() : Identifier.Empty, TargetName = Leak.FlowTargetHull?.DisplayName, - CheckVisibility = false, requiredCondition = () => Leak.Submarine == character.Submarine, // The Go To objective can be abandoned if the leak is fixed (in which case we don't want to use the dialogue) SpeakCannotReachCondition = () => !CheckObjectiveSpecific() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index 2a1062948..4bf65a912 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -1,7 +1,6 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using Barotrauma.Extensions; @@ -11,6 +10,8 @@ namespace Barotrauma { public override Identifier Identifier { get; set; } = "go to".ToIdentifier(); + public override bool KeepDivingGearOn => GetTargetHull() == null; + private AIObjectiveFindDivingGear findDivingGear; private readonly bool repeat; //how long until the path to the target is declared unreachable @@ -74,14 +75,6 @@ namespace Barotrauma _closeEnough = Math.Max(minDistance, value); } } - - // TODO: Currently we never check the visibility (to the end node), which is actually unintentional. - // I don't think it has caused any issues so far, so let's keep defaulting to false for now, because the less we do raycasts the better. - // However, if there are cases where the bots attempt to go through walls (select the end node that is behind an obstacle), we should set this true. - - // NOTE: This seemes to have caused an issue now Regalis11/Barotrauma#8067: namely, the bot was trying to use a waypoint that was obstructed by a shuttle - // because obstruction was only checked when checking visibility in PathFinder. Changed that so that obstructed nodes are no longer used. - public bool CheckVisibility { get; set; } public bool IgnoreIfTargetDead { get; set; } public bool AllowGoingOutside { get; set; } @@ -268,15 +261,15 @@ namespace Barotrauma { Character followTarget = Target as Character; bool needsDivingSuit = (!isInside || hasOutdoorNodes) && character.NeedsAir && !character.HasAbilityFlag(AbilityFlags.ImmuneToPressure); - bool needsDivingGear = (needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit)) && character.NeedsAir; + bool needsDivingGear = (needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit)); if (Mimic) { - if (HumanAIController.HasDivingSuit(followTarget) && character.NeedsAir) + if (HumanAIController.HasDivingSuit(followTarget)) { needsDivingGear = true; needsDivingSuit = true; } - else if (HumanAIController.HasDivingMask(followTarget) && character.NeedsAir) + else if (HumanAIController.HasDivingMask(followTarget)) { needsDivingGear = true; } @@ -505,7 +498,7 @@ namespace Barotrauma startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null), endNodeFilter: endNodeFilter, nodeFilter: nodeFilter, - checkVisiblity: CheckVisibility); + checkVisiblity: Target is Item || Target is Character); } if (!isInside && (PathSteering.CurrentPath == null || PathSteering.IsPathDirty || PathSteering.CurrentPath.Unreachable)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs index 67a417900..b13723512 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs @@ -60,6 +60,7 @@ namespace Barotrauma if (targetCondition.HasValue && container.Inventory.IsFull() && container.Inventory.AllItems.None(i => ItemMatchesTargetCondition(i, targetCondition.Value))) { return false; } if (!AIObjectiveCleanupItems.IsItemInsideValidSubmarine(item, character)) { return false; } if (item.GetRootInventoryOwner() is Character owner && owner != character) { return false; } + if (item.IsClaimedByBallastFlora) { return false; } if (!item.HasAccess(character)) { return false; } // Ignore items that require power but don't have it if (item.GetComponent() is Powered powered && powered.PowerConsumption > 0 && powered.Voltage < powered.MinVoltage) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index e77eceb92..9342c4239 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -10,6 +10,16 @@ namespace Barotrauma { class AIObjectiveManager { + public enum ObjectiveType + { + None = 0, + Order = 1, + Objective = 2, + + MinValue = 0, + MaxValue = 2 + } + public const float HighestOrderPriority = 70; public const float LowestOrderPriority = 60; public const float RunPriority = 50; @@ -184,28 +194,20 @@ namespace Barotrauma { var previousObjective = CurrentObjective; var firstObjective = Objectives.FirstOrDefault(); + bool currentObjectiveIsOrder = CurrentOrder != null && firstObjective != null && CurrentOrder.Priority > firstObjective.Priority; - if (currentObjectiveIsOrder) + + CurrentObjective = currentObjectiveIsOrder ? CurrentOrder : firstObjective; + + if (previousObjective == CurrentObjective) { return CurrentObjective; } + + previousObjective?.OnDeselected(); + CurrentObjective?.OnSelected(); + GetObjective().CalculatePriority(Math.Max(CurrentObjective.Priority - 10, 0)); + if (GameMain.NetworkMember is { IsServer: true }) { - CurrentObjective = CurrentOrder; - } - else - { - CurrentObjective = firstObjective; - } - if (previousObjective != CurrentObjective) - { - previousObjective?.OnDeselected(); - CurrentObjective?.OnSelected(); - GetObjective().CalculatePriority(Math.Max(CurrentObjective.Priority - 10, 0)); - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) - { - GameMain.NetworkMember.CreateEntityEvent(character, new object[] - { - NetEntityEvent.Type.ObjectiveManagerState, - currentObjectiveIsOrder ? "order" : "objective" - }); - } + GameMain.NetworkMember.CreateEntityEvent(character, + new Character.ObjectiveManagerStateEventData(currentObjectiveIsOrder ? ObjectiveType.Order : ObjectiveType.Objective)); } return CurrentObjective; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs index 80e9f3371..f1bf0e45f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -67,6 +67,11 @@ namespace Barotrauma Priority = 0; return Priority; } + else if (targetItem.IsClaimedByBallastFlora) + { + Priority = 0; + return Priority; + } var reactor = component?.Item.GetComponent(); if (reactor != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs index f4537800a..c908ac461 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs @@ -41,6 +41,7 @@ namespace Barotrauma if (!character.Submarine.IsConnectedTo(pump.Item.Submarine)) { return false; } } if (Character.CharacterList.Any(c => c.CurrentHull == pump.Item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; } + if (pump.Item.IsClaimedByBallastFlora) { return false; } if (IsReady(pump)) { return false; } return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index adf82ad04..27b75c722 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -1,7 +1,6 @@ using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; -using System.Collections.Generic; using System.Linq; using Barotrauma.Extensions; @@ -12,6 +11,7 @@ namespace Barotrauma public override Identifier Identifier { get; set; } = "repair item".ToIdentifier(); public override bool AllowInAnySub => true; + public override bool KeepDivingGearOn => Item?.CurrentHull == null; public Item Item { get; private set; } @@ -52,6 +52,10 @@ namespace Barotrauma Priority = 0; IsCompleted = true; } + else if (Item.IsClaimedByBallastFlora) + { + Priority = 0; + } else { float distanceFactor = 1; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs index a8b001bb3..005e3aa44 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs @@ -151,6 +151,7 @@ namespace Barotrauma if (!item.IsInteractable(character)) { return false; } if (item.IsFullCondition) { return false; } if (item.Submarine == null || character.Submarine == null) { return false; } + if (item.IsClaimedByBallastFlora) { return false; } //player crew ignores items in outposts if (character.IsOnPlayerTeam && item.Submarine.Info.IsOutpost) { return false; } if (!character.Submarine.IsEntityFoundOnThisSub(item, includingConnectedSubs: true)) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index 7b460210f..4be832fd0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -30,6 +30,7 @@ namespace Barotrauma private float findHullTimer; private bool ignoreOxygen; private readonly float findHullInterval = 1.0f; + private bool performedCpr; public AIObjectiveRescue(Character character, Character targetCharacter, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) @@ -220,12 +221,12 @@ namespace Barotrauma DialogueIdentifier = "dialogcannotreachpatient".ToIdentifier(), TargetName = targetCharacter.DisplayName }, - onCompleted: () => RemoveSubObjective(ref goToObjective), - onAbandon: () => - { - RemoveSubObjective(ref goToObjective); - Abandon = true; - }); + onCompleted: () => RemoveSubObjective(ref goToObjective), + onAbandon: () => + { + RemoveSubObjective(ref goToObjective); + Abandon = true; + }); } else { @@ -401,6 +402,7 @@ namespace Barotrauma { character.SelectCharacter(targetCharacter); character.AnimController.Anim = AnimController.Animation.CPR; + performedCpr = true; } else { @@ -436,9 +438,10 @@ namespace Barotrauma { bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter); if (isCompleted && targetCharacter != character && character.IsOnPlayerTeam) - { - character.Speak(TextManager.GetWithVariable("DialogTargetHealed", "[targetname]", targetCharacter.Name).Value, - null, 1.0f, $"targethealed{targetCharacter.Name}".ToIdentifier(), 60.0f); + { + string textTag = performedCpr ? "DialogTargetResuscitated" : "DialogTargetHealed"; + string message = TextManager.GetWithVariable(textTag, "[targetname]", targetCharacter.Name)?.Value; + character.Speak(message, delay: 1.0f, identifier: $"targethealed{targetCharacter.Name}".ToIdentifier(), minDurationBetweenSimilar: 60.0f); } return isCompleted; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index 9c1b018f9..b9c716c78 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -392,7 +392,6 @@ namespace Barotrauma return option; } - public ImmutableArray GetTargetItems(Identifier option = default) { if (option.IsEmpty || !OptionTargetItems.TryGetValue(option, out ImmutableArray optionTargetItems)) @@ -418,6 +417,28 @@ namespace Barotrauma } public override void Dispose() { } + + /// + /// Create an Order instance with a null target + /// + public Order CreateInstance(OrderTargetType targetType, Character orderGiver = null, bool isAutonomous = false) + { + try + { + return targetType switch + { + OrderTargetType.Entity => new Order(this, targetEntity: null, targetItem: null, orderGiver, isAutonomous), + OrderTargetType.Position => new Order(this, target: null, orderGiver), + OrderTargetType.WallSection => new Order(this, wall: null, sectionIndex: null, orderGiver), + _ => throw new NotImplementedException() + }; + } + catch (NotImplementedException e) + { + DebugConsole.ShowError($"Error creating a new Order instance: unexpected target type \"{targetType}\".\n{e.StackTrace.CleanupStackTrace()}"); + return null; + } + } } class Order @@ -510,28 +531,45 @@ namespace Barotrauma public readonly bool UseController; /// - /// Constructor for order instances + /// Constructor for orders with the target type OrderTargetType.Entity /// public Order(OrderPrefab prefab, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null, bool isAutonomous = false) : this(prefab, Identifier.Empty, 0, OrderType.Current, null, targetEntity, targetItem, orderGiver, isAutonomous) { } - + /// + /// Constructor for orders with the target type OrderTargetType.Entity + /// public Order(OrderPrefab prefab, Identifier option, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null, bool isAutonomous = false) : this(prefab, option, 0, OrderType.Current, null, targetEntity, targetItem, orderGiver, isAutonomous) { } + /// + /// Constructor for orders with the target type OrderTargetType.Position + /// public Order(OrderPrefab prefab, OrderTarget target, Character orderGiver = null) : this(prefab, prefab.Options.FirstOrDefault(), 0, OrderType.Current, null, target, orderGiver) { } + /// + /// Constructor for orders with the target type OrderTargetType.Position + /// public Order(OrderPrefab prefab, Identifier option, OrderTarget target, Character orderGiver = null) : this(prefab, option, 0, OrderType.Current, null, target, orderGiver) { } + /// + /// Constructor for orders with the target type OrderTargetType.WallSection + /// public Order(OrderPrefab prefab, Structure wall, int? sectionIndex, Character orderGiver = null) : this(prefab, Identifier.Empty, 0, OrderType.Current, null, wall, sectionIndex, orderGiver) { } + /// + /// Constructor for orders with the target type OrderTargetType.WallSection + /// public Order(OrderPrefab prefab, Identifier option, Structure wall, int? sectionIndex, Character orderGiver = null) : this(prefab, option, 0, OrderType.Current, null, wall, sectionIndex, orderGiver) { } - public Order(OrderPrefab prefab, Identifier option, int manualPriority, OrderType orderType, AIObjective aiObjective, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null, bool isAutonomous = false) + /// + /// Constructor for orders with the target type OrderTargetType.Entity + /// + private Order(OrderPrefab prefab, Identifier option, int manualPriority, OrderType orderType, AIObjective aiObjective, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null, bool isAutonomous = false) { Prefab = prefab; Option = option; @@ -561,14 +599,20 @@ namespace Barotrauma TargetType = OrderTargetType.Entity; } - public Order(OrderPrefab prefab, Identifier option, int manualPriority, OrderType orderType, AIObjective aiObjective, OrderTarget target, Character orderGiver = null) + /// + /// Constructor for orders with the target type OrderTargetType.Position + /// + private Order(OrderPrefab prefab, Identifier option, int manualPriority, OrderType orderType, AIObjective aiObjective, OrderTarget target, Character orderGiver = null) : this(prefab, option, manualPriority, orderType, aiObjective, targetEntity: null, targetItem: null, orderGiver) { TargetPosition = target; TargetType = OrderTargetType.Position; } - public Order(OrderPrefab prefab, Identifier option, int manualPriority, OrderType orderType, AIObjective aiObjective, Structure wall, int? sectionIndex, Character orderGiver = null) + /// + /// Constructor for orders with the target type OrderTargetType.WallSection + /// + private Order(OrderPrefab prefab, Identifier option, int manualPriority, OrderType orderType, AIObjective aiObjective, Structure wall, int? sectionIndex, Character orderGiver = null) : this(prefab, option, manualPriority, orderType, aiObjective, targetEntity: wall, null, orderGiver: orderGiver) { WallSectionIndex = sectionIndex; @@ -633,7 +677,7 @@ namespace Barotrauma public Order WithTargetEntity(Entity entity) { - return new Order(this, targetEntity: entity); + return new Order(this, targetEntity: entity, targetType: OrderTargetType.Entity); } public Order WithTargetSpatialEntity(ISpatialEntity spatialEntity) @@ -673,7 +717,7 @@ namespace Barotrauma public Order WithTargetPosition(OrderTarget targetPosition) { - return new Order(this, targetPosition: targetPosition); + return new Order(this, targetPosition: targetPosition, targetType: OrderTargetType.Position); } public Order Clone() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs index d171db72a..a112758f1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs @@ -313,8 +313,7 @@ namespace Barotrauma ShipCommandLog("Dismissing " + shipIssueWorker + " for character " + shipIssueWorker.OrderedCharacter); #endif var order = new Order(OrderPrefab.Dismissal, null).WithManualPriority(3).WithOrderGiver(character); - //character.Speak(orderPrefab.GetChatMessage(shipIssueWorker.OrderedCharacter.Name, "", givingOrderToSelf: false)); - shipIssueWorker.OrderedCharacter.SetOrder(order); + shipIssueWorker.OrderedCharacter.SetOrder(order, isNewOrder: true); shipIssueWorker.RemoveOrder(); break; } @@ -368,7 +367,7 @@ namespace Barotrauma ShipGlobalIssueFixLeaks shipGlobalIssueFixLeaks = new ShipGlobalIssueFixLeaks(this); for (int i = 0; i < crewSizeModifier; i++) { - var order = new Order(OrderPrefab.Prefabs["fixleaks"], null); + var order = OrderPrefab.Prefabs["fixleaks"].CreateInstance(OrderPrefab.OrderTargetType.Entity); ShipIssueWorkers.Add(new ShipIssueWorkerFixLeaks(this, order, shipGlobalIssueFixLeaks)); } shipGlobalIssues.Add(shipGlobalIssueFixLeaks); @@ -376,7 +375,7 @@ namespace Barotrauma ShipGlobalIssueRepairSystems shipGlobalIssueRepairSystems = new ShipGlobalIssueRepairSystems(this); for (int i = 0; i < crewSizeModifier; i++) { - var order = new Order(OrderPrefab.Prefabs["repairsystems"], null); + var order = OrderPrefab.Prefabs["repairsystems"].CreateInstance(OrderPrefab.OrderTargetType.Entity); ShipIssueWorkers.Add(new ShipIssueWorkerRepairSystems(this, order, shipGlobalIssueRepairSystems)); } shipGlobalIssues.Add(shipGlobalIssueRepairSystems); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs index 9de13ceed..25d23300b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs @@ -442,7 +442,7 @@ namespace Barotrauma } #if SERVER - public void ServerWrite(IWriteMessage msg, Client client, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client client, NetEntityEvent.IData extraData = null) { msg.Write(IsAlive); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index 50af42736..699f94e37 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -1017,24 +1017,29 @@ namespace Barotrauma { if (l.IsSevered) { continue; } + float rotation = l.body.Rotation; + if (l.DoesFlip) + { + if (RagdollParams.IsSpritesheetOrientationHorizontal) + { + //horizontally oriented sprites can be mirrored by rotating 180 deg and inverting the angle + rotation = -(l.body.Rotation + MathHelper.Pi); + } + else + { + //vertically oriented limbs can be mirrored by inverting the angle (neutral angle is straight upwards) + rotation = -l.body.Rotation; + } + } + TrySetLimbPosition(l, centerOfMass, new Vector2(centerOfMass.X - (l.SimPosition.X - centerOfMass.X), l.SimPosition.Y), + rotation, lerp); l.body.PositionSmoothingFactor = 0.8f; - - if (!l.DoesFlip) { continue; } - if (RagdollParams.IsSpritesheetOrientationHorizontal) - { - //horizontally oriented sprites can be mirrored by rotating 180 deg and inverting the angle - l.body.SetTransform(l.SimPosition, -(l.body.Rotation + MathHelper.Pi)); - } - else - { - //vertically oriented limbs can be mirrored by inverting the angle (neutral angle is straight upwards) - l.body.SetTransform(l.SimPosition, -l.body.Rotation); - } + } if (character.SelectedCharacter != null && CanDrag(character.SelectedCharacter)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index c82919b64..b386fdd83 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -1846,11 +1846,9 @@ namespace Barotrauma } float angle = flipAngle ? -limb.body.Rotation : limb.body.Rotation; - if (wrapAngle) angle = MathUtils.WrapAnglePi(angle); + if (wrapAngle) { angle = MathUtils.WrapAnglePi(angle); } - TrySetLimbPosition(limb, Collider.SimPosition, position); - - limb.body.SetTransform(limb.body.SimPosition, angle); + TrySetLimbPosition(limb, Collider.SimPosition, position, angle); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 39fd2305e..874e6e000 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -803,9 +803,9 @@ namespace Barotrauma } SeverLimbJointProjSpecific(limbJoint, playSound: true); - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + if (GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(character, new object[] { NetEntityEvent.Type.Status }); + GameMain.NetworkMember.CreateEntityEvent(character, new Character.StatusEventData()); } return true; } @@ -1693,7 +1693,7 @@ namespace Barotrauma if (limb.IsSevered) { continue; } //check visibility from the new position of the collider to the new position of this limb Vector2 movePos = limb.SimPosition + limbMoveAmount; - TrySetLimbPosition(limb, simPosition, movePos, lerp, ignorePlatforms); + TrySetLimbPosition(limb, simPosition, movePos, limb.Rotation, lerp, ignorePlatforms); } } } @@ -1708,7 +1708,7 @@ namespace Barotrauma IsHanging = true; } - protected void TrySetLimbPosition(Limb limb, Vector2 original, Vector2 simPosition, bool lerp = false, bool ignorePlatforms = true) + protected void TrySetLimbPosition(Limb limb, Vector2 original, Vector2 simPosition, float rotation, bool lerp = false, bool ignorePlatforms = true) { Vector2 movePos = simPosition; @@ -1730,11 +1730,12 @@ namespace Barotrauma if (lerp) { limb.body.TargetPosition = movePos; - limb.body.MoveToTargetPosition(true); + limb.body.TargetRotation = rotation; + limb.body.MoveToTargetPosition(true); } else { - limb.body.SetTransform(movePos, limb.Rotation); + limb.body.SetTransform(movePos, rotation); limb.PullJointWorldAnchorB = limb.PullJointWorldAnchorA; limb.PullJointEnabled = false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index 080c14292..ff3af7ccf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -487,6 +487,10 @@ namespace Barotrauma // TODO: do we want to apply the effect at the world position or the entity positions in each cases? -> go through also other cases where status effects are applied effect.Apply(effectType, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity, worldPosition); } + if (effect.HasTargetType(StatusEffect.TargetType.Parent)) + { + effect.Apply(effectType, deltaTime, attacker, attacker); + } if (targetCharacter != null) { if (effect.HasTargetType(StatusEffect.TargetType.Character)) @@ -551,6 +555,10 @@ namespace Barotrauma { effect.Apply(effectType, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity); } + if (effect.HasTargetType(StatusEffect.TargetType.Parent)) + { + effect.Apply(effectType, deltaTime, attacker, attacker); + } if (effect.HasTargetType(StatusEffect.TargetType.Character)) { effect.Apply(effectType, deltaTime, targetLimb.character, targetLimb.character); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 3c0307d51..3f6245bc7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -25,7 +25,7 @@ namespace Barotrauma FriendlyNPC = 3 } - partial class Character : Entity, IDamageable, ISerializableEntity, IClientSerializable, IServerSerializable + partial class Character : Entity, IDamageable, ISerializableEntity, IClientSerializable, IServerPositionSync { public readonly static List CharacterList = new List(); @@ -130,6 +130,22 @@ namespace Barotrauma } } + private Wallet wallet = new Wallet(); + + public Wallet Wallet + { + get + { + ThrowIfAccessingWalletsInSingleplayer(); + return wallet; + } + set + { + ThrowIfAccessingWalletsInSingleplayer(); + wallet = value; + } + } + public readonly HashSet Latchers = new HashSet(); public readonly HashSet AttachedProjectiles = new HashSet(); @@ -137,6 +153,17 @@ namespace Barotrauma protected ActiveTeamChange currentTeamChange; const string OriginalTeamIdentifier = "original"; + public static void ThrowIfAccessingWalletsInSingleplayer() + { +#if CLIENT && DEBUG + if (Screen.Selected is TestScreen) { return; } +#endif + if (GameMain.NetworkMember is null || GameMain.IsSingleplayer) + { + throw new InvalidOperationException($"Tried to access crew wallets in singleplayer. Use {nameof(CampaignMode)}.{nameof(CampaignMode.Bank)} or {nameof(CampaignMode)}.{nameof(CampaignMode.GetWallet)} instead."); + } + } + public void SetOriginalTeam(CharacterTeamType newTeam) { TryRemoveTeamChange(OriginalTeamIdentifier); @@ -158,12 +185,11 @@ namespace Barotrauma return; } // clear up any duties the character might have had from its old team (autonomous objectives are automatically recreated) - var order = new Order(OrderPrefab.Dismissal, Identifier.Empty, - manualPriority: 3, orderType: Order.OrderType.Current, aiObjective: null, target: null, orderGiver: this); - SetOrder(order, speak: false); + var order = OrderPrefab.Dismissal.CreateInstance(OrderPrefab.OrderTargetType.Entity, orderGiver: this).WithManualPriority(CharacterInfo.HighestManualOrderPriority); + SetOrder(order, isNewOrder: true, speak: false); #if SERVER - GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.TeamChange }); + GameMain.NetworkMember.CreateEntityEvent(this, new TeamChangeEventData()); #endif } @@ -225,9 +251,8 @@ namespace Barotrauma if (bestTeamChange.AggressiveBehavior) // this seemed like the least disruptive way to induce aggressive behavior { - var order = new Order(OrderPrefab.Prefabs["fightintruders"], Identifier.Empty, - manualPriority: 3, orderType: Order.OrderType.Current, aiObjective: null, target: null, orderGiver: this); - SetOrder(order, speak: false); + var order = OrderPrefab.Prefabs["fightintruders"].CreateInstance(OrderPrefab.OrderTargetType.Entity, orderGiver: this).WithManualPriority(CharacterInfo.HighestManualOrderPriority); + SetOrder(order, isNewOrder: true, speak: false); } } } @@ -1023,7 +1048,7 @@ namespace Barotrauma #if SERVER if (GameMain.Server != null && Spawner != null && createNetworkEvent) { - Spawner.CreateNetworkEvent(newCharacter, false); + Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(newCharacter)); } #endif return newCharacter; @@ -1178,6 +1203,10 @@ namespace Barotrauma info = new CharacterInfo(nonHuskedSpeciesName); } } + else if (Params.HasInfo && info == null) + { + info = new CharacterInfo(speciesName); + } if (IsHumanoid) { @@ -1418,9 +1447,9 @@ namespace Barotrauma { item.AddTag(s); } - if (createNetworkEvent && (GameMain.NetworkMember?.IsServer ?? false)) + if (createNetworkEvent && GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ChangeProperty, item.SerializableProperties[nameof(item.Tags).ToIdentifier()] }); + GameMain.NetworkMember.CreateEntityEvent(item, new Item.ChangePropertyEventData(item.SerializableProperties[nameof(item.Tags).ToIdentifier()])); } } } @@ -3199,7 +3228,7 @@ namespace Barotrauma } /// Force an order to be set for the character, bypassing hearing checks - public void SetOrder(Order order, bool speak = true, bool force = false) + public void SetOrder(Order order, bool isNewOrder, bool speak = true, bool force = false) { var orderGiver = order?.OrderGiver; //set the character order only if the character is close enough to hear the message @@ -3226,7 +3255,7 @@ namespace Barotrauma if (currentOrder.Identifier != order.Identifier) { continue; } if (currentOrder.TargetEntity != order.TargetEntity) { continue; } if (!currentOrder.AutoDismiss) { continue; } - character.SetOrder(currentOrder.GetDismissal(), speak: speak, force: force); + character.SetOrder(currentOrder.GetDismissal(), isNewOrder, speak: speak, force: force); break; } } @@ -3243,7 +3272,7 @@ namespace Barotrauma } if (orderToReplace is { AutoDismiss: true }) { - SetOrder(orderToReplace.GetDismissal(), speak: speak, force: force); + SetOrder(orderToReplace.GetDismissal(), isNewOrder, speak: speak, force: force); } break; } @@ -3251,10 +3280,10 @@ namespace Barotrauma } // Prevent adding duplicate orders - bool wasDuplicate = RemoveDuplicateOrders(order); + RemoveDuplicateOrders(order); AddCurrentOrder(order); - if (orderGiver != null && order.Identifier != "dismissed" && !wasDuplicate) + if (orderGiver != null && order.Identifier != "dismissed" && isNewOrder) { var abilityOrderedCharacter = new AbilityOrderedCharacter(this); orderGiver.CheckTalents(AbilityEffectType.OnGiveOrder, abilityOrderedCharacter); @@ -3976,14 +4005,14 @@ namespace Barotrauma HealthUpdateInterval = 0.0f; //clients aren't allowed to kill characters unless they receive a network message - if (!isNetworkMessage && GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) + if (!isNetworkMessage && GameMain.NetworkMember is { IsClient: true }) { return; } - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + if (GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.Status }); + GameMain.NetworkMember.CreateEntityEvent(this, new StatusEventData()); } isDead = true; @@ -4072,15 +4101,7 @@ namespace Barotrauma } } - if (GameMain.GameSession != null) - { - if (GameMain.GameSession.Campaign != null && TeamID == CharacterTeamType.Team1 && !IsAssistant) - { - GameMain.GameSession.Campaign.CrewHasDied = true; - } - - GameMain.GameSession.KillCharacter(this); - } + GameMain.GameSession?.KillCharacter(this); } partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool log); @@ -4245,7 +4266,7 @@ namespace Barotrauma if (!MathUtils.NearlyEqual(newItem.Condition, newItem.MaxCondition) && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { - GameMain.NetworkMember.CreateEntityEvent(newItem, new object[] { NetEntityEvent.Type.Status }); + GameMain.NetworkMember.CreateEntityEvent(newItem, new StatusEventData()); } #if SERVER newItem.GetComponent()?.SyncHistory(); @@ -4334,7 +4355,7 @@ namespace Barotrauma hull?.Submarine ?? Submarine); extraDuffelBags.Add(newDuffelBag); #if SERVER - Spawner.CreateNetworkEvent(newDuffelBag, false); + Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(newDuffelBag)); #endif } @@ -4547,7 +4568,7 @@ namespace Barotrauma info.UnlockedTalents.Add(talentPrefab.Identifier); if (characterTalents.Any(t => t.Prefab == talentPrefab)) { return false; } #if SERVER - GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.UpdateTalents }); + GameMain.NetworkMember.CreateEntityEvent(this, new UpdateTalentsEventData()); #endif CharacterTalent characterTalent = new CharacterTalent(talentPrefab, this); characterTalents.Add(characterTalent); @@ -4626,23 +4647,44 @@ namespace Barotrauma /// public void GiveMoney(int amount) { - if (!(GameMain.GameSession?.Campaign is CampaignMode campaign)) { return; } + if (!(GameMain.GameSession?.Campaign is { } campaign)) { return; } if (amount <= 0) { return; } - int prevAmount = campaign.Money; - campaign.Money += amount; - OnMoneyChanged(prevAmount, campaign.Money); + Wallet wallet; +#if SERVER + if (!(campaign is MultiPlayerCampaign mpCampaign)) { throw new InvalidOperationException("Campaign on a server is not a multiplayer campaign"); } + Client targetClient = null; + + foreach (Client client in GameMain.Server.ConnectedClients) + { + if (client.Character == this) + { + targetClient = client; + break; + } + } + + wallet = targetClient is null ? mpCampaign.Bank : mpCampaign.GetWallet(targetClient); +#else + wallet = campaign.Wallet; +#endif + + int prevAmount = wallet.Balance; + wallet.Give(amount); + OnMoneyChanged(prevAmount, wallet.Balance); } +#if CLIENT public void SetMoney(int amount) { - if (!(GameMain.GameSession?.Campaign is CampaignMode campaign)) { return; } - if (amount == campaign.Money) { return; } + if (!(GameMain.GameSession?.Campaign is { } campaign)) { return; } + if (amount == campaign.Wallet.Balance) { return; } - int prevAmount = campaign.Money; - campaign.Money = amount; - OnMoneyChanged(prevAmount, campaign.Money); + int prevAmount = campaign.Wallet.Balance; + campaign.Wallet.Balance = amount; + OnMoneyChanged(prevAmount, campaign.Wallet.Balance); } +#endif partial void OnMoneyChanged(int prevAmount, int newAmount); partial void OnTalentGiven(TalentPrefab talentPrefab); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs new file mode 100644 index 000000000..11eab9eba --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs @@ -0,0 +1,172 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using Barotrauma.Items.Components; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + partial class Character + { + public enum EventType + { + InventoryState = 0, + Control = 1, + Status = 2, + Treatment = 3, + SetAttackTarget = 4, + ExecuteAttack = 5, + AssignCampaignInteraction = 6, + ObjectiveManagerState = 7, + TeamChange = 8, + AddToCrew = 9, + UpdateExperience = 10, + UpdateTalents = 11, + UpdateSkills = 12, + UpdateMoney = 13, + UpdatePermanentStats = 14, + + MinValue = 0, + MaxValue = 14 + } + + private interface IEventData : NetEntityEvent.IData + { + public EventType EventType { get; } + } + + public struct InventoryStateEventData : IEventData + { + public EventType EventType => EventType.InventoryState; + } + + public struct ControlEventData : IEventData + { + public EventType EventType => EventType.Control; + public readonly Client Owner; + + public ControlEventData(Client owner) + { + Owner = owner; + } + } + + public struct StatusEventData : IEventData + { + public EventType EventType => EventType.Status; + } + + public struct TreatmentEventData : IEventData + { + public EventType EventType => EventType.Treatment; + } + + private interface IAttackEventData : IEventData + { + public Limb AttackLimb { get; } + public IDamageable TargetEntity { get; } + public Limb TargetLimb { get; } + public Vector2 TargetSimPos { get; } + } + + public struct SetAttackTargetEventData : IAttackEventData + { + public EventType EventType => EventType.SetAttackTarget; + public Limb AttackLimb { get; } + public IDamageable TargetEntity { get; } + public Limb TargetLimb { get; } + public Vector2 TargetSimPos { get; } + + public SetAttackTargetEventData(Limb attackLimb, IDamageable targetEntity, Limb targetLimb, Vector2 targetSimPos) + { + AttackLimb = attackLimb; + TargetEntity = targetEntity; + TargetLimb = targetLimb; + TargetSimPos = targetSimPos; + } + } + + public struct ExecuteAttackEventData : IAttackEventData + { + public EventType EventType => EventType.ExecuteAttack; + public Limb AttackLimb { get; } + public IDamageable TargetEntity { get; } + public Limb TargetLimb { get; } + public Vector2 TargetSimPos { get; } + + public ExecuteAttackEventData(Limb attackLimb, IDamageable targetEntity, Limb targetLimb, Vector2 targetSimPos) + { + AttackLimb = attackLimb; + TargetEntity = targetEntity; + TargetLimb = targetLimb; + TargetSimPos = targetSimPos; + } + } + + public struct AssignCampaignInteractionEventData : IEventData + { + public EventType EventType => EventType.AssignCampaignInteraction; + } + + public struct ObjectiveManagerStateEventData : IEventData + { + public EventType EventType => EventType.ObjectiveManagerState; + public readonly AIObjectiveManager.ObjectiveType ObjectiveType; + + public ObjectiveManagerStateEventData(AIObjectiveManager.ObjectiveType objectiveType) + { + ObjectiveType = objectiveType; + } + } + + private struct TeamChangeEventData : IEventData + { + public EventType EventType => EventType.TeamChange; + } + + public struct AddToCrewEventData : IEventData + { + public EventType EventType => EventType.AddToCrew; + public readonly CharacterTeamType TeamType; + public readonly ImmutableArray InventoryItems; + + public AddToCrewEventData(CharacterTeamType teamType, IEnumerable inventoryItems) + { + TeamType = teamType; + InventoryItems = inventoryItems.ToImmutableArray(); + } + + } + + public struct UpdateExperienceEventData : IEventData + { + public EventType EventType => EventType.UpdateExperience; + } + + public struct UpdateTalentsEventData : IEventData + { + public EventType EventType => EventType.UpdateTalents; + } + + public struct UpdateSkillsEventData : IEventData + { + public EventType EventType => EventType.UpdateSkills; + } + + private struct UpdateMoneyEventData : IEventData + { + public EventType EventType => EventType.UpdateMoney; + } + + public struct UpdatePermanentStatsEventData : IEventData + { + public EventType EventType => EventType.UpdatePermanentStats; + public readonly StatTypes StatType; + + public UpdatePermanentStatsEventData(StatTypes statType) + { + StatType = statType; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index e008a0432..4142ba190 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -299,7 +299,11 @@ namespace Barotrauma { if (handleBuff) { - Character.CharacterHealth.ApplyAffliction(Character.AnimController.GetLimb(LimbType.Head), AfflictionPrefab.List.FirstOrDefault(a => a.Identifier == "disguised").Instantiate(100f)); + var head = Character.AnimController.GetLimb(LimbType.Head); + if (head != null) + { + Character.CharacterHealth.ApplyAffliction(head, AfflictionPrefab.List.FirstOrDefault(a => a.Identifier == "disguised").Instantiate(100f)); + } } idCard ??= Character.Inventory?.GetItemInLimbSlot(InvSlotType.Card)?.GetComponent(); @@ -319,7 +323,11 @@ namespace Barotrauma if (handleBuff) { - Character.CharacterHealth.ReduceAfflictionOnLimb(Character.AnimController.GetLimb(LimbType.Head), "disguised".ToIdentifier(), 100f); + var head = Character.AnimController.GetLimb(LimbType.Head); + if (head != null) + { + Character.CharacterHealth.ReduceAfflictionOnLimb(head, "disguised".ToIdentifier(), 100f); + } } } @@ -572,15 +580,15 @@ namespace Barotrauma private void CheckColors() { - if (IsColorValid(Head.HairColor)) + if (!IsColorValid(Head.HairColor)) { Head.HairColor = SelectRandomColor(HairColors, Rand.RandSync.Unsynced); } - if (IsColorValid(Head.FacialHairColor)) + if (!IsColorValid(Head.FacialHairColor)) { Head.FacialHairColor = SelectRandomColor(FacialHairColors, Rand.RandSync.Unsynced); } - if (IsColorValid(Head.SkinColor)) + if (!IsColorValid(Head.SkinColor)) { Head.SkinColor = SelectRandomColor(SkinColors, Rand.RandSync.Unsynced); } @@ -736,7 +744,7 @@ namespace Barotrauma private int GetIdentifier(string name) { - int id = ToolBox.StringToInt(name + string.Join("", Head.Preset.TagSet)); + int id = ToolBox.StringToInt(name + string.Join("", Head.Preset.TagSet.OrderBy(s => s))); id ^= Head.HairIndex << 12; id ^= Head.BeardIndex << 18; id ^= Head.MoustacheIndex << 24; @@ -822,12 +830,12 @@ namespace Barotrauma { foreach (var limbElement in Ragdoll.MainElement.Elements()) { - if (!limbElement.GetAttributeString("type", "").Equals("head", StringComparison.OrdinalIgnoreCase)) { continue; } + if (!limbElement.GetAttributeString("type", string.Empty).Equals("head", StringComparison.OrdinalIgnoreCase)) { continue; } ContentXElement spriteElement = limbElement.GetChildElement("sprite"); if (spriteElement == null) { continue; } - string spritePath = spriteElement.Attribute("texture").Value; + string spritePath = spriteElement.GetAttributeContentPath("texture")?.Value; if (string.IsNullOrEmpty(spritePath)) { continue; } spritePath = ReplaceVars(spritePath); @@ -1298,7 +1306,7 @@ namespace Barotrauma var orders = LoadOrders(orderData); foreach (var order in orders) { - character.SetOrder(order, speak: false, force: true); + character.SetOrder(order, isNewOrder: true, speak: false, force: true); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index 9e74e40e9..4069abe8f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -41,9 +41,11 @@ namespace Barotrauma if (previousValue > 0.0f && value <= 0.0f) { DeactivateHusk(); + highestStrength = 0; } } } + private float highestStrength; public InfectionState State { @@ -75,6 +77,7 @@ namespace Barotrauma { if (HuskPrefab == null) { return; } base.Update(characterHealth, targetLimb, deltaTime); + highestStrength = Math.Max(_strength, highestStrength); character = characterHealth.Character; if (character == null) { return; } @@ -98,7 +101,7 @@ namespace Barotrauma DeactivateHusk(); if (Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: true }) { - character.SpeechImpediment = 100; + character.SpeechImpediment = 30; } State = InfectionState.Transition; } @@ -108,6 +111,10 @@ namespace Barotrauma { character.SetStun(Rand.Range(2f, 3f)); } + if (Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: true }) + { + character.SpeechImpediment = 100; + } State = InfectionState.Active; ActivateHusk(); } @@ -120,7 +127,57 @@ namespace Barotrauma } } - partial void UpdateMessages(); + private InfectionState? prevDisplayedMessage; + private void UpdateMessages() + { + if (Prefab is AfflictionPrefabHusk { SendMessages: false }) { return; } + if (prevDisplayedMessage.HasValue && prevDisplayedMessage.Value == State) { return; } + if (highestStrength > Strength) { return; } + + switch (State) + { + case InfectionState.Dormant: + if (Strength < DormantThreshold * 0.5f) + { + return; + } + if (character == Character.Controlled) + { +#if CLIENT + GUI.AddMessage(TextManager.Get("HuskDormant"), GUIStyle.Red); +#endif + } + else if (character.IsBot) + { + character.Speak(TextManager.Get("dialoghuskdormant").Value, delay: Rand.Range(0.5f, 5.0f), identifier: "huskdormant".ToIdentifier()); + } + break; + case InfectionState.Transition: + if (character == Character.Controlled) + { +#if CLIENT + GUI.AddMessage(TextManager.Get("HuskCantSpeak"), GUIStyle.Red); +#endif + } + else if (character.IsBot) + { + character.Speak(TextManager.Get("dialoghuskcantspeak").Value, delay: Rand.Range(0.5f, 5.0f), identifier: "huskcantspeak".ToIdentifier()); + } + break; + case InfectionState.Active: +#if CLIENT + if (character == Character.Controlled && character.Params.UseHuskAppendage) + { + GUI.AddMessage(TextManager.GetWithVariable("HuskActivate", "[Attack]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Attack)), GUIStyle.Red); + } +#endif + break; + case InfectionState.Final: + default: + break; + } + prevDisplayedMessage = State; + } private void ApplyDamage(float deltaTime, bool applyForce) { @@ -209,7 +266,9 @@ namespace Barotrauma { yield return CoroutineStatus.Success; } - +#if SERVER + var client = GameMain.Server?.ConnectedClients.FirstOrDefault(c => c.Character == character); +#endif character.Enabled = false; Entity.Spawner.AddEntityToRemoveQueue(character); UnsubscribeFromDeathEvent(); @@ -246,7 +305,6 @@ namespace Barotrauma if (huskPrefab.ControlHusk) { #if SERVER - var client = GameMain.Server?.ConnectedClients.FirstOrDefault(c => c.Character == character); if (client != null) { GameMain.Server.SetClientCharacter(client, husk); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index ad88711ce..320d295d0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -657,6 +657,7 @@ namespace Barotrauma if (!DoesBleed && newAffliction is AfflictionBleeding) { return; } if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; } if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == "stun") { return; } + if (Character.Params.Health.PoisonImmunity && newAffliction.Prefab.AfflictionType == "poison") { return; } if (newAffliction.Prefab is AfflictionPrefabHusk huskPrefab) { if (huskPrefab.TargetSpecies.None(s => s == Character.SpeciesName)) @@ -731,48 +732,47 @@ namespace Barotrauma StunTimer = Stun > 0 ? StunTimer + deltaTime : 0; - if (Character.GodMode) { return; } - - afflictionsToRemove.Clear(); - afflictionsToUpdate.Clear(); - foreach (KeyValuePair kvp in afflictions) + if (!Character.GodMode) { - var affliction = kvp.Key; - if (affliction.Strength <= 0.0f) + afflictionsToRemove.Clear(); + afflictionsToUpdate.Clear(); + foreach (KeyValuePair kvp in afflictions) { - SteamAchievementManager.OnAfflictionRemoved(affliction, Character); - if (!irremovableAfflictions.Contains(affliction)) { afflictionsToRemove.Add(affliction); } - continue; + var affliction = kvp.Key; + if (affliction.Strength <= 0.0f) + { + SteamAchievementManager.OnAfflictionRemoved(affliction, Character); + if (!irremovableAfflictions.Contains(affliction)) { afflictionsToRemove.Add(affliction); } + continue; + } + afflictionsToUpdate.Add(kvp); } - afflictionsToUpdate.Add(kvp); - } - foreach (KeyValuePair kvp in afflictionsToUpdate) - { - var affliction = kvp.Key; - Limb targetLimb = null; - if (kvp.Value != null) + foreach (KeyValuePair kvp in afflictionsToUpdate) { - int healthIndex = limbHealths.IndexOf(kvp.Value); - targetLimb = - Character.AnimController.Limbs.LastOrDefault(l => !l.IsSevered && !l.Hidden && l.HealthIndex == healthIndex) ?? - Character.AnimController.MainLimb; + var affliction = kvp.Key; + Limb targetLimb = null; + if (kvp.Value != null) + { + int healthIndex = limbHealths.IndexOf(kvp.Value); + targetLimb = + Character.AnimController.Limbs.LastOrDefault(l => !l.IsSevered && !l.Hidden && l.HealthIndex == healthIndex) ?? + Character.AnimController.MainLimb; + } + affliction.Update(this, targetLimb, deltaTime); + affliction.DamagePerSecondTimer += deltaTime; + if (affliction is AfflictionBleeding bleeding) + { + UpdateBleedingProjSpecific(bleeding, targetLimb, deltaTime); + } + Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier()); } - affliction.Update(this, targetLimb, deltaTime); - affliction.DamagePerSecondTimer += deltaTime; - if (affliction is AfflictionBleeding bleeding) + foreach (var affliction in afflictionsToRemove) { - UpdateBleedingProjSpecific(bleeding, targetLimb, deltaTime); - } - Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier()); - } - - foreach (var affliction in afflictionsToRemove) - { - afflictions.Remove(affliction); + afflictions.Remove(affliction); + } } Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.MovementSpeed)); - if (Character.InWater) { Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.SwimmingSpeed)); @@ -782,13 +782,16 @@ namespace Barotrauma Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.WalkingSpeed)); } - UpdateLimbAfflictionOverlays(); - UpdateSkinTint(); - CalculateVitality(); - - if (Vitality <= MinVitality) + if (!Character.GodMode) { - Kill(); + UpdateLimbAfflictionOverlays(); + UpdateSkinTint(); + CalculateVitality(); + + if (Vitality <= MinVitality) + { + Kill(); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index 5f9372f84..f48d7aa79 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -190,7 +190,7 @@ namespace Barotrauma GameMain.Server.EntityEventManager.Events.RemoveAll(ev => ev.Entity == item); } - Entity.Spawner.CreateNetworkEvent(item, false); + Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item)); } #endif if (itemElement.GetAttributeBool("equip", false)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs index 3f1993a7d..077e8a3da 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs @@ -152,7 +152,7 @@ namespace Barotrauma GameMain.Server.EntityEventManager.Events.RemoveAll(ev => ev.Entity == item); } - Entity.Spawner.CreateNetworkEvent(item, false); + Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item)); } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index 97e51110f..b08cafe51 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -1011,15 +1011,9 @@ namespace Barotrauma ExecuteAttack(damageTarget, targetLimb, out attackResult); } #if SERVER - GameMain.NetworkMember.CreateEntityEvent(character, new object[] - { - NetEntityEvent.Type.ExecuteAttack, - this, - (damageTarget as Entity)?.ID ?? Entity.NullEntityID, - damageTarget is Character && targetLimb != null ? Array.IndexOf(((Character)damageTarget).AnimController.Limbs, targetLimb) : 0, - attackSimPos.X, - attackSimPos.Y - }); + GameMain.NetworkMember.CreateEntityEvent(character, new Character.ExecuteAttackEventData( + attackLimb: this, targetEntity: damageTarget, targetLimb: targetLimb, + targetSimPos: attackSimPos)); #endif } @@ -1055,7 +1049,10 @@ namespace Barotrauma if (!attack.IsRunning) { // Set the main collider where the body lands after the attack - character.AnimController.Collider.SetTransform(character.AnimController.MainLimb.body.SimPosition, rotation: character.AnimController.Collider.Rotation); + if (Vector2.DistanceSquared(character.AnimController.Collider.SimPosition, character.AnimController.MainLimb.body.SimPosition) > 0.1f * 0.1f) + { + character.AnimController.Collider.SetTransform(character.AnimController.MainLimb.body.SimPosition, rotation: character.AnimController.Collider.Rotation); + } } return wasHit; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 5733c62f1..68d156778 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -162,7 +162,7 @@ namespace Barotrauma Identifier variantOf = MainElement.VariantOf(); if (!variantOf.IsEmpty) { - VariantFile = doc; + VariantFile = new XDocument(doc); #warning TODO: determine that CreateVariantXML is equipped to do this XElement newRoot = CreateVariantXml(MainElement, CharacterPrefab.FindBySpeciesName(variantOf).ConfigElement); var oldElement = MainElement; @@ -477,12 +477,15 @@ namespace Barotrauma [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float ConstantHealthRegeneration { get; private set; } - [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 100, DecimalCount = 2)] public float HealthRegenerationWhenEating { get; private set; } [Serialize(false, IsPropertySaveable.Yes), Editable] public bool StunImmunity { get; set; } + [Serialize(false, IsPropertySaveable.Yes), Editable] + public bool PoisonImmunity { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "Can afflictions affect the face/body tint of the character."), Editable] public bool ApplyAfflictionColors { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs index c25fa3367..7db320395 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs @@ -956,7 +956,7 @@ namespace Barotrauma Deformations = new Dictionary(); foreach (var deformationElement in element.GetChildElements("spritedeformation")) { - string typeName = deformationElement.GetAttributeString("typename", null) ?? deformationElement.GetAttributeString("type", ""); + string typeName = deformationElement.GetAttributeString("type", null) ?? deformationElement.GetAttributeString("typename", string.Empty); SpriteDeformationParams deformation = null; switch (typeName.ToLowerInvariant()) { @@ -982,7 +982,7 @@ namespace Barotrauma } if (deformation != null) { - deformation.TypeName = typeName; + deformation.Type = typeName; } Deformations.Add(deformation, deformationElement); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNoCrewDied.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNoCrewDied.cs index 177cd4bf2..acf06d837 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNoCrewDied.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNoCrewDied.cs @@ -1,20 +1,28 @@ -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class AbilityConditionNoCrewDied : AbilityConditionDataless { + public bool assistantsDontCount; + public AbilityConditionNoCrewDied(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { + assistantsDontCount = conditionElement.GetAttributeBool(nameof(assistantsDontCount), true); } protected override bool MatchesConditionSpecific() { - if (GameMain.GameSession?.Campaign is CampaignMode campaign) + if (GameMain.GameSession == null) { return false; } + + foreach (Character character in GameMain.GameSession.Casualties) { - return !campaign.CrewHasDied; + if (assistantsDontCount && character.Info?.Job?.Prefab.Identifier == "assistant") + { + continue; + } + if (character.CauseOfDeath != null && character.CauseOfDeath.Type != CauseOfDeathType.Disconnected) + { + return false; + } } return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs index 9a6732858..73e07dcf1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; +using System.IO; using System.Linq; using System.Reflection; using System.Xml.Linq; @@ -63,7 +64,7 @@ namespace Barotrauma public static Result CreateFromXElement(ContentPackage contentPackage, XElement element) { - Result fail(string error) + static Result fail(string error) => Result.Failure(error); Identifier elemName = element.NameAsIdentifier(); @@ -73,13 +74,16 @@ namespace Barotrauma { return fail($"Invalid content type \"{elemName}\""); } - if (filePath is null) { return fail($"No content path defined for file of type \"{elemName}\""); } try { + if (!File.Exists(filePath.FullPath)) + { + return fail($"Failed to load file \"{filePath}\" of type \"{elemName}\": file not found."); + } var file = type.CreateInstance(contentPackage, filePath); return file is null ? throw new Exception($"Content type is not implemented correctly") diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs index 794968346..3523bb443 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs @@ -1,4 +1,3 @@ -using System; using System.Linq; using System.Xml.Linq; diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs index c097e2aa4..dae572835 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs @@ -54,8 +54,10 @@ namespace Barotrauma public int Index => ContentPackageManager.EnabledPackages.IndexOf(this); - #warning TODO: remove this, unless we truly believe that determining "multiplayer-incompatible content" is something we should do - public readonly bool HasMultiplayerIncompatibleContent; + /// + /// Does the content package include some content that needs to match between all players in multiplayer. + /// + public readonly bool HasMultiplayerSyncedContent; protected ContentPackage(XDocument doc, string path) { @@ -93,7 +95,7 @@ namespace Barotrauma .Select(f => f.Error) .ToImmutableArray(); - HasMultiplayerIncompatibleContent = Files.Any(f => !f.NotSyncedInMultiplayer); + HasMultiplayerSyncedContent = Files.Any(f => !f.NotSyncedInMultiplayer); Hash = CalculateHash(); var expectedHash = rootElement.GetAttributeString("expectedhash", ""); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs index 58d0bd8a4..bf2faed95 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs @@ -392,6 +392,10 @@ namespace Barotrauma public static void LoadVanillaFileList() { VanillaCorePackage = new CorePackage(XDocument.Load(VanillaFileList), VanillaFileList); + foreach (string error in VanillaCorePackage.Errors) + { + DebugConsole.ThrowError(error); + } } public static IEnumerable Init() diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs index 3d0e13e7a..8b328672a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs @@ -101,23 +101,27 @@ namespace Barotrauma } private static bool StringEquality(string? a, string? b) - => (a.IsNullOrEmpty() && b.IsNullOrEmpty()) || - string.Equals(Path.GetFullPath(a.CleanUpPathCrossPlatform(false) ?? ""), - Path.GetFullPath(b.CleanUpPathCrossPlatform(false) ?? ""), - StringComparison.OrdinalIgnoreCase); + { + if (a.IsNullOrEmpty() || b.IsNullOrEmpty()) + { + return a.IsNullOrEmpty() == b.IsNullOrEmpty(); + } + return string.Equals(Path.GetFullPath(a.CleanUpPathCrossPlatform(false) ?? ""), + Path.GetFullPath(b.CleanUpPathCrossPlatform(false) ?? ""), StringComparison.OrdinalIgnoreCase); + } public static bool operator==(ContentPath a, ContentPath b) - => StringEquality(a.Value, b.Value); + => StringEquality(a?.Value, b?.Value); public static bool operator!=(ContentPath a, ContentPath b) => !(a == b); public static bool operator==(ContentPath a, string? b) - => StringEquality(a.Value, b); + => StringEquality(a?.Value, b); public static bool operator!=(ContentPath a, string? b) => !(a == b); public static bool operator==(string? a, ContentPath b) - => StringEquality(a, b.Value); + => StringEquality(a, b?.Value); public static bool operator!=(string? a, ContentPath b) => !(a == b); diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index dfbf631f7..8562d6800 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -67,8 +67,13 @@ namespace Barotrauma public void Execute(string[] args) { - if (OnExecute == null) return; - if (!CheatsEnabled && IsCheat) + if (OnExecute == null) { return; } + + bool allowCheats = false; +#if CLIENT + allowCheats = GameMain.NetworkMember == null && (GameMain.GameSession?.GameMode is TestGameMode || Screen.Selected is EditorScreen); +#endif + if (!allowCheats && !CheatsEnabled && IsCheat) { NewMessage("You need to enable cheats using the command \"enablecheats\" before you can use the command \"" + names[0] + "\".", Color.Red); #if USE_STEAM @@ -603,28 +608,27 @@ namespace Barotrauma commands.Add(new Command("giveaffliction", "giveaffliction [affliction name] [affliction strength] [character name] [limb type] [use relative strength]: Add an affliction to a character. If the name parameter is omitted, the affliction is added to the controlled character.", (string[] args) => { if (args.Length < 2) { return; } - - AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a => - a.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase) || - a.Identifier == args[0]); + string affliction = args[0]; + AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a => a.Identifier == affliction); if (afflictionPrefab == null) { - ThrowError("Affliction \"" + args[0] + "\" not found."); + afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a => a.Name.Equals(affliction, StringComparison.OrdinalIgnoreCase)); + } + if (afflictionPrefab == null) + { + ThrowError("Affliction \"" + affliction + "\" not found."); return; } - if (!float.TryParse(args[1], out float afflictionStrength)) { ThrowError("\"" + args[1] + "\" is not a valid affliction strength."); return; } - bool relativeStrength = false; if (args.Length > 4) { bool.TryParse(args[4], out relativeStrength); } - Character targetCharacter = args.Length <= 2 ? Character.Controlled : FindMatchingCharacter(new string[] { args[2] }); if (targetCharacter != null) { @@ -671,7 +675,7 @@ namespace Barotrauma { return new string[][] { - Character.CharacterList.Select(c => c.Name).Distinct().ToArray() + Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray() }; }, isCheat: true)); @@ -699,7 +703,7 @@ namespace Barotrauma { return new string[][] { - Character.CharacterList.Select(c => c.Name).Distinct().ToArray() + Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray() }; }, isCheat: true)); @@ -720,7 +724,7 @@ namespace Barotrauma { return new string[][] { - Character.CharacterList.Select(c => c.Name).Distinct().ToArray() + Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray() }; }, isCheat: true)); @@ -825,7 +829,7 @@ namespace Barotrauma { Character.Controlled?.Info?.Job?.Skills?.Select(skill => skill.Identifier.Value).ToArray() ?? Array.Empty(), new[]{ "max" }, - Character.CharacterList.Select(c => c.Name).Distinct().ToArray(), + Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray(), }; })); @@ -864,7 +868,7 @@ namespace Barotrauma return new string[][] { talentNames.Select(id => id).ToArray(), - Character.CharacterList.Select(c => c.Name).Distinct().ToArray() + Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray() }; }, isCheat: true)); @@ -916,7 +920,7 @@ namespace Barotrauma return new string[][] { availableArgs.ToArray(), - Character.CharacterList.Select(c => c.Name).Distinct().ToArray() + Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray() }; }, isCheat: true)); @@ -951,7 +955,7 @@ namespace Barotrauma return new[] { new string[] { "100" }, - Character.CharacterList.Select(c => c.Name).Distinct().ToArray(), + Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray(), }; })); @@ -1066,7 +1070,7 @@ namespace Barotrauma { return new string[][] { - Character.CharacterList.Select(c => c.Name).Distinct().ToArray() + Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray() }; }, isCheat: true)); @@ -1413,7 +1417,7 @@ namespace Barotrauma { return new string[][] { - Character.CharacterList.Select(c => c.Name).Distinct().ToArray() + Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray() }; }, isCheat: true)); @@ -1455,7 +1459,7 @@ namespace Barotrauma { return new string[][] { - Character.CharacterList.Where(c => c.IsDead).Select(c => c.Name).Distinct().ToArray() + Character.CharacterList.Where(c => c.IsDead).Select(c => c.Name).Distinct().OrderBy(n => n).ToArray() }; }, isCheat: true)); @@ -1467,7 +1471,7 @@ namespace Barotrauma return new string[][] { GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(), - Character.CharacterList.Select(c => c.Name).Distinct().ToArray() + Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray() }; })); @@ -1538,14 +1542,14 @@ namespace Barotrauma NewMessage((GameMain.GameSession.Map.AllowDebugTeleport ? "Enabled" : "Disabled") + " teleportation on the campaign map.", Color.White); }, isCheat: true)); - commands.Add(new Command("money", "money [amount]: Gives the specified amount of money to the crew when a campaign is active.", args => + commands.Add(new Command("money", "money [amount] [character]: Gives the specified amount of money to the crew when a campaign is active.", args => { if (args.Length == 0) { return; } if (GameMain.GameSession?.GameMode is CampaignMode campaign) { if (int.TryParse(args[0], out int money)) { - campaign.Money += money; + campaign.Bank.Give(money); GameAnalyticsManager.AddMoneyGainedEvent(money, GameAnalyticsManager.MoneySource.Cheat, "console"); } else @@ -1553,8 +1557,23 @@ namespace Barotrauma ThrowError($"\"{args[0]}\" is not a valid numeric value."); } } + }, isCheat: true, getValidArgs: () => new [] + { + new []{ string.Empty }, + Character.CharacterList.Select(c => c.Name).Distinct().ToArray() + })); + + commands.Add(new Command("showmoney", "showmoney: Shows the amount of money in everyones wallet.", args => + { + if (!(GameMain.GameSession?.GameMode is CampaignMode campaign)) + { + ThrowError("No campaign active!"); + return; + } + + NewMessage($"Bank: {campaign.Bank.Balance}"); }, isCheat: true)); - + commands.Add(new Command("skipeventcooldown", "skipeventcooldown: Skips the currently active event cooldown and triggers pending monster spawns immediately.", args => { GameMain.GameSession?.EventManager?.SkipEventCooldown(); @@ -1968,7 +1987,7 @@ namespace Barotrauma } } - private static string[] ListCharacterNames() => Character.CharacterList.OrderBy(c => c.IsDead).ThenByDescending(c => c.IsHuman).Select(c => c.Name).Distinct().ToArray(); + private static string[] ListCharacterNames() => Character.CharacterList.OrderBy(c => c.IsDead).ThenByDescending(c => c.IsHuman).ThenBy(c => c.Name).Select(c => c.Name).Distinct().ToArray(); private static Character FindMatchingCharacter(string[] args, bool ignoreRemotePlayers = false, Client allowedRemotePlayer = null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs index 3feeabf1a..eb6756fe0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs @@ -87,7 +87,7 @@ namespace Barotrauma #if SERVER if (GameMain.Server != null) { - Entity.Spawner.CreateNetworkEvent(item, false); + Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item)); } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMoneyAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMoneyAction.cs index 8d3141adf..ac6eef6b2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMoneyAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMoneyAction.cs @@ -1,4 +1,7 @@ +using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; +using Barotrauma.Networking; namespace Barotrauma { @@ -7,15 +10,36 @@ namespace Barotrauma [Serialize(0, IsPropertySaveable.Yes)] public int Amount { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } + public CheckMoneyAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } protected override bool? DetermineSuccess() { + Client matchingClient = null; + bool hasTag = !TargetTag.IsEmpty; +#if SERVER + IEnumerable targets = ParentEvent.GetTargets(TargetTag); + + if (hasTag) + { + foreach (Entity entity in targets) + { + if (entity is Character && GameMain.Server?.ConnectedClients.FirstOrDefault(c => c.Character == entity) is { } matchingCharacter) + { + matchingClient = matchingCharacter; + break; + } + } + } +#endif + if (GameMain.GameSession?.GameMode is CampaignMode campaign) { - return campaign.Money >= Amount; + return !hasTag ? campaign.Bank.CanAfford(Amount) : campaign.GetWallet(matchingClient).CanAfford(Amount); } return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs index 00c439419..c3b9fad93 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs @@ -182,7 +182,7 @@ namespace Barotrauma speaker.ActiveConversation = null; speaker.SetCustomInteract(null, null); #if SERVER - GameMain.NetworkMember.CreateEntityEvent(speaker, new object[] { NetEntityEvent.Type.AssignCampaignInteraction }); + GameMain.NetworkMember.CreateEntityEvent(speaker, new Character.AssignCampaignInteractionEventData()); #endif var humanAI = speaker.AIController as HumanAIController; if (humanAI != null && !speaker.IsDead && !speaker.Removed) @@ -259,7 +259,7 @@ namespace Barotrauma speaker.SetCustomInteract( TryStartConversation, TextManager.Get("CampaignInteraction.Talk")); - GameMain.NetworkMember.CreateEntityEvent(speaker, new object[] { NetEntityEvent.Type.AssignCampaignInteraction }); + GameMain.NetworkMember.CreateEntityEvent(speaker, new Character.AssignCampaignInteractionEventData()); #endif } return; @@ -369,7 +369,7 @@ namespace Barotrauma speaker.CampaignInteractionType = CampaignMode.InteractionType.None; speaker.SetCustomInteract(null, null); #if SERVER - GameMain.NetworkMember.CreateEntityEvent(speaker, new object[] { NetEntityEvent.Type.AssignCampaignInteraction }); + GameMain.NetworkMember.CreateEntityEvent(speaker, new Character.AssignCampaignInteractionEventData()); #endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs index ae7273383..c871e45ab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs @@ -1,5 +1,9 @@ using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; +using Barotrauma.Networking; namespace Barotrauma { @@ -10,6 +14,9 @@ namespace Barotrauma [Serialize(0, IsPropertySaveable.Yes)] public int Amount { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } + private bool isFinished; public override bool IsFinished(ref string goTo) @@ -25,13 +32,44 @@ namespace Barotrauma { if (isFinished) { return; } +#if SERVER + bool hasTag = !TargetTag.IsEmpty; + List matchingClients = new List(); + if (hasTag) + { + IEnumerable targets = ParentEvent.GetTargets(TargetTag); + + foreach (Entity entity in targets) + { + if (entity is Character && GameMain.Server?.ConnectedClients.FirstOrDefault(c => c.Character == entity) is { } matchingCharacter) + { + matchingClients.Add(matchingCharacter); + break; + } + } + } +#endif + if (GameMain.GameSession?.GameMode is CampaignMode campaign) { - campaign.Money += Amount; - GameAnalyticsManager.AddMoneyGainedEvent(Amount, GameAnalyticsManager.MoneySource.Event, ParentEvent.Prefab.Identifier.Value); #if SERVER - (campaign as MultiPlayerCampaign).LastUpdateID++; + if (!hasTag) + { + campaign.Bank.Give(Amount); + } + else + { + foreach (Client client in matchingClients) + { + campaign.GetWallet(client).Give(Amount); + } + } + + ((MultiPlayerCampaign)campaign).LastUpdateID++; +#else + campaign.Wallet.Give(Amount); #endif + GameAnalyticsManager.AddMoneyGainedEvent(Amount, GameAnalyticsManager.MoneySource.Event, ParentEvent.Prefab.Identifier.Value); } isFinished = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs index d4a5f8ed7..90e6eb540 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs @@ -61,7 +61,7 @@ namespace Barotrauma npc.GiveIdCardTags(subWaypoint, createNetworkEvent: true); } #if SERVER - GameMain.NetworkMember.CreateEntityEvent(npc, new object[] { NetEntityEvent.Type.AddToCrew, TeamTag, npc.Inventory.AllItems.Select(it => it.ID).ToArray() }); + GameMain.NetworkMember.CreateEntityEvent(npc, new Character.AddToCrewEventData(TeamTag, npc.Inventory.AllItems)); #endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs index 470d6a129..09a689776 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs @@ -145,7 +145,7 @@ namespace Barotrauma npc.SetCustomInteract( (speaker, player) => { if (e1 == speaker) { Trigger(speaker, player); } else { Trigger(player, speaker); } }, TextManager.Get("CampaignInteraction.Talk")); - GameMain.NetworkMember.CreateEntityEvent(npc, new object[] { NetEntityEvent.Type.AssignCampaignInteraction }); + GameMain.NetworkMember.CreateEntityEvent(npc, new Character.AssignCampaignInteractionEventData()); #endif } if (!AllowMultipleTargets) { return; } @@ -194,7 +194,7 @@ namespace Barotrauma npc.SetCustomInteract(null, null); npc.RequireConsciousnessForCustomInteract = true; #if SERVER - GameMain.NetworkMember.CreateEntityEvent(npc, new object[] { NetEntityEvent.Type.AssignCampaignInteraction }); + GameMain.NetworkMember.CreateEntityEvent(npc, new Character.AssignCampaignInteractionEventData()); #endif } else if (npcOrItem.TryGet(out Item item)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index d979fc275..8bbb511bf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -460,9 +460,12 @@ namespace Barotrauma selectedEvents[eventSet].Add(newEvent); } + Location location = (GameMain.GameSession?.GameMode as CampaignMode)?.Map?.CurrentLocation ?? level?.StartLocation; foreach (EventSet childEventSet in eventSet.ChildSets) { - CreateEvents(childEventSet, rand); + if (!IsValidForLevel(childEventSet, level)) { continue; } + if (location != null && !IsValidForLocation(childEventSet, location)) { continue; } + CreateEvents(childEventSet, rand); } } } @@ -474,10 +477,7 @@ namespace Barotrauma Random rand = random ?? new MTRandom(ToolBox.StringToInt(level.Seed)); var allowedEventSets = - eventSets.Where(es => - level.Difficulty >= es.MinLevelDifficulty && level.Difficulty <= es.MaxLevelDifficulty && - level.LevelData.Type == es.LevelType && - (es.BiomeIdentifier.IsEmpty || es.BiomeIdentifier == level.LevelData.Biome.Identifier)); + eventSets.Where(set => IsValidForLevel(set, level)); if (requireCampaignSet.HasValue) { @@ -501,13 +501,9 @@ namespace Barotrauma } Location location = (GameMain.GameSession?.GameMode as CampaignMode)?.Map?.CurrentLocation ?? level?.StartLocation; - LocationType locationType = location?.GetLocationType(); - if (location != null) { - allowedEventSets = allowedEventSets.Where(set => - set.LocationTypeIdentifiers == null || - set.LocationTypeIdentifiers.Any(identifier => identifier == locationType.Identifier)); + allowedEventSets = allowedEventSets.Where(set => IsValidForLocation(set, location)); } float totalCommonness = allowedEventSets.Sum(e => e.GetCommonness(level)); @@ -526,6 +522,20 @@ namespace Barotrauma return null; } + private bool IsValidForLevel(EventSet eventSet, Level level) + { + return + level.Difficulty >= eventSet.MinLevelDifficulty && level.Difficulty <= eventSet.MaxLevelDifficulty && + level.LevelData.Type == eventSet.LevelType && + (eventSet.BiomeIdentifier.IsEmpty || eventSet.BiomeIdentifier == level.LevelData.Biome.Identifier); + } + + private bool IsValidForLocation(EventSet eventSet, Location location) + { + return eventSet.LocationTypeIdentifiers == null || + eventSet.LocationTypeIdentifiers.Any(identifier => identifier == location.GetLocationType().Identifier); + } + private bool CanStartEventSet(EventSet eventSet) { ISpatialEntity refEntity = GetRefEntity(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index 2d91d363c..96bb78082 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -210,7 +210,7 @@ namespace Barotrauma Additive = element.GetAttributeBool("additive", false); - string levelTypeStr = element.GetAttributeString("leveltype", "LocationConnection"); + string levelTypeStr = element.GetAttributeString("leveltype", parentSet?.LevelType.ToString() ?? "LocationConnection"); if (!Enum.TryParse(levelTypeStr, true, out LevelType)) { DebugConsole.ThrowError($"Error in event set \"{Identifier}\". \"{levelTypeStr}\" is not a valid level type."); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index 0bccbf953..40c00e220 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -212,7 +212,7 @@ namespace Barotrauma { return null; } - + int probabilitySum = allowedMissions.Sum(m => m.Commonness); int randomNumber = rand.NextInt32() % probabilitySum; foreach (MissionPrefab missionPrefab in allowedMissions) @@ -377,10 +377,16 @@ namespace Barotrauma crewCharacters.ForEach(c => missionMoneyGainMultiplier.Value += c.GetStatValue(StatTypes.MissionMoneyGainMultiplier)); int totalReward = (int)(reward * missionMoneyGainMultiplier.Value); - campaign.Money += totalReward; - GameAnalyticsManager.AddMoneyGainedEvent(totalReward, GameAnalyticsManager.MoneySource.MissionReward, Prefab.Identifier.Value); +#if SERVER + totalReward = DistributeRewardsToCrew(GetSalaryEligibleCrew(), totalReward); +#endif + if (totalReward > 0) + { + campaign.Bank.Give(totalReward); + } + foreach (Character character in crewCharacters) { character.Info.MissionsCompletedSinceDeath++; @@ -409,6 +415,57 @@ namespace Barotrauma } } +#if SERVER + public static int DistributeRewardsToCrew(IEnumerable crew, int totalReward) + { + int remainingRewards = totalReward; + HashSet nonBotCrew = crew.Where(c => !c.IsBot).ToHashSet(); + float sum = nonBotCrew.Sum(c => c.Wallet.RewardDistribution); + if (sum == 0) { return remainingRewards; } + foreach (Character character in nonBotCrew) + { + float rewardWeight = character.Wallet.RewardDistribution / sum; + int reward = (int)Math.Floor(totalReward * rewardWeight); + reward = Math.Max(remainingRewards, reward); + character.Wallet.Give(reward); + remainingRewards -= reward; + if (0 >= remainingRewards) { break; } + } + + return remainingRewards; + } +#endif + + public static IEnumerable GetSalaryEligibleCrew() + { + if (!(GameMain.GameSession.CrewManager is { } crewManager)) { return Array.Empty(); } + + IEnumerable characters = crewManager.GetCharacters(); +#if SERVER + return GameMain.Server.ConnectedClients.Select(c => c.Character).Where(IsAlive).Concat(characters); +#elif CLIENT + return characters; +#endif + static bool IsAlive(Character c) { return c.Info != null && !c.IsDead; } + } + + + public static (int Amount, int Percentage) GetRewardShare(int rewardDistribution, IEnumerable crew, Option reward) + { + float sum = crew.Sum(c => c.Wallet.RewardDistribution) + rewardDistribution; + if (sum == 0) { return (0, 0); } + + float rewardWeight = rewardDistribution / sum; + int rewardPercentage = (int)(rewardWeight * 100); + + return reward switch + { + Some { Value: var amount } => ((int)(amount * rewardWeight), rewardPercentage), + None _ => (0, rewardPercentage), + _ => throw new ArgumentOutOfRangeException() + }; + } + protected void ChangeLocationType(LocationTypeChange change) { if (change == null) { throw new ArgumentException(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs index 82a9f705e..d79130b93 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs @@ -361,7 +361,7 @@ namespace Barotrauma if (!SendUserStatistics) { return; } if (sentEventIdentifiers.Contains(identifier)) { return; } - if (GameMain.VanillaContent == null || ContentPackageManager.EnabledPackages.All.Any(p => p.HasMultiplayerIncompatibleContent && p != GameMain.VanillaContent)) + if (GameMain.VanillaContent == null || ContentPackageManager.EnabledPackages.All.Any(p => p.HasMultiplayerSyncedContent && p != GameMain.VanillaContent)) { message = "[MODDED] " + message; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index d1e7c70d0..7311d6c05 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -168,7 +168,7 @@ namespace Barotrauma foreach (Item spawnedItem in spawnedItems) { #if SERVER - Entity.Spawner.CreateNetworkEvent(spawnedItem, remove: false); + Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(spawnedItem)); #endif foreach (ItemComponent ic in spawnedItem.Components) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index 234bbefa9..585de85c8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Barotrauma.Networking; #if SERVER using Barotrauma.Networking; #endif @@ -18,12 +19,34 @@ namespace Barotrauma public int Quantity { get; set; } public bool? IsStoreComponentEnabled { get; set; } - public PurchasedItem(ItemPrefab itemPrefab, int quantity) + public readonly int BuyerCharacterInfoId; + + public PurchasedItem(ItemPrefab itemPrefab, int quantity, int buyerCharacterInfoId) { ItemPrefab = itemPrefab; Quantity = quantity; IsStoreComponentEnabled = null; + BuyerCharacterInfoId = buyerCharacterInfoId; } + +#if CLIENT + public PurchasedItem(ItemPrefab itemPrefab, int quantity, Client buyer = null) + { + ItemPrefab = itemPrefab; + Quantity = quantity; + IsStoreComponentEnabled = null; + BuyerCharacterInfoId = buyer?.Character?.Info?.ID ?? Character.Controlled?.Info?.ID ?? 0; + } +#elif SERVER + public PurchasedItem(ItemPrefab itemPrefab, int quantity, Client buyer) + { + ItemPrefab = itemPrefab; + Quantity = quantity; + IsStoreComponentEnabled = null; + BuyerCharacterInfoId = buyer?.Character?.Info?.ID ?? 0; + } +#endif + } class SoldItem @@ -156,7 +179,7 @@ namespace Barotrauma OnPurchasedItemsChanged?.Invoke(); } - public void ModifyItemQuantityInBuyCrate(ItemPrefab itemPrefab, int changeInQuantity) + public void ModifyItemQuantityInBuyCrate(ItemPrefab itemPrefab, int changeInQuantity, Client client = null) { var itemInCrate = ItemsInBuyCrate.Find(i => i.ItemPrefab == itemPrefab); if (itemInCrate != null) @@ -167,15 +190,15 @@ namespace Barotrauma ItemsInBuyCrate.Remove(itemInCrate); } } - else if(changeInQuantity > 0) + else if (changeInQuantity > 0) { - itemInCrate = new PurchasedItem(itemPrefab, changeInQuantity); + itemInCrate = new PurchasedItem(itemPrefab, changeInQuantity, client); ItemsInBuyCrate.Add(itemInCrate); } OnItemsInBuyCrateChanged?.Invoke(); } - public void ModifyItemQuantityInSubSellCrate(ItemPrefab itemPrefab, int changeInQuantity) + public void ModifyItemQuantityInSubSellCrate(ItemPrefab itemPrefab, int changeInQuantity, Client client = null) { var itemInCrate = ItemsInSellFromSubCrate.Find(i => i.ItemPrefab == itemPrefab); if (itemInCrate != null) @@ -188,13 +211,13 @@ namespace Barotrauma } else if (changeInQuantity > 0) { - itemInCrate = new PurchasedItem(itemPrefab, changeInQuantity); + itemInCrate = new PurchasedItem(itemPrefab, changeInQuantity, client); ItemsInSellFromSubCrate.Add(itemInCrate); } OnItemsInSellFromSubCrateChanged?.Invoke(); } - public void PurchaseItems(List itemsToPurchase, bool removeFromCrate) + public void PurchaseItems(List itemsToPurchase, bool removeFromCrate, Client client = null) { // Check all the prices before starting the transaction // to make sure the modifiers stay the same for the whole transaction @@ -210,13 +233,13 @@ namespace Barotrauma } else { - purchasedItem = new PurchasedItem(item.ItemPrefab, item.Quantity); + purchasedItem = new PurchasedItem(item.ItemPrefab, item.Quantity, client); PurchasedItems.Add(purchasedItem); } // Exchange money var itemValue = item.Quantity * buyValues[item.ItemPrefab]; - campaign.Money -= itemValue; + campaign.GetWallet(client).TryDeduct(itemValue); GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier.Value); Location.StoreCurrentBalance += itemValue; @@ -427,7 +450,7 @@ namespace Barotrauma #if SERVER if (GameMain.Server != null) { - Entity.Spawner.CreateNetworkEvent(itemContainer.Item, false); + Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(itemContainer.Item)); } #endif } @@ -438,7 +461,7 @@ namespace Barotrauma itemSpawned(item); #if SERVER - Entity.Spawner?.CreateNetworkEvent(item, false); + Entity.Spawner?.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item)); #endif (itemContainer?.Item ?? item).CampaignInteractionType = CampaignMode.InteractionType.Cargo; static void itemSpawned(Item item) @@ -491,7 +514,8 @@ namespace Barotrauma if (item?.ItemPrefab == null) { continue; } itemsElement.Add(new XElement("item", new XAttribute("id", item.ItemPrefab.Identifier), - new XAttribute("qty", item.Quantity))); + new XAttribute("qty", item.Quantity), + new XAttribute("buyer", item.BuyerCharacterInfoId))); } parentElement.Add(itemsElement); } @@ -503,12 +527,15 @@ namespace Barotrauma { foreach (XElement itemElement in element.GetChildElements("item")) { - var id = itemElement.GetAttributeString("id", null); + string id = itemElement.GetAttributeString("id", null); if (string.IsNullOrWhiteSpace(id)) { continue; } var prefab = ItemPrefab.Prefabs.Find(p => p.Identifier == id); if (prefab == null) { continue; } - var qty = itemElement.GetAttributeInt("qty", 0); - purchasedItems.Add(new PurchasedItem(prefab, qty)); + int qty = itemElement.GetAttributeInt("qty", 0); + int buyerId = itemElement.GetAttributeInt("buyer", 0); + + purchasedItems.Add(new PurchasedItem(prefab, qty, buyerId)); + } } SetPurchasedItems(purchasedItems); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index 6e38a47f3..6b4cc9fd5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -20,6 +20,16 @@ namespace Barotrauma private readonly List characterInfos = new List(); private readonly List characters = new List(); + public IEnumerable GetCharacters() + { + return characters; + } + + public IEnumerable GetCharacterInfos() + { + return characterInfos; + } + private Character welcomeMessageNPC; public List CharacterInfos => characterInfos; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs index c31f37f60..581566f00 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs @@ -1,8 +1,6 @@ #nullable enable -using System; -using System.Collections.Generic; -using System.Xml.Linq; using Microsoft.Xna.Framework; +using System; namespace Barotrauma { @@ -27,6 +25,8 @@ namespace Barotrauma public LocalizedString Description { get; } public LocalizedString ShortDescription { get; } + public int MenuOrder { get; } + /// /// How low the reputation can drop on this faction /// @@ -52,6 +52,7 @@ namespace Barotrauma public FactionPrefab(ContentXElement element, FactionsFile file) : base(file, element.GetAttributeIdentifier("identifier", string.Empty)) { + MenuOrder = element.GetAttributeInt("menuorder", 0); MinReputation = element.GetAttributeInt("minreputation", -100); MaxReputation = element.GetAttributeInt("maxreputation", 100); InitialReputation = element.GetAttributeInt("initialreputation", 0); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs index 820d8227e..3e33d0669 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs @@ -164,7 +164,7 @@ namespace Barotrauma ("[reputationvalue]", ((int)Math.Round(value)).ToString())); if (addColorTags) { - formattedReputation = $"‖color:{XMLExtensions.ColorToString(GetReputationColor(normalizedValue))}‖"+ formattedReputation+"‖end‖"; + formattedReputation = $"‖color:{XMLExtensions.ToStringHex(GetReputationColor(normalizedValue))}‖{formattedReputation}‖end‖"; } return formattedReputation; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs new file mode 100644 index 000000000..d32c25575 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs @@ -0,0 +1,193 @@ +using System; +using System.Xml.Linq; +using Barotrauma.Networking; + +namespace Barotrauma +{ + internal readonly struct WalletChangedEvent + { + public readonly Wallet Wallet; + public readonly WalletInfo Info; + public readonly WalletChangedData ChangedData; + + public WalletChangedEvent(Wallet wallet, WalletChangedData changedData, WalletInfo info) + { + Wallet = wallet; + Info = info; + ChangedData = changedData; + } + } + + [NetworkSerialize] + internal struct WalletInfo : INetSerializableStruct + { + public int RewardDistribution; + public int Balance; + } + + internal struct NetWalletUpdate : INetSerializableStruct + { + [NetworkSerialize(ArrayMaxSize = NetConfig.MaxPlayers + 1)] + public NetWalletTransaction[] Transactions; + } + + [NetworkSerialize] + internal struct NetWalletTransfer : INetSerializableStruct + { + public Option Sender; + public Option Receiver; + public int Amount; + } + + internal struct NetWalletSalaryUpdate : INetSerializableStruct + { + [NetworkSerialize] + public ushort Target; + + [NetworkSerialize(MinValueInt = 0, MaxValueInt = 100)] + public int NewRewardDistribution; + } + + [NetworkSerialize] + internal struct WalletChangedData : INetSerializableStruct + { + public Option RewardDistributionChanged; + public Option BalanceChanged; + + public WalletChangedData MergeInto(WalletChangedData other) + { + other.BalanceChanged = AddOptionalInt(other.BalanceChanged, BalanceChanged); + other.RewardDistributionChanged = AddOptionalInt(other.RewardDistributionChanged, RewardDistributionChanged); + return other; + + static Option AddOptionalInt(Option a, Option b) + { + return a switch + { + Some some1 => b switch + { + Some some2 => Option.Some(some1.Value + some2.Value), + None _ => Option.Some(some1.Value), + _ => throw new ArgumentOutOfRangeException(nameof(b)) + }, + None _ => b switch + { + Some some1 => Option.Some(some1.Value), + None _ => Option.None(), + _ => throw new ArgumentOutOfRangeException(nameof(b)) + }, + _ => throw new ArgumentOutOfRangeException(nameof(a)) + }; + } + } + } + + [NetworkSerialize] + internal struct NetWalletTransaction : INetSerializableStruct + { + public Option CharacterID; + public WalletChangedData ChangedData; + public WalletInfo Info; + } + + // ReSharper disable ValueParameterNotUsed + internal sealed class InvalidWallet : Wallet + { + public override int Balance + { + get => 0; + set => new InvalidOperationException("Tried to set the balance on an invalid wallet"); + } + + public override int RewardDistribution + { + get => 0; + set => new InvalidOperationException("Tried to set the reward distribution on an invalid wallet"); + } + } + + internal partial class Wallet + { + public static readonly Wallet Invalid = new InvalidWallet(); + + public const string LowerCaseSaveElementName = "wallet"; + + private const string AttributeNameBalance = "balance", + AttrubuteNameRewardDistribution = "rewarddistribution", + SaveElementName = "Wallet"; + + private int balance; + + public virtual int Balance + { + get => balance; + set => balance = ClampBalance(value); + } + + private int rewardDistribution; + + public virtual int RewardDistribution + { + get => rewardDistribution; + set => rewardDistribution = ClampRewardDistribution(value); + } + + public Wallet() { } + + public Wallet(XElement element) + { + balance = ClampBalance(element.GetAttributeInt(AttributeNameBalance, 0)); + rewardDistribution = ClampBalance(element.GetAttributeInt(AttrubuteNameRewardDistribution, 0)); + } + + public XElement Save() + { + XElement element = new XElement(SaveElementName, new XAttribute(AttributeNameBalance, Balance), new XAttribute(AttrubuteNameRewardDistribution, RewardDistribution)); + return element; + } + + public bool TryDeduct(int price) + { + if (!CanAfford(price)) { return false; } + + Deduct(price); + return true; + } + + public bool CanAfford(int price) => Balance >= price; + public void Refund(int price) => Give(price); + + public void Give(int amount) + { + Balance += amount; + SettingsChanged(balanceChanged: Option.Some(amount), rewardChanged: Option.None()); + } + + public void Deduct(int price) + { + Balance -= price; + SettingsChanged(balanceChanged: Option.Some(-price), rewardChanged: Option.None()); + } + + public void SetRewardDistrubiton(int value) + { + int oldValue = RewardDistribution; + RewardDistribution = value; + SettingsChanged(balanceChanged: Option.None(), rewardChanged: Option.Some(RewardDistribution - oldValue)); + } + + public WalletInfo CreateWalletInfo() + { + return new WalletInfo + { + Balance = Balance, + RewardDistribution = RewardDistribution + }; + } + + partial void SettingsChanged(Option balanceChanged, Option rewardChanged); + + private static int ClampBalance(int value) => Math.Clamp(value, 0, CampaignMode.MaxMoney); + private static int ClampRewardDistribution(int value) => Math.Clamp(value, 0, 100); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index b384c847f..dbde33069 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -68,7 +68,7 @@ namespace Barotrauma abstract partial class CampaignMode : GameMode { - const int MaxMoney = int.MaxValue / 2; //about 1 billion + public const int MaxMoney = int.MaxValue / 2; //about 1 billion public const int InitialMoney = 8500; //duration of the cinematic + credits at the end of the campaign @@ -102,6 +102,8 @@ namespace Barotrauma private readonly List extraMissions = new List(); + public readonly NamedEvent OnMoneyChanged = new NamedEvent(); + public enum TransitionType { None, @@ -167,12 +169,7 @@ namespace Barotrauma } } - private int money; - public int Money - { - get { return money; } - set { money = MathHelper.Clamp(value, 0, MaxMoney); } - } + public Wallet Bank; public LevelData NextLevel { @@ -183,11 +180,20 @@ namespace Barotrauma protected CampaignMode(GameModePreset preset) : base(preset) { - Money = InitialMoney; + Bank = new Wallet + { + Balance = InitialMoney + }; + CargoManager = new CargoManager(this); MedicalClinic = new MedicalClinic(this); } + public virtual Wallet GetWallet(Client client = null) + { + return Bank; + } + /// /// The location that's displayed as the "current one" in the map screen. Normally the current outpost or the location at the start of the level, /// but when selecting the next destination at the end of the level at an uninhabited location we use the location at the end @@ -200,7 +206,7 @@ namespace Barotrauma { return Level.Loaded.EndLocation; } - return Level.Loaded?.StartLocation ?? Map.CurrentLocation; + return Level.Loaded?.StartLocation ?? Map.CurrentLocation; } public List GetSubsToLeaveBehind(Submarine leavingSub) @@ -255,8 +261,6 @@ namespace Barotrauma PurchasedLostShuttles = false; var connectedSubs = Submarine.MainSub.GetConnectedSubs(); wasDocked = Level.Loaded.StartOutpost != null && connectedSubs.Contains(Level.Loaded.StartOutpost); - - ResetTalentData(); } public void InitCampaignData() @@ -702,21 +706,20 @@ namespace Barotrauma string eventId = "FinishCampaign:"; GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none")); GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0)); - GameAnalyticsManager.AddDesignEvent(eventId + "Money", Money); + GameAnalyticsManager.AddDesignEvent(eventId + "Money", Bank.Balance); GameAnalyticsManager.AddDesignEvent(eventId + "Playtime", TotalPlayTime); GameAnalyticsManager.AddDesignEvent(eventId + "PassedLevels", TotalPassedLevels); } protected virtual void EndCampaignProjSpecific() { } - public bool TryHireCharacter(Location location, CharacterInfo characterInfo) + public bool TryHireCharacter(Location location, CharacterInfo characterInfo, Client client = null) { if (characterInfo == null) { return false; } - if (Money < characterInfo.Salary) { return false; } + if (!GetWallet(client).TryDeduct(characterInfo.Salary)) { return false; } characterInfo.IsNewHire = true; location.RemoveHireableCharacter(characterInfo); CrewManager.AddCharacterInfo(characterInfo); - Money -= characterInfo.Salary; GameAnalyticsManager.AddMoneySpentEvent(characterInfo.Salary, GameAnalyticsManager.MoneySink.Crew, characterInfo.Job?.Prefab.Identifier.Value ?? "unknown"); return true; } @@ -740,8 +743,7 @@ namespace Barotrauma HumanAIController humanAI = npc.AIController as HumanAIController; if (humanAI == null) { yield return CoroutineStatus.Success; } - var waitOrderPrefab = OrderPrefab.Prefabs["wait"]; - var waitOrder = new Order(waitOrderPrefab, Identifier.Empty, null, orderGiver: null); + var waitOrder = OrderPrefab.Prefabs["wait"].CreateInstance(OrderPrefab.OrderTargetType.Entity); humanAI.SetForcedOrder(waitOrder); var waitObjective = humanAI.ObjectiveManager.ForcedOrder; humanAI.FaceTarget(interactor); @@ -856,7 +858,7 @@ namespace Barotrauma { GameMain.Server.SendDirectChatMessage(Networking.ChatMessage.Create( - TextManager.Get("RadioAnnouncerName").Value, + TextManager.Get("RadioAnnouncerName").Value, TextManager.Get("TooFarFromOutpostWarning").Value, Networking.ChatMessageType.Default, null), c); } #endif @@ -906,7 +908,7 @@ namespace Barotrauma public void LogState() { DebugConsole.NewMessage("********* CAMPAIGN STATUS *********", Color.White); - DebugConsole.NewMessage(" Money: " + Money, Color.White); + DebugConsole.NewMessage(" Money: " + Bank.Balance, Color.White); DebugConsole.NewMessage(" Current location: " + map.CurrentLocation.Name, Color.White); DebugConsole.NewMessage(" Available destinations: ", Color.White); @@ -960,13 +962,5 @@ namespace Barotrauma } } - // Talent relevant data, only stored for the duration of the mission - private void ResetTalentData() - { - CrewHasDied = false; - } - - public bool CrewHasDied { get; set; } - } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CharacterCampaignData.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CharacterCampaignData.cs index 0df3b6f0c..cc4948562 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CharacterCampaignData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CharacterCampaignData.cs @@ -26,33 +26,6 @@ namespace Barotrauma private XElement itemData; private XElement healthData; public XElement OrderData { get; private set; } - - public void Refresh(Character character) - { - healthData = new XElement("health"); - character.CharacterHealth.Save(healthData); - if (character.Inventory != null) - { - itemData = new XElement("inventory"); - Character.SaveInventory(character.Inventory, itemData); - } - OrderData = new XElement("orders"); - CharacterInfo.SaveOrderData(character.Info, OrderData); - } - - public XElement Save() - { - XElement element = new XElement("CharacterCampaignData", - new XAttribute("name", Name), - new XAttribute("endpoint", ClientEndPoint), - new XAttribute("steamid", SteamID)); - - CharacterInfo?.Save(element); - if (itemData != null) { element.Add(itemData); } - if (healthData != null) { element.Add(healthData); } - if (OrderData != null) { element.Add(OrderData); } - - return element; - } + public XElement WalletData; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index ab5ea60ed..81c8cfbf2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -99,7 +99,6 @@ namespace Barotrauma /// private void Load(XElement element) { - Money = element.GetAttributeInt("money", 0); PurchasedLostShuttles = element.GetAttributeBool("purchasedlostshuttles", false); PurchasedHullRepairs = element.GetAttributeBool("purchasedhullrepairs", false); PurchasedItemRepairs = element.GetAttributeBool("purchaseditemrepairs", false); @@ -166,6 +165,9 @@ namespace Barotrauma case "stats": LoadStats(subElement); break; + case Wallet.LowerCaseSaveElementName: + Bank = new Wallet(subElement); + break; #if SERVER case "savedexperiencepoints": foreach (XElement savedExp in subElement.Elements()) @@ -177,6 +179,15 @@ namespace Barotrauma } } + int oldMoney = element.GetAttributeInt("money", 0); + if (oldMoney > 0) + { + Bank = new Wallet + { + Balance = oldMoney + }; + } + CampaignMetadata ??= new CampaignMetadata(this); UpgradeManager ??= new UpgradeManager(this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index d9188c9b1..0da770a21 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; +using Barotrauma.Networking; namespace Barotrauma { @@ -30,10 +31,14 @@ namespace Barotrauma private readonly List missions = new List(); public IEnumerable Missions { get { return missions; } } + private readonly HashSet casualties = new HashSet(); + public IEnumerable Casualties { get { return casualties; } } + + public CharacterTeamType? WinningTeam; public bool IsRunning { get; private set; } - + public bool RoundEnding { get; private set; } public Level? Level { get; private set; } @@ -201,7 +206,8 @@ namespace Barotrauma var campaign = MultiPlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub, settings); if (selectedSub != null) { - campaign.Money = Math.Max(MultiPlayerCampaign.MinimumInitialMoney, campaign.Money - selectedSub.Price); + campaign.Bank.TryDeduct(selectedSub.Price); + campaign.Bank.Balance = Math.Max(campaign.Bank.Balance, MultiPlayerCampaign.MinimumInitialMoney); } return campaign; } @@ -211,7 +217,8 @@ namespace Barotrauma var campaign = SinglePlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub, settings); if (selectedSub != null) { - campaign.Money = Math.Max(SinglePlayerCampaign.MinimumInitialMoney, campaign.Money - selectedSub.Price); + campaign.Bank.TryDeduct(selectedSub.Price); + campaign.Bank.Balance = Math.Max(campaign.Bank.Balance, MultiPlayerCampaign.MinimumInitialMoney); } return campaign; } @@ -264,7 +271,7 @@ namespace Barotrauma /// /// Switch to another submarine. The sub is loaded when the next round starts. /// - public SubmarineInfo SwitchSubmarine(SubmarineInfo newSubmarine, int cost) + public SubmarineInfo SwitchSubmarine(SubmarineInfo newSubmarine, int cost, Client? client = null) { if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name)) { @@ -283,19 +290,21 @@ namespace Barotrauma } } - Campaign!.Money -= cost; + if ((GameMain.NetworkMember is null || GameMain.NetworkMember is { IsServer: true }) && cost > 0) + { + Campaign!.GetWallet(client).TryDeduct(cost); + } GameAnalyticsManager.AddMoneySpentEvent(cost, GameAnalyticsManager.MoneySink.SubmarineSwitch, newSubmarine.Name); return newSubmarine; } - public void PurchaseSubmarine(SubmarineInfo newSubmarine) + public void PurchaseSubmarine(SubmarineInfo newSubmarine, Client? client = null) { if (Campaign is null) { return; } - if (Campaign.Money < newSubmarine.Price) { return; } + if ((GameMain.NetworkMember is null || GameMain.NetworkMember is { IsServer: true }) && !Campaign.GetWallet(client).TryDeduct(newSubmarine.Price)) { return; } if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name)) { - Campaign.Money -= newSubmarine.Price; GameAnalyticsManager.AddMoneySpentEvent(newSubmarine.Price, GameAnalyticsManager.MoneySink.SubmarinePurchase, newSubmarine.Name); OwnedSubmarines.Add(newSubmarine); } @@ -346,7 +355,7 @@ namespace Barotrauma public void StartRound(LevelData? levelData, bool mirrorLevel = false, SubmarineInfo? startOutpost = null, SubmarineInfo? endOutpost = null) { AfflictionPrefab.LoadAllEffects(); - + MirrorLevel = mirrorLevel; if (SubmarineInfo == null) { @@ -411,9 +420,6 @@ namespace Barotrauma } } - //Clear out the stored grids - Powered.Grids.Clear(); - Level? level = null; if (levelData != null) { @@ -422,6 +428,11 @@ namespace Barotrauma InitializeLevel(level); + //Clear out the cached grids and force update + Powered.Grids.Clear(); + + casualties.Clear(); + GameAnalyticsManager.AddProgressionEvent( GameAnalyticsManager.ProgressionStatus.Start, GameMode?.Preset?.Identifier.Value ?? "none"); @@ -480,7 +491,7 @@ namespace Barotrauma existingRoundSummary.ContinueButton.Visible = true; } - RoundSummary = new RoundSummary(Submarine.Info, GameMode, Missions, StartLocation, EndLocation); + RoundSummary = new RoundSummary(GameMode, Missions, StartLocation, EndLocation); if (!(GameMode is TutorialMode) && !(GameMode is TestGameMode)) { @@ -723,7 +734,7 @@ namespace Barotrauma } partial void UpdateProjSpecific(float deltaTime); - + public static IEnumerable GetSessionCrewCharacters() { #if SERVER @@ -745,7 +756,7 @@ namespace Barotrauma { IEnumerable crewCharacters = GetSessionCrewCharacters(); - int prevMoney = (GameMode as CampaignMode)?.Money ?? 0; + int prevMoney = (GameMode as CampaignMode)?.Bank.Balance ?? 0; // FIXME personal wallets - reward distribution foreach (Mission mission in missions) { @@ -759,14 +770,13 @@ namespace Barotrauma if (missions.Any()) { - if (missions.Any()) + if (missions.Any(m => m.Completed)) { foreach (Character character in crewCharacters) { character.CheckTalents(AbilityEffectType.OnAnyMissionCompleted); } } - if (missions.All(m => m.Completed)) { foreach (Character character in crewCharacters) @@ -818,7 +828,7 @@ namespace Barotrauma LogEndRoundStats(eventId); if (GameMode is CampaignMode campaignMode) { - GameAnalyticsManager.AddDesignEvent(eventId + "MoneyEarned", campaignMode.Money - prevMoney); + GameAnalyticsManager.AddDesignEvent(eventId + "MoneyEarned", campaignMode.Bank.Balance - prevMoney); // FIXME personal wallets - reward distrubiton campaignMode.TotalPlayTime += roundDuration; } #if CLIENT @@ -910,6 +920,10 @@ namespace Barotrauma public void KillCharacter(Character character) { + if (CrewManager != null && CrewManager.GetCharacters().Contains(character)) + { + casualties.Add(character); + } #if CLIENT CrewManager?.KillCharacter(character); #endif @@ -917,6 +931,7 @@ namespace Barotrauma public void ReviveCharacter(Character character) { + casualties.Remove(character); #if CLIENT CrewManager?.ReviveCharacter(character); #endif @@ -939,7 +954,7 @@ namespace Barotrauma List excessPackages = new List(); foreach (ContentPackage cp in ContentPackageManager.EnabledPackages.All) { - //if (!cp.HasMultiplayerIncompatibleContent) { continue; } + if (!cp.HasMultiplayerSyncedContent) { continue; } if (!contentPackagePaths.Any(p => p == cp.Path)) { excessPackages.Add(cp.Name); @@ -949,7 +964,7 @@ namespace Barotrauma bool orderMismatch = false; if (missingPackages.Count == 0 && missingPackages.Count == 0) { - var enabledPackages = ContentPackageManager.EnabledPackages.All/*.Where(cp => cp.HasMultiplayerIncompatibleContent)*/.ToImmutableArray(); + var enabledPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent).ToImmutableArray(); for (int i = 0; i < contentPackagePaths.Count && i < enabledPackages.Length; i++) { if (contentPackagePaths[i] != enabledPackages[i].Path) @@ -1015,7 +1030,7 @@ namespace Barotrauma } if (Map != null) { rootElement.Add(new XAttribute("mapseed", Map.Seed)); } rootElement.Add(new XAttribute("selectedcontentpackages", - string.Join("|", ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerIncompatibleContent).Select(cp => cp.Path)))); + string.Join("|", ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent).Select(cp => cp.Path)))); ((CampaignMode)GameMode).Save(doc.Root); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs index 235b157d2..3204ee2a5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Barotrauma.Extensions; +using Barotrauma.Networking; namespace Barotrauma { @@ -177,17 +178,20 @@ namespace Barotrauma } } + public readonly List PendingHeals = new List(); + + public Action? OnUpdate; + private readonly CampaignMode? campaign; public MedicalClinic(CampaignMode campaign) { this.campaign = campaign; +#if CLIENT + campaign.OnMoneyChanged.RegisterOverwriteExisting(nameof(MedicalClinic).ToIdentifier(), OnMoneyChanged); +#endif } - public readonly List PendingHeals = new List(); - - public Action? OnUpdate; - private static bool IsOutpostInCombat() { if (!(Level.Loaded is { Type: LevelData.LevelType.Outpost })) { return false; } @@ -203,14 +207,13 @@ namespace Barotrauma return false; } - private HealRequestResult HealAllPending(bool force = false) + private HealRequestResult HealAllPending(bool force = false, Client? client = null) { int totalCost = GetTotalCost(); if (!force) { - if (GetMoney() < totalCost) { return HealRequestResult.InsufficientFunds; } - if (IsOutpostInCombat()) { return HealRequestResult.Refused; } + if (!GetWallet(client).TryDeduct(totalCost)) { return HealRequestResult.InsufficientFunds; } } ImmutableArray crew = GetCrewCharacters(); @@ -225,11 +228,6 @@ namespace Barotrauma } } - if (campaign != null) - { - campaign.Money -= totalCost; - } - ClearPendingHeals(); return HealRequestResult.Success; @@ -316,7 +314,10 @@ namespace Barotrauma private int GetAdjustedPrice(int price) => campaign?.Map?.CurrentLocation is { Type: { HasOutpost: true } } currentLocation ? currentLocation.GetAdjustedHealCost(price) : int.MaxValue; - public int GetMoney() => campaign?.Money ?? 0; + public Wallet GetWallet(Client? c = null) + { + return campaign?.GetWallet(c) ?? Wallet.Invalid; + } public static ImmutableArray GetCrewCharacters() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs index aede268f8..6a8df4f5e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Barotrauma.Networking; using Barotrauma.Extensions; using Microsoft.Xna.Framework; @@ -178,7 +179,7 @@ namespace Barotrauma /// Purchased upgrades are temporarily stored in and they are applied /// after the next round starts similarly how items are spawned in the stowage room after the round starts. /// - public void PurchaseUpgrade(UpgradePrefab prefab, UpgradeCategory category, bool force = false) + public void PurchaseUpgrade(UpgradePrefab prefab, UpgradeCategory category, bool force = false, Client? client = null) { if (!CanUpgradeSub()) { @@ -215,7 +216,7 @@ namespace Barotrauma price = 0; } - if (Campaign.Money >= price) + if (Campaign.GetWallet(client).TryDeduct(price)) // FIXME personal wallets { if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { @@ -227,7 +228,6 @@ namespace Barotrauma } } - Campaign.Money -= price; GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarineUpgrade, prefab.Identifier.Value); PurchasedUpgrade? upgrade = FindMatchingUpgrade(prefab, category); @@ -253,14 +253,14 @@ namespace Barotrauma else { DebugConsole.ThrowError("Tried to purchase an upgrade with insufficient funds, the transaction has not been completed.\n" + - $"Upgrade: {prefab.Name}, Cost: {price}, Have: {Campaign.Money}"); + $"Upgrade: {prefab.Name}, Cost: {price}, Have: {Campaign.GetWallet(client).Balance}"); } } /// /// Purchases an item swap and handles logic for deducting the credit. /// - public void PurchaseItemSwap(Item itemToRemove, ItemPrefab itemToInstall, bool force = false) + public void PurchaseItemSwap(Item itemToRemove, ItemPrefab itemToInstall, bool force = false, Client? client = null) { if (!CanUpgradeSub()) { @@ -313,7 +313,7 @@ namespace Barotrauma price = 0; } - if (Campaign.Money >= price) + if (Campaign.GetWallet(client).TryDeduct(price)) { PurchasedItemSwaps.RemoveAll(p => linkedItems.Contains(p.ItemToRemove)); if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) @@ -326,7 +326,6 @@ namespace Barotrauma } } - Campaign.Money -= price; GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarineWeapon, itemToInstall.Identifier.Value); foreach (Item itemToSwap in linkedItems) @@ -355,7 +354,7 @@ namespace Barotrauma else { DebugConsole.ThrowError("Tried to swap an item with insufficient funds, the transaction has not been completed.\n" + - $"Item to remove: {itemToRemove.Name}, Item to install: {itemToInstall.Name}, Cost: {price}, Have: {Campaign.Money}"); + $"Item to remove: {itemToRemove.Name}, Item to install: {itemToInstall.Name}, Cost: {price}, Have: {Campaign.GetWallet(client).Balance}"); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index f4818dc48..3cedefab8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -58,7 +58,7 @@ namespace Barotrauma IsEquipped = new bool[capacity]; SlotTypes = new InvSlotType[capacity]; - AccessibleWhenAlive = element.GetAttributeBool("accessiblewhenalive", true); + AccessibleWhenAlive = element.GetAttributeBool("accessiblewhenalive", false); AccessibleByOwner = element.GetAttributeBool("accessiblebyowner", AccessibleWhenAlive); string[] slotTypeNames = ParseSlotTypes(element); @@ -159,14 +159,14 @@ namespace Barotrauma { return base.CanBePutInSlot(item, i, ignoreCondition) && item.AllowedSlots.Any(s => s.HasFlag(SlotTypes[i])) && - (SlotTypes[i] == InvSlotType.Any || slots[i].ItemCount < 1); + (SlotTypes[i] == InvSlotType.Any || slots[i].Items.Count < 1); } public override bool CanBePutInSlot(ItemPrefab itemPrefab, int i, float? condition, int? quality = null) { return base.CanBePutInSlot(itemPrefab, i, condition, quality) && - (SlotTypes[i] == InvSlotType.Any || slots[i].ItemCount < 1); + (SlotTypes[i] == InvSlotType.Any || slots[i].Items.Count < 1); } public bool CanBeAutoMovedToCorrectSlots(Item item) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs index f4826ee0b..bcee8a722 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs @@ -507,7 +507,7 @@ namespace Barotrauma.Items.Components list.Remove(this); } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { //no further data needed, the event just triggers the discharge } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs index 9f135fdb9..c77aa4a7c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs @@ -229,7 +229,6 @@ namespace Barotrauma.Items.Components int amount = Rand.Range(minAmount, maxAmount, Rand.RandSync.Unsynced); Vector2 offset = SpawnAreaOffset; - offset.Y = -offset.Y; switch (SpawnAreaShape) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs index 5cde984fb..bff4ee42c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs @@ -707,7 +707,7 @@ namespace Barotrauma.Items.Components #if SERVER for (int i = 0; i < Vines.Count; i += VineChunkSize) { - GameMain.Server.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ComponentState, item.GetComponentIndex(this), i }); + item.CreateServerEvent(this, new EventData(offset: i)); } #elif CLIENT ResetPlanterSize(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index b8caed282..6f3383913 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -12,6 +12,16 @@ namespace Barotrauma.Items.Components { partial class Holdable : Pickable, IServerSerializable, IClientSerializable { + private readonly struct EventData : IEventData + { + public readonly Vector2 AttachPos; + + public EventData(Vector2 attachPos) + { + AttachPos = attachPos; + } + } + const float MaxAttachDistance = 150.0f; //the position(s) in the item that the Character grabs @@ -155,8 +165,8 @@ namespace Barotrauma.Items.Components [Editable, Serialize(false, IsPropertySaveable.No, description: "Should the item swing around when it's being used (for example, when firing a weapon or a welding tool).")] public bool SwingWhenUsing { get; set; } - [ConditionallyEditable(ConditionallyEditable.ConditionType.Attachable, MinValueFloat = 0.0f, MaxValueFloat = 0.999f, DecimalCount = 3), Serialize(0.85f, IsPropertySaveable.No, description: "Sprite depth that's used when the item is attached to a wall.")] - public float SpriteDepthWhenAttached + [ConditionallyEditable(ConditionallyEditable.ConditionType.Attachable, MinValueFloat = 0.0f, MaxValueFloat = 0.999f, DecimalCount = 3), Serialize(0.55f, IsPropertySaveable.No, description: "Sprite depth that's used when the item is NOT attached to a wall.")] + public float SpriteDepthWhenDropped { get; set; @@ -244,12 +254,12 @@ namespace Barotrauma.Items.Components } } - private bool loadedFromXml; + private bool loadedFromInstance; public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) { base.Load(componentElement, usePrefabValues, idRemap); - loadedFromXml = true; + loadedFromInstance = true; if (usePrefabValues) { @@ -583,7 +593,7 @@ namespace Barotrauma.Items.Components Attached = true; #if CLIENT - item.DrawDepthOffset = SpriteDepthWhenAttached - item.SpriteDepth; + item.DrawDepthOffset = 0.0f; #endif } @@ -600,6 +610,9 @@ namespace Barotrauma.Items.Components requiredItems.Clear(); DisplayMsg = ""; PickKey = InputType.Select; +#if CLIENT + item.DrawDepthOffset = SpriteDepthWhenDropped - item.SpriteDepth; +#endif } public override void ParseMsg() @@ -663,12 +676,7 @@ namespace Barotrauma.Items.Components { #if CLIENT Vector2 attachPos = ConvertUnits.ToSimUnits(GetAttachPosition(character)); - GameMain.Client.CreateEntityEvent(item, new object[] - { - NetEntityEvent.Type.ComponentState, - item.GetComponentIndex(this), - attachPos - }); + item.CreateClientEvent(this, new EventData(attachPos)); #endif } return false; @@ -867,8 +875,8 @@ namespace Barotrauma.Items.Components { if (!attachable) { return; } - //a mod has overridden the item, and the base item didn't have a Holdable component = a mod made the item movable/detachable - if (item.Prefab.IsOverride && !loadedFromXml) + //the Holdable component didn't get loaded from an instance of the item, just the prefab xml = a mod or update must've made the item movable/detachable + if (!loadedFromInstance) { if (attachedByDefault) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs index 68683091a..e0c965cf4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs @@ -67,7 +67,6 @@ namespace Barotrauma.Items.Components [Serialize("#ffffff", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public Color OwnerSkinColor { get; set; } - #warning TODO: figure out how to set Vector2.Zero as the default here [Serialize("0,0", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public Vector2 OwnerSheetIndex { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index 95152d686..353a0b0f6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -436,13 +436,10 @@ namespace Barotrauma.Items.Components #if SERVER if (GameMain.Server != null && targetCharacter != null) //TODO: Log structure hits { - GameMain.Server.CreateEntityEvent(item, new object[] - { - Networking.NetEntityEvent.Type.ApplyStatusEffect, + GameMain.Server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData( success ? ActionType.OnUse : ActionType.OnFailure, - null, //itemcomponent - targetCharacter.ID, targetLimb - }); + targetItemComponent: null, + targetCharacter, targetLimb)); string logStr = picker?.LogName + " used " + item.Name; if (item.ContainedItems != null && item.ContainedItems.Any()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs index 432ae93bf..ddd615402 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs @@ -282,12 +282,12 @@ namespace Barotrauma.Items.Components } } - public virtual void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public virtual void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - msg.Write(activePicker == null ? (ushort)0 : activePicker.ID); + msg.Write(activePicker?.ID ?? (ushort)0); } - public virtual void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public virtual void ClientEventRead(IReadMessage msg, float sendingTime) { ushort pickerID = msg.ReadUInt16(); if (pickerID == 0) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs index 3e9121f91..40d209d5b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs @@ -178,11 +178,11 @@ namespace Barotrauma.Items.Components throwDone = true; IsActive = true; - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + if (GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnSecondaryUse, this, CurrentThrower.ID }); + GameMain.NetworkMember.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnSecondaryUse, this, CurrentThrower)); } - if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) + if (!(GameMain.NetworkMember is { IsClient: true })) { //Stun grenades, flares, etc. all have their throw-related things handled in "onSecondaryUse" ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, CurrentThrower, user: CurrentThrower); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index afe64e268..8726d8462 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -6,6 +6,7 @@ using System.Reflection; using System.Xml.Linq; using Barotrauma.Extensions; using Barotrauma.IO; +using Barotrauma.Networking; #if CLIENT using Microsoft.Xna.Framework.Graphics; using Barotrauma.Sounds; @@ -1036,6 +1037,30 @@ namespace Barotrauma.Items.Components } } + public interface IEventData { } + + public virtual bool ValidateEventData(NetEntityEvent.IData data) + => true; + + protected T ExtractEventData(NetEntityEvent.IData data) where T : IEventData + => TryExtractEventData(data, out T componentData) + ? componentData + : throw new Exception($"Malformed item component state event for {item.Name} " + + $"(item ID {item.ID}, component type {GetType().Name}): " + + $"could not extract ComponentData of type {typeof(T).Name}"); + + protected bool TryExtractEventData(NetEntityEvent.IData data, out T componentData) + { + componentData = default; + if (data is Item.ComponentStateEventData { ComponentData: T nestedData }) + { + componentData = nestedData; + return true; + } + + return false; + } + #region AI related protected const float AIUpdateInterval = 0.2f; protected float aiUpdateTimer; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index f4897de76..7a2cb168d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -370,7 +370,10 @@ namespace Barotrauma.Items.Components public Item GetFocusTarget() { - item.SendSignal(new Signal(MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture), sender: user), "position_out"); + var positionOut = item.Connections.Find(c => c.Name == "position_out"); + if (positionOut == null) { return null; } + + item.SendSignal(new Signal(MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture), sender: user), positionOut); for (int i = item.LastSentSignalRecipients.Count - 1; i >= 0; i--) { @@ -380,7 +383,16 @@ namespace Barotrauma.Items.Components return item.LastSentSignalRecipients[i].Item; } } - + + foreach (var recipientPanel in item.GetConnectedComponentsRecursive(positionOut, allowTraversingBackwards: false)) + { + if (recipientPanel.Item.Condition <= 0.0f) { continue; } + if (recipientPanel.Item.Prefab.FocusOnSelected) + { + return recipientPanel.Item; + } + } + return null; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index 004d78dd2..c09526555 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -221,7 +221,7 @@ namespace Barotrauma.Items.Components } float condition = deconstructProduct.CopyCondition ? - percentageHealth * itemPrefab.Health : + percentageHealth * itemPrefab.Health * deconstructProduct.OutConditionMax : itemPrefab.Health * Rand.Range(deconstructProduct.OutConditionMin, deconstructProduct.OutConditionMax); if (DeconstructItemsSimultaneously && deconstructProduct.RequiredOtherItem.Length > 0) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 4fc2c9cf0..d0ad2a519 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -354,7 +354,14 @@ namespace Barotrauma.Items.Components { foreach (Item containedItem in availableItem.OwnInventory.AllItemsMod) { - containedItem.Drop(dropper: null); + if (availableItem.GetComponent()?.RemoveContainedItemsOnDeconstruct ?? false) + { + Entity.Spawner.AddItemToRemoveQueue(containedItem); + } + else + { + containedItem.Drop(dropper: null); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index 37dbc543f..b375eb205 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -169,7 +169,7 @@ namespace Barotrauma.Items.Components hull.BallastFlora = new BallastFloraBehavior(hull, ballastFloraPrefab, offset, firstGrowth: true); #if SERVER - hull.BallastFlora.SendNetworkMessage(hull.BallastFlora, BallastFloraBehavior.NetworkHeader.Spawn); + hull.BallastFlora.SendNetworkMessage(new BallastFloraBehavior.SpawnEventData()); #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs index 7abf1b323..4d9561116 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs @@ -1,11 +1,9 @@ -using Barotrauma.Networking; +using Barotrauma.Extensions; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; -using Barotrauma.Extensions; using System.Globalization; +using System.Linq; namespace Barotrauma.Items.Components { @@ -287,7 +285,7 @@ namespace Barotrauma.Items.Components } else if (autoTemp) { - UpdateAutoTemp(10.0f, deltaTime * 2f); + UpdateAutoTemp(2.0f, deltaTime); } @@ -300,7 +298,14 @@ namespace Barotrauma.Items.Components if (!item.HasTag("reactorfuel")) { continue; } if (fissionRate > 0.0f) { - item.Condition -= fissionRate / 100.0f * fuelConsumptionRate * deltaTime; + bool isConnectedToFriendlyOutpost = Level.IsLoadedOutpost && + Item.Submarine?.TeamID == CharacterTeamType.Team1 && + Item.Submarine.GetConnectedSubs().Any(s => s.Info.IsOutpost && s.TeamID == CharacterTeamType.FriendlyNPC); + + if (!isConnectedToFriendlyOutpost) + { + item.Condition -= fissionRate / 100.0f * fuelConsumptionRate * deltaTime; + } } fuelLeft += item.ConditionPercentage; } @@ -418,7 +423,7 @@ namespace Barotrauma.Items.Components { float idealLoad = MaxPowerOutput / minMaxPower.ReactorMaxOutput * loadLeft; float loadAdjust = MathHelper.Clamp((ratio - 0.5f) * 25 + idealLoad - (turbineOutput / 100 * MaxPowerOutput), -MaxPowerOutput / 100, MaxPowerOutput / 100); - newLoad = MathHelper.Clamp(loadLeft - (expectedPower + output) + loadAdjust, 0, loadLeft); + newLoad = MathHelper.Clamp(loadLeft - (expectedPower - output) + loadAdjust, 0, loadLeft); } if (float.IsNegative(newLoad)) @@ -498,7 +503,6 @@ namespace Barotrauma.Items.Components if (temperature > optimalTemperature.Y) { - float prevFireTimer = fireTimer; fireTimer += MathHelper.Lerp(deltaTime * 2.0f, deltaTime, item.Condition / item.MaxCondition); #if SERVER if (fireTimer > Math.Min(5.0f, FireDelay / 2) && blameOnBroken?.Character?.SelectedConstruction == item) @@ -506,9 +510,10 @@ namespace Barotrauma.Items.Components GameMain.Server.KarmaManager.OnReactorOverHeating(item, blameOnBroken.Character, deltaTime); } #endif - if (fireTimer >= FireDelay && prevFireTimer < fireDelay) + if (fireTimer >= FireDelay) { new FireSource(item.WorldPosition); + fireTimer = 0.0f; } } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs index 97657bc39..d60aad468 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs @@ -349,7 +349,7 @@ namespace Barotrauma.Items.Components } } - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerEventRead(IReadMessage msg, Client c) { bool isActive = msg.ReadBoolean(); bool directionalPing = useDirectionalPing; @@ -394,7 +394,7 @@ namespace Barotrauma.Items.Components #endif } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write(currentMode == Mode.Active); if (currentMode == Mode.Active) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index b41f7c4bb..3a8095561 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -225,7 +225,7 @@ namespace Barotrauma.Items.Components var dockingConnection = item.Connections.FirstOrDefault(c => c.Name == "toggle_docking"); if (dockingConnection != null) { - var connectedPorts = item.GetConnectedComponentsRecursive(dockingConnection); + var connectedPorts = item.GetConnectedComponentsRecursive(dockingConnection, allowTraversingBackwards: false); DockingSources.AddRange(connectedPorts.Where(p => p.Item.Submarine != null && !p.Item.Submarine.Info.IsOutpost)); } } @@ -271,13 +271,9 @@ namespace Barotrauma.Items.Components item.CreateClientEvent(this); correctionTimer = CorrectionDelay; } - else #endif #if SERVER - if (GameMain.Server != null) - { - item.CreateServerEvent(this); - } + item.CreateServerEvent(this); #endif networkUpdateTimer = 0.1f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs index 3d1631663..46eac6dca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs @@ -2,7 +2,6 @@ using Microsoft.Xna.Framework; using System; using System.Globalization; -using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -11,7 +10,7 @@ namespace Barotrauma.Items.Components //[power/min] private float capacity; - private float charge; + private float charge, prevCharge; //how fast the battery can be recharged private float maxRechargeSpeed; @@ -34,7 +33,7 @@ namespace Barotrauma.Items.Components get { return currPowerOutput; } private set { - System.Diagnostics.Debug.Assert(value >= 0.0f); + System.Diagnostics.Debug.Assert(value >= 0.0f, $"Tried to set PowerContainer's output to a negative value ({value})"); currPowerOutput = Math.Max(0, value); } } @@ -140,6 +139,7 @@ namespace Barotrauma.Items.Components { IsActive = true; InitProjSpecific(); + prevCharge = Charge; } partial void InitProjSpecific(); @@ -171,7 +171,7 @@ namespace Barotrauma.Items.Components loadReading = powerOut.Grid.Load; } - item.SendSignal(((int)Math.Round(-CurrPowerOutput)).ToString(), "power_value_out"); + item.SendSignal(((int)Math.Round(CurrPowerOutput)).ToString(), "power_value_out"); item.SendSignal(((int)Math.Round(loadReading)).ToString(), "load_value_out"); item.SendSignal(((int)Math.Round(Charge)).ToString(), "charge"); item.SendSignal(((int)Math.Round(Charge / capacity * 100)).ToString(), "charge_%"); @@ -194,6 +194,8 @@ namespace Barotrauma.Items.Components } else { + if (item.Condition <= 0.0f) { return 0.0f; } + float missingCharge = capacity - charge; float targetRechargeSpeed = rechargeSpeed; @@ -231,7 +233,7 @@ namespace Barotrauma.Items.Components if (connection == powerOut) { float maxOutput; - float chargeRatio = charge / capacity; + float chargeRatio = prevCharge / capacity; if (chargeRatio < 0.1f) { maxOutput = Math.Max(chargeRatio * 10.0f, 0.0f) * MaxOutPut; @@ -242,7 +244,7 @@ namespace Barotrauma.Items.Components } //Limit max power out to not exceed the charge of the container - maxOutput = Math.Min(maxOutput, charge * 60 / UpdateInterval); + maxOutput = Math.Min(maxOutput, prevCharge * 60 / UpdateInterval); return new PowerRange(0.0f, maxOutput); } @@ -261,18 +263,11 @@ namespace Barotrauma.Items.Components /// public override float GetConnectionPowerOut(Connection connection, float power, PowerRange minMaxPower, float load) { - if (connection == powerOut) + //Only power out connection can provide power and Max poweroutput can't be negative + if (connection == powerOut && minMaxPower.Max > 0) { - //Calculate the max power the container can output - float maxPowerOutput = MaxOutPut; - float chargeRatio = charge / capacity; - if (chargeRatio < 0.1f) - { - maxPowerOutput *= Math.Max(chargeRatio * 10.0f, 0.0f); - } - //Set power output based on the relative max power output capabilities and load demand - CurrPowerOutput = MathHelper.Clamp((load - power) / minMaxPower.Max, 0, 1) * maxPowerOutput; + CurrPowerOutput = MathHelper.Clamp((load - power) / minMaxPower.Max, 0, 1) * MinMaxPowerOut(connection, load).Max; return CurrPowerOutput; } return 0.0f; @@ -292,6 +287,7 @@ namespace Barotrauma.Items.Components { //Decrease charge based on how much power is leaving the device Charge = Math.Clamp(Charge - CurrPowerOutput / 60 * UpdateInterval, 0, Capacity); + prevCharge = Charge; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs index 82db50a9f..388b719fc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs @@ -176,23 +176,6 @@ namespace Barotrauma.Items.Components { RefreshConnections(); - float powerReadingOut = 0; - float loadReadingOut = ExtraLoad; - if (powerLoad < 0) - { - powerReadingOut = -powerLoad; - loadReadingOut = 0; - } - - if (powerOut != null && powerOut.Grid != null) - { - powerReadingOut = powerOut.Grid.Power; - loadReadingOut = powerOut.Grid.Load; - } - - item.SendSignal(((int)Math.Round(powerReadingOut)).ToString(), "power_value_out"); - item.SendSignal(((int)Math.Round(loadReadingOut)).ToString(), "load_value_out"); - if (Timing.TotalTime > extraLoadSetTime + 1.0) { //Decay the extra load to 0 from either positive or negative @@ -216,22 +199,36 @@ namespace Barotrauma.Items.Components ApplyStatusEffects(ActionType.OnActive, deltaTime, null); - //if the item can't be fixed, don't allow it to break - if (!item.Repairables.Any() || !CanBeOverloaded) { return; } - - if (prevSentPowerValue != (int)-CurrPowerConsumption || powerSignal == null) + float powerReadingOut = 0; + float loadReadingOut = ExtraLoad; + if (powerLoad < 0) { - prevSentPowerValue = (int)Math.Round(-CurrPowerConsumption); + powerReadingOut = -powerLoad; + loadReadingOut = 0; + } + + if (powerOut != null && powerOut.Grid != null) + { + powerReadingOut = powerOut.Grid.Power; + loadReadingOut = powerOut.Grid.Load; + } + + if (prevSentPowerValue != (int)powerReadingOut || powerSignal == null) + { + prevSentPowerValue = (int)Math.Round(powerReadingOut); powerSignal = prevSentPowerValue.ToString(); } - if (prevSentLoadValue != (int)powerLoad || loadSignal == null) + if (prevSentLoadValue != (int)loadReadingOut || loadSignal == null) { - prevSentLoadValue = (int)Math.Round(powerLoad); + prevSentLoadValue = (int)Math.Round(loadReadingOut); loadSignal = prevSentLoadValue.ToString(); } item.SendSignal(powerSignal, "power_value_out"); item.SendSignal(loadSignal, "load_value_out"); + //if the item can't be fixed, don't allow it to break + if (!item.Repairables.Any() || !CanBeOverloaded) { return; } + float maxOverVoltage = Math.Max(OverloadVoltage, 1.0f); Overload = Voltage > maxOverVoltage; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs index 7d0e21ff8..be83e2267 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs @@ -187,14 +187,11 @@ namespace Barotrauma.Items.Components protected void UpdateOnActiveEffects(float deltaTime) { - if (currPowerConsumption <= 0.0f) + if (currPowerConsumption <= 0.0f && PowerConsumption <= 0.0f) { //if the item consumes no power, ignore the voltage requirement and //apply OnActive statuseffects as long as this component is active - if (PowerConsumption <= 0.0f) - { - ApplyStatusEffects(ActionType.OnActive, deltaTime, null); - } + ApplyStatusEffects(ActionType.OnActive, deltaTime, null); return; } @@ -216,6 +213,11 @@ namespace Barotrauma.Items.Components powerOnSoundPlayed = false; } #endif + if (powerIn == null) + { + //power down the device here if it has no power connection (= receives power from contained battery cells instead of the "normal" power logic) + Voltage -= deltaTime; + } } public override void Update(float deltaTime, Camera cam) @@ -238,7 +240,11 @@ namespace Barotrauma.Items.Components else if (c.Name == "power_out") { powerOut = c; - powerOut.Priority = Priority; + // Connection takes the lowest priority + if (Priority > powerOut.Priority) + { + powerOut.Priority = Priority; + } } else if (c.Name == "power") { @@ -258,7 +264,11 @@ namespace Barotrauma.Items.Components #endif } powerOut = c; - powerOut.Priority = Priority; + // Connection takes the lowest priority + if (Priority > powerOut.Priority) + { + powerOut.Priority = Priority; + } } else { @@ -591,7 +601,7 @@ namespace Barotrauma.Items.Components foreach (Connection con in grid.Connections) { Powered device = con.Item.GetComponent(); - device.GridResolved(con); + device?.GridResolved(con); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index 2890cc214..fcddb442b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -196,6 +196,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(true, IsPropertySaveable.No)] + public bool FriendlyFire + { + get; + set; + } + private float deactivationTimer; [Serialize(0f, IsPropertySaveable.No)] @@ -309,7 +316,7 @@ namespace Barotrauma.Items.Components { #if SERVER launchRot = rotation; - Item.CreateServerEvent(this, new object[] { true }); //true = indicate that this is a launch event + Item.CreateServerEvent(this, new EventData(launch: true)); #endif } } @@ -673,7 +680,7 @@ namespace Barotrauma.Items.Components { Unstick(); #if SERVER - item.CreateServerEvent(this); + item.CreateServerEvent(this, new EventData(launch: false)); #endif } } @@ -697,9 +704,9 @@ namespace Barotrauma.Items.Components return false; } if (hits.Contains(target.Body)) { return false; } - if (ShouldIgnoreSubmarineCollision(target, contact)) + if (target.Body.UserData is Submarine) { - return false; + if (ShouldIgnoreSubmarineCollision(ref target, contact)) { return false; } } else if (target.Body.UserData is Limb limb) { @@ -709,6 +716,10 @@ namespace Barotrauma.Items.Components limb.body?.ApplyLinearImpulse(item.body.LinearVelocity * item.body.Mass * 0.1f, item.SimPosition); return false; } + if (!FriendlyFire && User != null && limb.character.IsFriendly(User)) + { + return false; + } } else if (target.Body.UserData is Item item) { @@ -893,8 +904,8 @@ namespace Barotrauma.Items.Components #if SERVER if (GameMain.NetworkMember.IsServer) { - GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, actionType, this, targetLimb.character.ID, targetLimb, (ushort)0, item.WorldPosition }); - GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnImpact, this, targetLimb.character.ID, targetLimb, (ushort)0, item.WorldPosition }); + GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(actionType, this, targetLimb.character, targetLimb, null, item.WorldPosition)); + GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, targetLimb.character, targetLimb, null, item.WorldPosition)); } #endif } @@ -905,8 +916,8 @@ namespace Barotrauma.Items.Components #if SERVER if (GameMain.NetworkMember.IsServer) { - GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, actionType, this, (ushort)0, null, (target.Body.UserData as Entity)?.ID ?? 0, item.WorldPosition }); - GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnImpact, this, (ushort)0, null, (target.Body.UserData as Entity)?.ID ?? 0, item.WorldPosition }); + GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(actionType, this, null, null, target.Body.UserData as Entity, item.WorldPosition)); + GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, null, null, target.Body.UserData as Entity, item.WorldPosition)); } #endif } @@ -952,7 +963,7 @@ namespace Barotrauma.Items.Components #if SERVER if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { - item.CreateServerEvent(this); + item.CreateServerEvent(this, new EventData(launch: false)); } #endif item.body.LinearVelocity *= speedMultiplier; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index 3e1439340..cbd83caea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -230,7 +230,7 @@ namespace Barotrauma.Items.Components { ApplyStatusEffects(ActionType.OnFailure, 1.0f, CurrentFixer); #if SERVER - GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFailure, this, CurrentFixer.ID }); + GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnFailure, this, CurrentFixer)); #endif } } @@ -256,10 +256,10 @@ namespace Barotrauma.Items.Components if (!CheckCharacterSuccess(character, bestRepairItem)) { GameServer.Log($"{GameServer.CharacterLogName(character)} failed to {(action == FixActions.Sabotage ? "sabotage" : "repair")} {item.Name}", ServerLog.MessageType.ItemInteraction); - GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFailure, this, character.ID }); + GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnFailure, this, character)); if (bestRepairItem != null && bestRepairItem.GetComponent() is Holdable h) { - GameMain.Server?.CreateEntityEvent(bestRepairItem, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFailure, h, character.ID }); + GameMain.Server?.CreateEntityEvent(bestRepairItem, new Item.ApplyStatusEffectEventData(ActionType.OnFailure, h, character)); } return false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs index c31b4ff6e..b999a961b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs @@ -15,6 +15,8 @@ namespace Barotrauma.Items.Components //the output is sent if both inputs have received a signal within the timeframe protected float timeFrame; + protected readonly Character[] signalSender = new Character[2]; + [Serialize(999999.0f, IsPropertySaveable.Yes, description: "The output of the item is restricted below this value.", alwaysUseInstanceValues: true), InGameEditable(MinValueFloat = -999999.0f, MaxValueFloat = 999999.0f)] public float ClampMax @@ -33,7 +35,7 @@ namespace Barotrauma.Items.Components [InGameEditable(DecimalCount = 2), Serialize(0.0f, IsPropertySaveable.Yes, description: "The item must have received signals to both inputs within this timeframe to output the result." + - " If set to 0, the inputs must be received at the same time.", alwaysUseInstanceValues: true)] + " If set to 0, the inputs must be received at the same time.", alwaysUseInstanceValues: true, translationTextTag: "sp.")] public float TimeFrame { get { return timeFrame; } @@ -71,7 +73,7 @@ namespace Barotrauma.Items.Components float output = Calculate(receivedSignal[0], receivedSignal[1]); if (MathUtils.IsValid(output)) { - item.SendSignal(MathHelper.Clamp(output, ClampMin, ClampMax).ToString("G", CultureInfo.InvariantCulture), "signal_out"); + item.SendSignal(new Signal(MathHelper.Clamp(output, ClampMin, ClampMax).ToString("G", CultureInfo.InvariantCulture), sender: signalSender[0] ?? signalSender[1]), "signal_out"); } } @@ -83,11 +85,13 @@ namespace Barotrauma.Items.Components { case "signal_in1": float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[0]); + signalSender[0] = signal.sender; timeSinceReceived[0] = 0.0f; IsActive = true; break; case "signal_in2": float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[1]); + signalSender[1] = signal.sender; timeSinceReceived[1] = 0.0f; IsActive = true; break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs index b9dd02749..c7fd99f51 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs @@ -109,10 +109,23 @@ namespace Barotrauma.Items.Components return true; } - private void Write(IWriteMessage msg, object[] extraData) + private readonly struct EventData : IEventData { - if (extraData == null || extraData.Length < 3) { return; } - msg.WriteRangedInteger((int)extraData[2], 0, Signals.Length - 1); + public readonly int SignalIndex; + + public EventData(int signalIndex) + { + SignalIndex = signalIndex; + } + } + + public override bool ValidateEventData(NetEntityEvent.IData data) + => TryExtractEventData(data, out _); + + private void Write(IWriteMessage msg, NetEntityEvent.IData extraData) + { + var eventData = ExtractEventData(extraData); + msg.WriteRangedInteger(eventData.SignalIndex, 0, Signals.Length - 1); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs index 48258b2ca..f7f7d38eb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs @@ -400,7 +400,7 @@ namespace Barotrauma.Items.Components } - public void ClientWrite(IWriteMessage msg, object[] extraData = null) + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { #if CLIENT TriggerRewiringSound(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs index fb9b68e44..cdeb9bf46 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs @@ -8,6 +8,16 @@ namespace Barotrauma.Items.Components { partial class CustomInterface : ItemComponent, IClientSerializable, IServerSerializable { + private readonly struct EventData : IEventData + { + public readonly CustomInterfaceElement BtnElement; + + public EventData(CustomInterfaceElement btnElement) + { + BtnElement = btnElement; + } + } + class CustomInterfaceElement : ISerializableEntity { public bool ContinuousSignal; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs index 111ed4ea2..e274d06eb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs @@ -1,7 +1,6 @@ using FarseerPhysics; using Microsoft.Xna.Framework; using System; -using System.Linq; using System.Xml.Linq; namespace Barotrauma.Items.Components diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/NotComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/NotComponent.cs index 6f6059251..77b054dce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/NotComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/NotComponent.cs @@ -1,13 +1,11 @@ -using System.Xml.Linq; - -namespace Barotrauma.Items.Components +namespace Barotrauma.Items.Components { class NotComponent : ItemComponent { private bool signalReceived; private bool continuousOutput; - [Editable, Serialize(false, IsPropertySaveable.Yes, description: "When enabled, the component continuously outputs \"1\" when it's not receiving a signal.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize(false, IsPropertySaveable.Yes, description: "When enabled, the component continuously outputs \"1\" when it's not receiving a signal.", alwaysUseInstanceValues: true)] public bool ContinuousOutput { get { return continuousOutput; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs index 9ade22e1c..ab0034584 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs @@ -372,12 +372,12 @@ namespace Barotrauma.Items.Components IsOn = on; } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write(isOn); } - public void ClientRead(ServerNetObject type, IReadMessage msg, float _) + public void ClientEventRead(IReadMessage msg, float sendingTime) { SetState(msg.ReadBoolean(), true); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SmokeDetector.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SmokeDetector.cs index 22cbef2ee..24f68fa5c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SmokeDetector.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SmokeDetector.cs @@ -68,7 +68,7 @@ namespace Barotrauma.Items.Components { foreach (FireSource fireSource in hull.FireSources) { - if (fireSource.IsInDamageRange(item.WorldPosition, fireSource.DamageRange * 2.0f)) { return true; } + if (fireSource.IsInDamageRange(item.WorldPosition, Math.Max(fireSource.DamageRange * 2.0f, 500.0f))) { return true; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/StringComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/StringComponent.cs index a8c69fdeb..4533df4ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/StringComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/StringComponent.cs @@ -1,7 +1,4 @@ -using Microsoft.Xna.Framework; -using System; -using System.Globalization; -using System.Xml.Linq; +using System; namespace Barotrauma.Items.Components { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs index b16dce24c..5804411b7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs @@ -20,6 +20,8 @@ namespace Barotrauma.Items.Components private readonly float[] receivedSignal = new float[2]; private readonly float[] timeSinceReceived = new float[2]; + protected Character signalSender; + [Serialize(FunctionType.Sin, IsPropertySaveable.No, description: "Which kind of function to run the input through.", alwaysUseInstanceValues: true)] public FunctionType Function { @@ -56,7 +58,7 @@ namespace Barotrauma.Items.Components { float angle = (float)Math.Atan2(receivedSignal[1], receivedSignal[0]); if (!UseRadians) { angle = MathHelper.ToDegrees(angle); } - item.SendSignal(angle.ToString("G", CultureInfo.InvariantCulture), "signal_out"); + item.SendSignal(new Signal(angle.ToString("G", CultureInfo.InvariantCulture), sender: signalSender), "signal_out"); } } } @@ -65,6 +67,7 @@ namespace Barotrauma.Items.Components { float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float value); bool sendOutputImmediately = true; + signalSender = signal.sender; switch (Function) { case FunctionType.Sin: diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs index ee21d0939..19588f114 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs @@ -455,12 +455,7 @@ namespace Barotrauma.Items.Components #if CLIENT if (GameMain.NetworkMember != null) { - GameMain.Client.CreateEntityEvent(item, new object[] - { - NetEntityEvent.Type.ComponentState, - item.GetComponentIndex(this), - nodes.Count - }); + item.CreateClientEvent(this, new ClientEventData(nodes.Count)); } #endif } @@ -482,12 +477,7 @@ namespace Barotrauma.Items.Components #if CLIENT if (GameMain.NetworkMember != null) { - GameMain.Client.CreateEntityEvent(item, new object[] - { - NetEntityEvent.Type.ComponentState, - item.GetComponentIndex(this), - nodes.Count - }); + item.CreateClientEvent(this, new ClientEventData(nodes.Count)); } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index ca3398fee..824c85659 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -49,8 +49,6 @@ namespace Barotrauma.Items.Components private ChargingState currentChargingState; - private float currentBarrelSpin = 0f; - private readonly List activeProjectiles = new List(); public IEnumerable ActiveProjectiles => activeProjectiles; @@ -330,10 +328,10 @@ namespace Barotrauma.Items.Components public override void OnMapLoaded() { base.OnMapLoaded(); - FindLightComponent(); if (loadedRotationLimits.HasValue) { RotationLimits = loadedRotationLimits.Value; } if (loadedBaseRotation.HasValue) { BaseRotation = loadedBaseRotation.Value; } targetRotation = rotation; + FindLightComponent(); UpdateTransformedBarrelPos(); } @@ -736,6 +734,16 @@ namespace Barotrauma.Items.Components return true; } + private readonly struct EventData : IEventData + { + public readonly Item Projectile; + + public EventData(Item projectile) + { + Projectile = projectile; + } + } + private void Launch(Item projectile, Character user = null, float? launchRotation = null, float tinkeringStrength = 0f) { reload = reloadTime; @@ -749,7 +757,7 @@ namespace Barotrauma.Items.Components if (projectile != null) { activeProjectiles.Add(projectile); - projectile.Drop(null); + projectile.Drop(null, setTransform: false); if (projectile.body != null) { projectile.body.Dir = 1.0f; @@ -787,12 +795,11 @@ namespace Barotrauma.Items.Components } } - if (projectile.Container != null) { projectile.Container.RemoveContained(projectile); } - } - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) - { - GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ComponentState, item.GetComponentIndex(this), projectile }); + projectile.Container?.RemoveContained(projectile); } +#if SERVER + item.CreateServerEvent(this, new EventData(projectile)); +#endif ApplyStatusEffects(ActionType.OnUse, 1.0f, user: user); LaunchProjSpecific(); @@ -1630,11 +1637,11 @@ namespace Barotrauma.Items.Components } } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - if (extraData.Length > 2) + if (TryExtractEventData(extraData, out EventData eventData)) { - msg.Write(!(extraData[2] is Item item) ? ushort.MaxValue : item.ID); + msg.Write(eventData.Projectile.ID); msg.WriteRangedSingle(MathHelper.Clamp(rotation, minRotation, maxRotation), minRotation, maxRotation, 16); } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index 784669f37..8c148b411 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -540,16 +540,16 @@ namespace Barotrauma.Items.Components Variant = loadedVariant; } } - public override void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public override void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write((byte)Variant); - base.ServerWrite(msg, c, extraData); + base.ServerEventWrite(msg, c, extraData); } - public override void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public override void ClientEventRead(IReadMessage msg, float sendingTime) { Variant = (int)msg.ReadByte(); - base.ClientRead(type, msg, sendingTime); + base.ClientEventRead(msg, sendingTime); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index 4348caf46..dedbe948c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -10,7 +10,7 @@ namespace Barotrauma { partial class Inventory : IServerSerializable, IClientSerializable { - public const int MaxStackSize = 32; + public const int MaxStackSize = (1 << 6) - 1; //the max value that will fit in 6 bits, i.e 63 public class ItemSlot { @@ -18,15 +18,7 @@ namespace Barotrauma public bool HideIfEmpty; - public IEnumerable Items - { - get { return items; } - } - - public int ItemCount - { - get { return items.Count; } - } + public IReadOnlyList Items => items; public bool CanBePut(Item item, bool ignoreCondition = false) { @@ -631,7 +623,7 @@ namespace Barotrauma { if (!slots[i].Any()) { return false; } var item = slots[i].FirstOrDefault(); - if (slots[i].ItemCount < item.Prefab.MaxStackSize) { return false; } + if (slots[i].Items.Count < item.Prefab.MaxStackSize) { return false; } } } else @@ -842,29 +834,30 @@ namespace Barotrauma public virtual void CreateNetworkEvent() { - if (GameMain.NetworkMember != null) + if (GameMain.NetworkMember == null) { return; } + if (GameMain.NetworkMember.IsClient) { syncItemsDelay = 1.0f; } + + if (Owner is Character character) { - if (GameMain.NetworkMember.IsClient) { syncItemsDelay = 1.0f; } - GameMain.NetworkMember.CreateEntityEvent(Owner as INetSerializable, new object[] { NetEntityEvent.Type.InventoryState }); + GameMain.NetworkMember.CreateEntityEvent(character, new Character.InventoryStateEventData()); + } + else if (Owner is Item item) + { + GameMain.NetworkMember.CreateEntityEvent(item, new Item.InventoryStateEventData()); } } public Item FindItem(Func predicate, bool recursive) { - Item match = AllItems.FirstOrDefault(i => predicate(i)); + Item match = AllItems.FirstOrDefault(predicate); if (match == null && recursive) { foreach (var item in AllItems) { - if (item == null) { continue; } - if (item.OwnInventory != null) - { - match = item.OwnInventory.FindItem(predicate, recursive: true); - if (match != null) - { - return match; - } - } + if (item?.OwnInventory == null) { continue; } + + match = item.OwnInventory.FindItem(predicate, recursive: true); + if (match != null) { return match; } } } return match; @@ -946,16 +939,31 @@ namespace Barotrauma slots[index].RemoveItem(item); } - - public void SharedWrite(IWriteMessage msg, object[] extraData = null) + public void SharedRead(IReadMessage msg, out List[] newItemIds) + { + byte slotCount = msg.ReadByte(); + newItemIds = new List[slotCount]; + for (int i = 0; i < slotCount; i++) + { + newItemIds[i] = new List(); + int itemCount = msg.ReadRangedInteger(0, MaxStackSize); + for (int j = 0; j < itemCount; j++) + { + newItemIds[i].Add(msg.ReadUInt16()); + } + } + } + + public void SharedWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { msg.Write((byte)capacity); for (int i = 0; i < capacity; i++) { - msg.WriteRangedInteger(slots[i].ItemCount, 0, MaxStackSize); - foreach (Item item in slots[i].Items) + msg.WriteRangedInteger(slots[i].Items.Count, 0, MaxStackSize); + for (int j = 0; j < Math.Min(slots[i].Items.Count, MaxStackSize); j++) { - msg.Write((ushort)(item == null ? 0 : item.ID)); + var item = slots[i].Items[j]; + msg.Write(item?.ID ?? (ushort)0); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index e892953f4..1e2638509 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -20,7 +20,7 @@ using Microsoft.Xna.Framework.Graphics; namespace Barotrauma { - partial class Item : MapEntity, IDamageable, IIgnorable, ISerializableEntity, IServerSerializable, IClientSerializable + partial class Item : MapEntity, IDamageable, IIgnorable, ISerializableEntity, IServerPositionSync, IClientSerializable { public static List ItemList = new List(); public new ItemPrefab Prefab => base.Prefab as ItemPrefab; @@ -573,7 +573,7 @@ namespace Barotrauma if (connections == null) { return; } foreach (Connection c in connections.Values) { - if (c.IsPower && c.Grid != null) + if (c.IsPower) { Powered.ChangedConnections.Add(c); foreach (Connection conn in c.Recipients) @@ -827,6 +827,23 @@ namespace Barotrauma public bool IgnoreByAI(Character character) => HasTag("ignorebyai") || OrderedToBeIgnored && character.IsOnPlayerTeam; public bool OrderedToBeIgnored { get; set; } + public bool HasBallastFloraInHull + { + get + { + return CurrentHull?.BallastFlora != null; + } + } + + public bool IsClaimedByBallastFlora + { + get + { + if (CurrentHull?.BallastFlora == null) { return false; } + return CurrentHull.BallastFlora.ClaimedTargets.Contains(this); + } + } + public Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine, ushort id = Entity.NullEntityID, bool callOnItemLoaded = true) : this(new Rectangle( (int)(position.X - itemPrefab.Sprite.size.X / 2 * itemPrefab.Scale), @@ -1417,6 +1434,7 @@ namespace Barotrauma public bool HasAccess(Character character) { + if (HiddenInGame) { return false; } if (character.IsBot && IgnoreByAI(character)) { return false; } if (!IsInteractable(character)) { return false; } var itemContainer = GetComponent(); @@ -1656,14 +1674,13 @@ namespace Barotrauma public void SendPendingNetworkUpdates() { - if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsServer) { return; } - if (conditionUpdatePending) - { - GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.Status }); - lastSentCondition = condition; - sendConditionUpdateTimer = NetConfig.ItemConditionUpdateInterval; - conditionUpdatePending = false; - } + if (!(GameMain.NetworkMember is { IsServer: true })) { return; } + if (!conditionUpdatePending) { return; } + + GameMain.NetworkMember.CreateEntityEvent(this, new StatusEventData()); + lastSentCondition = condition; + sendConditionUpdateTimer = NetConfig.ItemConditionUpdateInterval; + conditionUpdatePending = false; } private bool isActive = true; @@ -1919,20 +1936,19 @@ namespace Barotrauma private void HandleCollision(float impact) { OnCollisionProjSpecific(impact); - if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) - { - if (ImpactTolerance > 0.0f && condition > 0.0f && Math.Abs(impact) > ImpactTolerance) - { - ApplyStatusEffects(ActionType.OnImpact, 1.0f); -#if SERVER - GameMain.Server?.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnImpact }); -#endif - } + if (GameMain.NetworkMember is { IsClient: true }) { return; } - foreach (Item contained in ContainedItems) - { - if (contained.body != null) { contained.HandleCollision(impact); } - } + if (ImpactTolerance > 0.0f && condition > 0.0f && Math.Abs(impact) > ImpactTolerance) + { + ApplyStatusEffects(ActionType.OnImpact, 1.0f); +#if SERVER + GameMain.Server?.CreateEntityEvent(this, new ApplyStatusEffectEventData(ActionType.OnImpact)); +#endif + } + + foreach (Item contained in ContainedItems) + { + if (contained.body != null) { contained.HandleCollision(impact); } } } @@ -1995,14 +2011,14 @@ namespace Barotrauma /// /// Note: This function generates garbage and might be a bit too heavy to be used once per frame. /// - public List GetConnectedComponents(bool recursive = false) where T : ItemComponent + public List GetConnectedComponents(bool recursive = false, bool allowTraversingBackwards = true) where T : ItemComponent { List connectedComponents = new List(); if (recursive) { HashSet alreadySearched = new HashSet(); - GetConnectedComponentsRecursive(alreadySearched, connectedComponents); + GetConnectedComponentsRecursive(alreadySearched, connectedComponents, allowTraversingBackwards: allowTraversingBackwards); return connectedComponents; } @@ -2025,7 +2041,7 @@ namespace Barotrauma return connectedComponents; } - private void GetConnectedComponentsRecursive(HashSet alreadySearched, List connectedComponents, bool ignoreInactiveRelays = false) where T : ItemComponent + private void GetConnectedComponentsRecursive(HashSet alreadySearched, List connectedComponents, bool ignoreInactiveRelays = false, bool allowTraversingBackwards = true) where T : ItemComponent { ConnectionPanel connectionPanel = GetComponent(); if (connectionPanel == null) { return; } @@ -2034,18 +2050,18 @@ namespace Barotrauma { if (alreadySearched.Contains(c)) { continue; } alreadySearched.Add(c); - GetConnectedComponentsRecursive(c, alreadySearched, connectedComponents, ignoreInactiveRelays); + GetConnectedComponentsRecursive(c, alreadySearched, connectedComponents, ignoreInactiveRelays, allowTraversingBackwards); } } /// /// Note: This function generates garbage and might be a bit too heavy to be used once per frame. /// - public List GetConnectedComponentsRecursive(Connection c, bool ignoreInactiveRelays = false) where T : ItemComponent + public List GetConnectedComponentsRecursive(Connection c, bool ignoreInactiveRelays = false, bool allowTraversingBackwards = true) where T : ItemComponent { List connectedComponents = new List(); HashSet alreadySearched = new HashSet(); - GetConnectedComponentsRecursive(c, alreadySearched, connectedComponents, ignoreInactiveRelays); + GetConnectedComponentsRecursive(c, alreadySearched, connectedComponents, ignoreInactiveRelays, allowTraversingBackwards); return connectedComponents; } @@ -2062,7 +2078,7 @@ namespace Barotrauma ("signal_in2".ToIdentifier(), "signal_out".ToIdentifier()) }.ToImmutableArray(); - private void GetConnectedComponentsRecursive(Connection c, HashSet alreadySearched, List connectedComponents, bool ignoreInactiveRelays) where T : ItemComponent + private void GetConnectedComponentsRecursive(Connection c, HashSet alreadySearched, List connectedComponents, bool ignoreInactiveRelays, bool allowTraversingBackwards = true) where T : ItemComponent { alreadySearched.Add(c); @@ -2087,12 +2103,12 @@ namespace Barotrauma foreach (Connection wifiOutput in receiverConnections) { if ((wifiOutput.IsOutput == recipient.IsOutput) || alreadySearched.Contains(wifiOutput)) { continue; } - GetConnectedComponentsRecursive(wifiOutput, alreadySearched, connectedComponents, ignoreInactiveRelays); + GetConnectedComponentsRecursive(wifiOutput, alreadySearched, connectedComponents, ignoreInactiveRelays, allowTraversingBackwards); } } } - recipient.Item.GetConnectedComponentsRecursive(recipient, alreadySearched, connectedComponents, ignoreInactiveRelays); + recipient.Item.GetConnectedComponentsRecursive(recipient, alreadySearched, connectedComponents, ignoreInactiveRelays, allowTraversingBackwards); } if (ignoreInactiveRelays) @@ -2111,12 +2127,12 @@ namespace Barotrauma if (pairedConnection != null) { if (alreadySearched.Contains(pairedConnection)) { return; } - GetConnectedComponentsRecursive(pairedConnection, alreadySearched, connectedComponents, ignoreInactiveRelays); + GetConnectedComponentsRecursive(pairedConnection, alreadySearched, connectedComponents, ignoreInactiveRelays, allowTraversingBackwards); } } } searchFromAToB(input, output); - searchFromAToB(output, input); + if (allowTraversingBackwards) { searchFromAToB(output, input); } } } @@ -2490,7 +2506,7 @@ namespace Barotrauma #if CLIENT if (GameMain.Client != null) { - GameMain.Client.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.Treatment, character.ID, targetLimb }); + GameMain.Client.CreateEntityEvent(this, new TreatmentEventData(character, targetLimb)); return; } #endif @@ -2523,12 +2539,10 @@ namespace Barotrauma } } - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + if (GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(this, new object[] - { - NetEntityEvent.Type.ApplyStatusEffect, actionType, ic, character.ID, targetLimb - }); + GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData( + actionType, ic, character, targetLimb)); } if (ic.DeleteOnUse) { remove = true; } @@ -2553,12 +2567,18 @@ namespace Barotrauma if (ic.Combine(item, user)) { isCombined = true; } } #if CLIENT - if (isCombined) { GameMain.Client?.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.Combine, item.ID }); } + if (isCombined) { GameMain.Client?.CreateEntityEvent(this, new CombineEventData(item)); } #endif return isCombined; } - public void Drop(Character dropper, bool createNetworkEvent = true) + /// + /// + /// + /// Character who dropped the item + /// Should clients be notified of the item being dropped + /// Should the transform of the physics body be updated. Only disable this if you're moving the item somewhere else / calling SetTransform manually immediately after dropping! + public void Drop(Character dropper, bool createNetworkEvent = true, bool setTransform = true) { if (createNetworkEvent) { @@ -2585,7 +2605,7 @@ namespace Barotrauma "Failed to drop the item \"" + Name + "\" (body has been removed" + (Removed ? ", item has been removed)" : ")")); } - else + else if (setTransform) { body.SetTransform(dropper.SimPosition, 0.0f); } @@ -2596,7 +2616,10 @@ namespace Barotrauma if (Container != null) { - SetTransform(Container.SimPosition, 0.0f); + if (setTransform) + { + SetTransform(Container.SimPosition, 0.0f); + } Container.RemoveContained(this); Container = null; } @@ -2646,12 +2669,12 @@ namespace Barotrauma return allProperties; } - private void WritePropertyChange(IWriteMessage msg, object[] extraData, bool inGameEditableOnly) + private void WritePropertyChange(IWriteMessage msg, ChangePropertyEventData extraData, bool inGameEditableOnly) { //ignoreConditions: true = include all ConditionallyEditable properties at this point, //to ensure client/server doesn't get any properties mixed up if there's some conditions that can vary between the server and the clients var allProperties = inGameEditableOnly ? GetInGameEditableProperties(ignoreConditions: true) : GetProperties(); - SerializableProperty property = extraData[1] as SerializableProperty; + SerializableProperty property = extraData.SerializableProperty; if (property != null) { var propertyOwner = allProperties.Find(p => p.Second == property); @@ -2910,9 +2933,9 @@ namespace Barotrauma } #endif - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + if (GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.ChangeProperty, property }); + GameMain.NetworkMember.CreateEntityEvent(this, new ChangePropertyEventData(property)); } } @@ -2980,7 +3003,7 @@ namespace Barotrauma #if SERVER if (createNetworkEvent) { - Spawner.CreateNetworkEvent(item, remove: false); + Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item)); } #endif @@ -3016,7 +3039,7 @@ namespace Barotrauma { if (!(property.GetValue(item)?.Equals(prevValue) ?? true)) { - GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ChangeProperty, property }); + GameMain.NetworkMember.CreateEntityEvent(item, new ChangePropertyEventData(property)); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs new file mode 100644 index 000000000..a4a939944 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs @@ -0,0 +1,114 @@ +using System; +using Barotrauma.Items.Components; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + partial class Item + { + public enum EventType + { + ComponentState = 0, + InventoryState = 1, + Treatment = 2, + ChangeProperty = 3, + Combine = 4, + Status = 5, + AssignCampaignInteraction = 6, + ApplyStatusEffect = 7, + Upgrade = 8, + + MinValue = 0, + MaxValue = 6 + } + + public interface IEventData : NetEntityEvent.IData + { + public EventType EventType { get; } + } + + public struct ComponentStateEventData : IEventData + { + public EventType EventType => EventType.ComponentState; + public readonly ItemComponent Component; + public readonly ItemComponent.IEventData ComponentData; + + public ComponentStateEventData(ItemComponent component, ItemComponent.IEventData componentData) + { + Component = component; + ComponentData = componentData; + } + } + + public readonly struct InventoryStateEventData : IEventData + { + public EventType EventType => EventType.InventoryState; + public readonly ItemContainer Component; + + public InventoryStateEventData(ItemContainer component) + { + Component = component; + } + } + + public readonly struct ChangePropertyEventData : IEventData + { + public EventType EventType => EventType.ChangeProperty; + public readonly SerializableProperty SerializableProperty; + + public ChangePropertyEventData(SerializableProperty serializableProperty) + { + SerializableProperty = serializableProperty; + } + } + + private readonly struct StatusEventData : IEventData + { + public EventType EventType => EventType.Status; + } + + private readonly struct AssignCampaignInteractionEventData : IEventData + { + public EventType EventType => EventType.AssignCampaignInteraction; + } + + public readonly struct ApplyStatusEffectEventData : IEventData + { + public EventType EventType => EventType.ApplyStatusEffect; + public readonly ActionType ActionType; + public readonly ItemComponent TargetItemComponent; + public readonly Character TargetCharacter; + public readonly Limb TargetLimb; + public readonly Entity UseTarget; + public readonly Vector2? WorldPosition; + + public ApplyStatusEffectEventData( + ActionType actionType, + ItemComponent targetItemComponent = null, + Character targetCharacter = null, + Limb targetLimb = null, + Entity useTarget = null, + Vector2? worldPosition = null) + { + ActionType = actionType; + TargetItemComponent = targetItemComponent; + TargetCharacter = targetCharacter; + TargetLimb = targetLimb; + UseTarget = useTarget; + WorldPosition = worldPosition; + } + } + + private readonly struct UpgradeEventData : IEventData + { + public EventType EventType => EventType.Upgrade; + public readonly Upgrade Upgrade; + + public UpgradeEventData(Upgrade upgrade) + { + Upgrade = upgrade; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs index 7df7912f9..a25062e49 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs @@ -48,14 +48,14 @@ namespace Barotrauma if (ItemOwnsSelf(item)) { return false; } if (i < 0 || i >= slots.Length) { return false; } if (!container.CanBeContained(item, i)) { return false; } - return item != null && slots[i].CanBePut(item, ignoreCondition) && slots[i].ItemCount < container.GetMaxStackSize(i); + return item != null && slots[i].CanBePut(item, ignoreCondition) && slots[i].Items.Count < container.GetMaxStackSize(i); } public override bool CanBePutInSlot(ItemPrefab itemPrefab, int i, float? condition, int? quality = null) { if (i < 0 || i >= slots.Length) { return false; } if (!container.CanBeContained(itemPrefab, i)) { return false; } - return itemPrefab != null && slots[i].CanBePut(itemPrefab, condition, quality) && slots[i].ItemCount < container.GetMaxStackSize(i); + return itemPrefab != null && slots[i].CanBePut(itemPrefab, condition, quality) && slots[i].Items.Count < container.GetMaxStackSize(i); } public override int HowManyCanBePut(ItemPrefab itemPrefab, int i, float? condition) @@ -74,7 +74,7 @@ namespace Barotrauma { if (!slots[i].Any()) { return false; } var item = slots[i].FirstOrDefault(); - if (slots[i].ItemCount < Math.Min(item.Prefab.MaxStackSize, container.GetMaxStackSize(i))) { return false; } + if (slots[i].Items.Count < Math.Min(item.Prefab.MaxStackSize, container.GetMaxStackSize(i))) { return false; } } } else @@ -145,8 +145,7 @@ namespace Barotrauma return; } - int componentIndex = container.Item.GetComponentIndex(container); - if (componentIndex == -1) + if (!container.Item.Components.Contains(container)) { DebugConsole.Log("Creating a network event for the item \"" + container.Item + "\" failed, ItemContainer not found in components"); return; @@ -155,7 +154,7 @@ namespace Barotrauma if (GameMain.NetworkMember != null) { if (GameMain.NetworkMember.IsClient) { syncItemsDelay = 1.0f; } - GameMain.NetworkMember.CreateEntityEvent(Owner as INetSerializable, new object[] { NetEntityEvent.Type.InventoryState, componentIndex }); + GameMain.NetworkMember.CreateEntityEvent(Owner as INetSerializable, new Item.InventoryStateEventData(container)); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index 23137fab7..eaa18470a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -43,10 +43,6 @@ namespace Barotrauma OutConditionMax = element.GetAttributeFloat("outconditionmax", element.GetAttributeFloat("outcondition", 1.0f)); CopyCondition = element.GetAttributeBool("copycondition", false); Commonness = element.GetAttributeFloat("commonness", 1.0f); - if (element.Attribute("copycondition") != null && element.Attribute("outcondition") != null) - { - DebugConsole.AddWarning($"Invalid deconstruction output in \"{parentDebugName}\": the output item \"{ItemIdentifier}\" has the out condition set, but is also set to copy the condition of the deconstructed item. Ignoring the out condition."); - } RequiredDeconstructor = element.GetAttributeStringArray("requireddeconstructor", element.Parent?.GetAttributeStringArray("requireddeconstructor", new string[0]) ?? new string[0]); RequiredOtherItem = element.GetAttributeStringArray("requiredotheritem", new string[0]); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs index 9e02177fb..9708fd2e4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Xml.Linq; using Barotrauma.Extensions; using Barotrauma.Items.Components; +using Barotrauma.Networking; using FarseerPhysics; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; @@ -58,7 +59,9 @@ namespace Barotrauma.MapCreatures.Behavior public float AccumulatedDamage; public float DamageVisualizationTimer; +#if CLIENT public Vector2 ShakeAmount; +#endif // Adjacent tiles, used to free up sides when this branch gets removed public readonly Dictionary Connections = new Dictionary(); @@ -286,7 +289,7 @@ namespace Barotrauma.MapCreatures.Behavior public float PowerConsumptionTimer; private float defenseCooldown, toxinsCooldown, fireCheckCooldown; - private float selfDamageTimer, toxinsTimer; + private float selfDamageTimer, toxinsTimer, toxinsSpawnTimer; private readonly List branchesVulnerableToFire = new List(); @@ -552,11 +555,12 @@ namespace Barotrauma.MapCreatures.Behavior Anger -= deltaTime; } - // This entire scope is probably very heavy for GC, need to experiment if (toxinsTimer > 0.1f) { - if (!AttackItemPrefab.IsEmpty) + toxinsSpawnTimer -= deltaTime; + if (!AttackItemPrefab.IsEmpty && toxinsSpawnTimer <= 0.0f) { + toxinsSpawnTimer = 1.0f; Dictionary> branches = new Dictionary>(); foreach (BallastFloraBranch branch in Branches) { @@ -581,7 +585,7 @@ namespace Barotrauma.MapCreatures.Behavior randomBranch.SpawningItem = true; ItemPrefab prefab = ItemPrefab.Find(null, AttackItemPrefab); - #warning TODO: Parent needs a nullability sanity check +#warning TODO: Parent needs a nullability sanity check Entity.Spawner?.AddItemToSpawnQueue(prefab, Parent!.Position + Offset + randomBranch.Position, Parent.Submarine, onSpawned: item => { randomBranch.AttackItem = item; @@ -826,13 +830,13 @@ namespace Barotrauma.MapCreatures.Behavior { if (root != null) { - Vector2 rootGrowthPos = Rand.Vector(rootGrowthCount * Rand.Range(3.0f, 5.0f)); + Vector2 rootGrowthPos = Rand.Vector(Math.Max(rootGrowthCount, 1) * Rand.Range(3.0f, 5.0f)); TryGrowBranch(root, TileSide.None, out List newRootGrowth, isRootGrowth: true, forcePosition: rootGrowthPos); } } #if SERVER - SendNetworkMessage(this, NetworkHeader.BranchCreate, newBranch, parent.ID); + SendNetworkMessage(new BranchCreateEventData(newBranch, parent)); #endif return true; } @@ -874,7 +878,7 @@ namespace Barotrauma.MapCreatures.Behavior #if SERVER if (!load) { - SendNetworkMessage(this, NetworkHeader.Infect, target.ID, true, branch); + SendNetworkMessage(new InfectEventData(target, InfectEventData.InfectState.Yes, branch)); } #endif } @@ -1002,8 +1006,10 @@ namespace Barotrauma.MapCreatures.Behavior StateMachine.EnterState(new DefendWithPumpState(branch, ClaimedTargets, attacker)); defenseCooldown = 180f; } - - defenseCooldown = 10f; + else + { + defenseCooldown = 10f; + } } } @@ -1104,7 +1110,7 @@ namespace Barotrauma.MapCreatures.Behavior #if SERVER if (!wasRemoved) { - SendNetworkMessage(this, NetworkHeader.BranchRemove, branch); + SendNetworkMessage(new BranchRemoveEventData(branch)); } #endif } @@ -1135,7 +1141,7 @@ namespace Barotrauma.MapCreatures.Behavior } }); #if SERVER - SendNetworkMessage(this, NetworkHeader.Infect, item.ID, false); + SendNetworkMessage(new InfectEventData(item, InfectEventData.InfectState.No, null)); #endif } @@ -1153,7 +1159,7 @@ namespace Barotrauma.MapCreatures.Behavior StateMachine?.State?.Exit(); #if SERVER - SendNetworkMessage(this, NetworkHeader.Kill); + SendNetworkMessage(new KillEventData()); #endif } @@ -1175,7 +1181,7 @@ namespace Barotrauma.MapCreatures.Behavior _entityList.Remove(this); #if SERVER - SendNetworkMessage(this, NetworkHeader.Remove); + SendNetworkMessage(new KillEventData()); #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraEventData.cs new file mode 100644 index 000000000..40c174e1a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraEventData.cs @@ -0,0 +1,74 @@ +using Barotrauma.Networking; + +namespace Barotrauma.MapCreatures.Behavior +{ + internal partial class BallastFloraBehavior + { + public interface IEventData : NetEntityEvent.IData + { + public NetworkHeader NetworkHeader { get; } + } + + public readonly struct SpawnEventData : IEventData + { + public NetworkHeader NetworkHeader => NetworkHeader.Spawn; + } + + private readonly struct KillEventData : IEventData + { + public NetworkHeader NetworkHeader => NetworkHeader.Kill; + } + + private readonly struct BranchCreateEventData : IEventData + { + public NetworkHeader NetworkHeader => NetworkHeader.BranchCreate; + public readonly BallastFloraBranch NewBranch; + public readonly BallastFloraBranch Parent; + + public BranchCreateEventData(BallastFloraBranch newBranch, BallastFloraBranch parent) + { + NewBranch = newBranch; + Parent = parent; + } + } + + private readonly struct BranchRemoveEventData : IEventData + { + public NetworkHeader NetworkHeader => NetworkHeader.BranchRemove; + public readonly BallastFloraBranch Branch; + + public BranchRemoveEventData(BallastFloraBranch branch) + { + Branch = branch; + } + } + + private readonly struct BranchDamageEventData : IEventData + { + public NetworkHeader NetworkHeader => NetworkHeader.BranchDamage; + public readonly BallastFloraBranch Branch; + + public BranchDamageEventData(BallastFloraBranch branch) + { + Branch = branch; + } + } + + private readonly struct InfectEventData : IEventData + { + public enum InfectState { Yes, No } + + public NetworkHeader NetworkHeader => NetworkHeader.Infect; + public readonly Item Item; + public readonly InfectState Infect; + public readonly BallastFloraBranch Infector; + + public InfectEventData(Item item, InfectState infect, BallastFloraBranch infector) + { + Item = item; + Infect = infect; + Infector = infector; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/BallastFloraStateMachine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/BallastFloraStateMachine.cs index 7d0eb16bd..af4265b28 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/BallastFloraStateMachine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/BallastFloraStateMachine.cs @@ -17,6 +17,8 @@ namespace Barotrauma.MapCreatures.Behavior { lastState = State; State?.Exit(); + State = null; + newState.Enter(); State = newState; } @@ -35,11 +37,9 @@ namespace Barotrauma.MapCreatures.Behavior { case ExitState.Running: break; - case ExitState.ReturnLast when lastState != null && lastState.GetState() == ExitState.Running: EnterState(lastState); break; - default: EnterState(new GrowIdleState(parent)); break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs index ab74f2a8e..beace2553 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using Barotrauma.IO; using System.Linq; using System.Text; +using Barotrauma.Networking; namespace Barotrauma { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index 442917563..a26a12e44 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -237,9 +237,9 @@ namespace Barotrauma if (!fireProof) { item.ApplyStatusEffects(ActionType.OnFire, 1.0f); - if (item.Condition <= 0.0f && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + if (item.Condition <= 0.0f && GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFire }); + GameMain.NetworkMember.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnFire)); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs index ee66d4483..d00151206 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs @@ -363,9 +363,9 @@ namespace Barotrauma if (item.Position.Y < position.Y - size.Y || item.Position.Y > hull.Rect.Y) { continue; } item.ApplyStatusEffects(ActionType.OnFire, deltaTime); - if (item.Condition <= 0.0f && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + if (item.Condition <= 0.0f && GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFire }); + GameMain.NetworkMember.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnFire)); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index bc8844700..698152a24 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -248,16 +248,17 @@ namespace Barotrauma } linkedTo.Clear(); + int tolerance = 1; Vector2[] searchPos = new Vector2[2]; if (IsHorizontal) { - searchPos[0] = new Vector2(rect.X, rect.Y - rect.Height / 2); - searchPos[1] = new Vector2(rect.Right, rect.Y - rect.Height / 2); + searchPos[0] = new Vector2(rect.X - tolerance, rect.Y - rect.Height / 2); + searchPos[1] = new Vector2(rect.Right + tolerance, rect.Y - rect.Height / 2); } else { - searchPos[0] = new Vector2(rect.Center.X, rect.Y); - searchPos[1] = new Vector2(rect.Center.X, rect.Y - rect.Height); + searchPos[0] = new Vector2(rect.Center.X, rect.Y + tolerance); + searchPos[1] = new Vector2(rect.Center.X, rect.Y - rect.Height - tolerance); } for (int i = 0; i < 2; i++) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index d86671e8d..c83684db8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -320,7 +320,8 @@ namespace Barotrauma roomName != null && ( roomName.Contains("ballast", StringComparison.OrdinalIgnoreCase) || roomName.Contains("bilge", StringComparison.OrdinalIgnoreCase) || - roomName.Contains("airlock", StringComparison.OrdinalIgnoreCase)); + roomName.Contains("airlock", StringComparison.OrdinalIgnoreCase) || + roomName.Contains("dockingport", StringComparison.OrdinalIgnoreCase)); private bool isWetRoom; [Editable, Serialize(false, IsPropertySaveable.Yes, description: "It's normal for this hull to be filled with water. If the room name contains 'ballast', 'bilge', or 'airlock', you can't disable this setting.")] @@ -729,9 +730,9 @@ namespace Barotrauma var decal = DecalManager.CreateDecal(decalName, scale, worldPosition, this, spriteIndex); if (decal != null) { - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + if (GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(this, new object[] { false }); + GameMain.NetworkMember.CreateEntityEvent(this, new DecalEventData()); } decals.Add(decal); } @@ -739,6 +740,96 @@ namespace Barotrauma return decal; } + #region Shared network write + private void SharedStatusWrite(IWriteMessage msg) + { + msg.WriteRangedSingle(MathHelper.Clamp(waterVolume / Volume, 0.0f, 1.5f), 0.0f, 1.5f, 8); + + msg.WriteRangedInteger(Math.Min(FireSources.Count, 16), 0, 16); + for (int i = 0; i < Math.Min(FireSources.Count, 16); i++) + { + var fireSource = FireSources[i]; + Vector2 normalizedPos = new Vector2( + (fireSource.Position.X - rect.X) / rect.Width, + (fireSource.Position.Y - (rect.Y - rect.Height)) / rect.Height); + + msg.WriteRangedSingle(MathHelper.Clamp(normalizedPos.X, 0.0f, 1.0f), 0.0f, 1.0f, 8); + msg.WriteRangedSingle(MathHelper.Clamp(normalizedPos.Y, 0.0f, 1.0f), 0.0f, 1.0f, 8); + msg.WriteRangedSingle(MathHelper.Clamp(fireSource.Size.X / rect.Width, 0.0f, 1.0f), 0, 1.0f, 8); + } + } + + private void SharedBackgroundSectionsWrite(IWriteMessage msg, in BackgroundSectionsEventData backgroundSectionsEventData) + { + int sectorToUpdate = backgroundSectionsEventData.SectorStartIndex; + int start = sectorToUpdate * BackgroundSectionsPerNetworkEvent; + int end = Math.Min((sectorToUpdate + 1) * BackgroundSectionsPerNetworkEvent, BackgroundSections.Count - 1); + msg.WriteRangedInteger(sectorToUpdate, 0, BackgroundSections.Count - 1); + for (int i = start; i < end; i++) + { + msg.WriteRangedSingle(BackgroundSections[i].ColorStrength, 0.0f, 1.0f, 8); + msg.Write(BackgroundSections[i].Color.PackedValue); + } + } + #endregion + + #region Shared network read + public readonly struct NetworkFireSource + { + public readonly Vector2 Position; + public readonly float Size; + + public NetworkFireSource(Hull hull, Vector2 normalizedPosition, float normalizedSize) + { + Position = hull.Rect.Location.ToVector2() + + new Vector2(0, -hull.Rect.Height) + + normalizedPosition * hull.Rect.Size.ToVector2(); + Size = normalizedSize * hull.Rect.Width; + } + } + + private void SharedStatusRead(IReadMessage msg, out float newWaterVolume, out NetworkFireSource[] newFireSources) + { + newWaterVolume = msg.ReadRangedSingle(0.0f, 1.5f, 8) * Volume; + + int fireSourceCount = msg.ReadRangedInteger(0, 16); + newFireSources = new NetworkFireSource[fireSourceCount]; + for (int i = 0; i < fireSourceCount; i++) + { + float x = MathHelper.Clamp(msg.ReadRangedSingle(0.0f, 1.0f, 8), 0.05f, 0.95f); + float y = MathHelper.Clamp(msg.ReadRangedSingle(0.0f, 1.0f, 8), 0.05f, 0.95f); + float size = msg.ReadRangedSingle(0.0f, 1.0f, 8); + newFireSources[i] = new NetworkFireSource(this, new Vector2(x, y), size); + } + } + + private readonly struct BackgroundSectionNetworkUpdate + { + public readonly int SectionIndex; + public readonly Color Color; + public readonly float ColorStrength; + public BackgroundSectionNetworkUpdate(int sectionIndex, Color color, float colorStrength) + { + SectionIndex = sectionIndex; + Color = color; + ColorStrength = colorStrength; + } + } + + private void SharedBackgroundSectionRead(IReadMessage msg, Action action, out int sectorToUpdate) + { + sectorToUpdate = msg.ReadRangedInteger(0, BackgroundSections.Count - 1); + int start = sectorToUpdate * BackgroundSectionsPerNetworkEvent; + int end = Math.Min((sectorToUpdate + 1) * BackgroundSectionsPerNetworkEvent, BackgroundSections.Count - 1); + for (int i = start; i < end; i++) + { + float colorStrength = msg.ReadRangedSingle(0.0f, 1.0f, 8); + Color color = new Color(msg.ReadUInt32()); + + action(new BackgroundSectionNetworkUpdate(i, color, colorStrength)); + } + } + #endregion public override void Update(float deltaTime, Camera cam) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/HullEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/HullEventData.cs new file mode 100644 index 000000000..bf28c09aa --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/HullEventData.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Barotrauma.Extensions; +using Barotrauma.MapCreatures.Behavior; +using Barotrauma.Networking; + +namespace Barotrauma +{ + partial class Hull + { + [Flags] + public enum EventType + { + Status = 0, + Decal = 1, + BackgroundSections = 2, + BallastFlora = 3, + + MinValue = 0, + MaxValue = 3 + } + + public interface IEventData : NetEntityEvent.IData + { + public EventType EventType { get; } + } + + private readonly struct StatusEventData : IEventData + { + public EventType EventType => EventType.Status; + } + + private readonly struct DecalEventData : IEventData + { + public EventType EventType => EventType.Decal; + public readonly Decal Decal; + + public DecalEventData(Decal decal) + { + Decal = decal; + } + } + + private readonly struct BackgroundSectionsEventData : IEventData + { + public EventType EventType => EventType.BackgroundSections; + public readonly int SectorStartIndex; + + public BackgroundSectionsEventData(int sectorStartIndex) + { + SectorStartIndex = sectorStartIndex; + } + } + + public readonly struct BallastFloraEventData : IEventData + { + public EventType EventType => EventType.BallastFlora; + public readonly BallastFloraBehavior Behavior; + public readonly BallastFloraBehavior.IEventData SubEventData; + + public BallastFloraEventData(BallastFloraBehavior behavior, BallastFloraBehavior.IEventData subEventData) + { + Behavior = behavior; + SubEventData = subEventData; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/IDamageable.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/IDamageable.cs index 0d653cd60..f48a4ec23 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/IDamageable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/IDamageable.cs @@ -1,24 +1,31 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; namespace Barotrauma { interface IDamageable { - Vector2 SimPosition - { - get; - } - - Vector2 WorldPosition - { - get; - } - - float Health - { - get; - } + Vector2 SimPosition { get; } + Vector2 WorldPosition { get; } + float Health { get; } AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound=true); + + + public readonly struct AttackEventData + { + public readonly ISpatialEntity Attacker; + public readonly IDamageable TargetEntity; + public readonly Limb TargetLimb; + public readonly Vector2 AttackSimPosition; + + public AttackEventData(ISpatialEntity attacker, IDamageable targetEntity, Limb targetLimb, Vector2 attackSimPosition) + { + Attacker = attacker; + TargetEntity = targetEntity; + TargetLimb = targetLimb; + AttackSimPosition = attackSimPosition; + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 5fac8f4b8..cd5e684d5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -18,6 +18,12 @@ namespace Barotrauma { partial class Level : Entity, IServerSerializable { + public enum EventType + { + SingleDestructibleWall, + GlobalDestructibleWall + } + //all entities are disabled after they reach this depth public const int MaxEntityDepth = -300000; public const float ShaftHeight = 1000.0f; @@ -3136,13 +3142,14 @@ namespace Barotrauma UnsyncedExtraWalls[i].Update(deltaTime); } - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) +#if SERVER + if (GameMain.NetworkMember is { IsServer: true }) { foreach (LevelWall wall in ExtraWalls) { - if (wall is DestructibleLevelWall destructibleWall && destructibleWall.NetworkUpdatePending) + if (wall is DestructibleLevelWall { NetworkUpdatePending: true } destructibleWall) { - GameMain.NetworkMember.CreateEntityEvent(this, new object[] { destructibleWall }); + GameMain.NetworkMember.CreateEntityEvent(this, new SingleLevelWallEventData(destructibleWall)); destructibleWall.NetworkUpdatePending = false; } } @@ -3151,11 +3158,12 @@ namespace Barotrauma { if (ExtraWalls.Any(w => w.Body.BodyType != BodyType.Static)) { - GameMain.NetworkMember.CreateEntityEvent(this); + GameMain.NetworkMember.CreateEntityEvent(this, new GlobalLevelWallEventData()); } networkUpdateTimer = 0.0f; } } +#endif #if CLIENT backgroundCreatureManager.Update(deltaTime, cam); @@ -4288,28 +4296,5 @@ namespace Barotrauma Loaded = null; } - - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) - { - if (extraData != null && extraData.Length > 0 && extraData[0] is DestructibleLevelWall destructibleWall) - { - int index = ExtraWalls.IndexOf(destructibleWall); - msg.Write(false); - msg.Write((ushort)(index == -1 ? ushort.MaxValue : index)); - //write health using one byte - msg.Write((byte)MathHelper.Clamp((int)(MathUtils.InverseLerp(0.0f, destructibleWall.MaxHealth, destructibleWall.Damage) * 255.0f), 0, 255)); - } - else - { - msg.Write(true); - foreach (LevelWall levelWall in ExtraWalls) - { - if (levelWall.Body.BodyType == BodyType.Static) { continue; } - msg.Write(levelWall.Body.Position.X); - msg.Write(levelWall.Body.Position.Y); - msg.WriteRangedSingle(levelWall.MoveState, 0.0f, MathHelper.TwoPi, 16); - } - } - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs index 9bb8b1561..c3b30aac8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -30,6 +30,16 @@ namespace Barotrauma { } + private readonly struct EventData : NetEntityEvent.IData + { + public readonly LevelObject LevelObject; + + public EventData(LevelObject levelObject) + { + LevelObject = levelObject; + } + } + class SpawnPosition { public readonly GraphEdge GraphEdge; @@ -522,12 +532,12 @@ namespace Barotrauma foreach (LevelObject obj in updateableObjects) { - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + if (GameMain.NetworkMember is { IsServer: true }) { obj.NetworkUpdateTimer -= deltaTime; if (obj.NeedsNetworkSyncing && obj.NetworkUpdateTimer <= 0.0f) { - GameMain.NetworkMember.CreateEntityEvent(this, new object[] { obj }); + GameMain.NetworkMember.CreateEntityEvent(this, new EventData(obj)); obj.NeedsNetworkSyncing = false; obj.NetworkUpdateTimer = NetConfig.LevelObjectUpdateInterval; } @@ -607,9 +617,10 @@ namespace Barotrauma partial void RemoveProjSpecific(); - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - LevelObject obj = extraData[0] as LevelObject; + if (!(extraData is EventData eventData)) { throw new Exception($"Malformed LevelObjectManager event: expected {nameof(LevelObjectManager)}.{nameof(EventData)}"); } + LevelObject obj = eventData.LevelObject; msg.WriteRangedInteger(objects.IndexOf(obj), 0, objects.Count); obj.ServerWrite(msg, c); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index a6884d570..b70413c68 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -734,7 +734,7 @@ namespace Barotrauma if (prefab == null) { continue; } var qty = stockElement.GetAttributeInt("qty", 0); if (qty < 1) { continue; } - StoreStock.Add(new PurchasedItem(prefab, qty)); + StoreStock.Add(new PurchasedItem(prefab, qty, buyer: null)); } StepsSinceSpecialsUpdated = storeElement.GetAttributeInt("stepssincespecialsupdated", 0); @@ -792,7 +792,7 @@ namespace Barotrauma { quantity = priceInfo.MinAvailableAmount; } - stock.Add(new PurchasedItem(prefab, quantity)); + stock.Add(new PurchasedItem(prefab, quantity, buyer: null)); } } return stock; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs index 91da0bd7d..84bf74588 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs @@ -34,7 +34,7 @@ namespace Barotrauma public override Sprite Sprite { get; } - public override string OriginalName => Name.Value; + public override string OriginalName { get; } public override ImmutableHashSet Tags { get; } @@ -132,7 +132,7 @@ namespace Barotrauma public StructurePrefab(ContentXElement element, StructureFile file) : base(element, file) { - Name = element.GetAttributeString("name", ""); + OriginalName = element.GetAttributeString("name", ""); ConfigElement = element; var parentType = element.Parent?.GetAttributeIdentifier("prefabtype", Identifier.Empty) ?? Identifier.Empty; @@ -144,20 +144,16 @@ namespace Barotrauma Identifier descriptionIdentifier = element.GetAttributeIdentifier("descriptionidentifier", ""); - if (Name.IsNullOrEmpty()) - { - Name = TextManager.Get($"EntityName.{Identifier}"); - if (!nameIdentifier.IsEmpty) - { - Name = TextManager.Get($"EntityName.{nameIdentifier}").Fallback(Name); - } + Name = TextManager.Get(nameIdentifier.IsEmpty + ? $"EntityName.{Identifier}" + : $"EntityName.{nameIdentifier}", + $"EntityName.{fallbackNameIdentifier}"); - if (!fallbackNameIdentifier.IsEmpty) - { - Name = Name.Fallback(TextManager.Get($"EntityName.{fallbackNameIdentifier}")); - } + if (parentType == "wrecked") + { + Name = TextManager.GetWithVariable("wreckeditemformat", "[name]", Name); } - + var tags = new HashSet(); string joinedTags = element.GetAttributeString("tags", ""); if (string.IsNullOrEmpty(joinedTags)) joinedTags = element.GetAttributeString("Tags", ""); @@ -251,14 +247,6 @@ namespace Barotrauma DecorativeSpriteGroups = decorativeSpriteGroups.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray())).ToImmutableDictionary(); #endif - if (parentType == "wrecked") - { - if (!Name.IsNullOrEmpty()) - { - Name = TextManager.GetWithVariable("wreckeditemformat", "[name]", Name); - } - } - string categoryStr = element.GetAttributeString("category", "Structure"); if (!Enum.TryParse(categoryStr, true, out MapEntityCategory category)) { @@ -323,6 +311,15 @@ namespace Barotrauma DebugConsole.ThrowError( "Structure prefab \"" + Name + "\" has no identifier. All structure prefabs have a unique identifier string that's used to differentiate between items during saving and loading."); } +#if DEBUG + if (!Category.HasFlag(MapEntityCategory.Legacy) && !HideInMenus) + { + if (!string.IsNullOrEmpty(OriginalName)) + { + DebugConsole.AddWarning($"Structure \"{(Identifier == Identifier.Empty ? Name : Identifier.Value)}\" has a hard-coded name, and won't be localized to other languages."); + } + } +#endif Tags = tags.ToImmutableHashSet(); AllowedLinks = Enumerable.Empty().ToImmutableHashSet(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index 1ba321562..b205c4af5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -21,7 +21,7 @@ namespace Barotrauma None = 0, Left = 1, Right = 2 } - partial class Submarine : Entity, IServerSerializable + partial class Submarine : Entity, IServerPositionSync { public SubmarineInfo Info { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs index b56eb93b7..3706116ef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs @@ -219,8 +219,12 @@ namespace Barotrauma.Networking public static string ApplyDistanceEffect(string message, ChatMessageType type, Character sender, Character receiver) { if (sender == null) { return ""; } - - string spokenMsg = ApplyDistanceEffect(receiver, sender, message, SpeakRange * (1.0f - sender.SpeechImpediment / 100.0f), 3.0f); + float range = SpeakRange; + if (type == ChatMessageType.Default && sender.SpeechImpediment > 0) + { + range *= 1.0f - sender.SpeechImpediment / 100.0f; + } + string spokenMsg = ApplyDistanceEffect(receiver, sender, message, range, 3.0f); switch (type) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs index c5095ed42..f48d81a46 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; using Barotrauma.Extensions; @@ -18,6 +19,13 @@ namespace Barotrauma.Networking private static PipeType writeStream; private static PipeType readStream; + private enum WriteStatus : byte + { + Success = 0x00, + Heartbeat = 0x01, + Crash = 0xFF + } + private static ManualResetEvent writeManualResetEvent; private static volatile bool shutDown; @@ -29,6 +37,8 @@ namespace Barotrauma.Networking private static int readIncTotal; private static ConcurrentQueue msgsToWrite; + private static ConcurrentQueue errorsToWrite; + private static ConcurrentQueue msgsToRead; private static Thread readThread; @@ -44,6 +54,8 @@ namespace Barotrauma.Networking readTempBytes = new byte[ReadBufferSize]; msgsToWrite = new ConcurrentQueue(); + errorsToWrite = new ConcurrentQueue(); + msgsToRead = new ConcurrentQueue(); shutDown = false; @@ -127,9 +139,11 @@ namespace Barotrauma.Networking } } + static partial void HandleCrashString(string str); + private static void UpdateRead() { - Span msgLengthSpan = stackalloc byte[2]; + Span msgLengthSpan = stackalloc byte[3]; while (!shutDown) { CheckPipeConnected(nameof(readStream), readStream); @@ -154,13 +168,26 @@ namespace Barotrauma.Networking if (!readBytes(msgLengthSpan)) { shutDown = true; break; } int msgLength = msgLengthSpan[0] | (msgLengthSpan[1] << 8); + WriteStatus writeStatus = (WriteStatus)msgLengthSpan[2]; if (msgLength > 0) { byte[] msg = new byte[msgLength]; if (!readBytes(msg.AsSpan())) { shutDown = true; break; } - msgsToRead.Enqueue(msg); + switch (writeStatus) + { + case WriteStatus.Success: + msgsToRead.Enqueue(msg); + break; + case WriteStatus.Heartbeat: + //do nothing + break; + case WriteStatus.Crash: + HandleCrashString(Encoding.UTF8.GetString(msg)); + shutDown = true; + break; + } } Thread.Yield(); @@ -173,9 +200,9 @@ namespace Barotrauma.Networking { CheckPipeConnected(nameof(writeStream), writeStream); - bool msgAvailable; byte[] msg; + byte[] msg; - void writeMsg() + void writeMsg(WriteStatus writeStatus) { // It's SUPER IMPORTANT that this stack allocation // remains in this local function and is never inlined, @@ -183,11 +210,12 @@ namespace Barotrauma.Networking // when the function returns; placing it in the loop // this method is based around would lead to a stack // overflow real quick! - Span bytesToWrite = stackalloc byte[2 + msg.Length]; + Span bytesToWrite = stackalloc byte[3 + msg.Length]; bytesToWrite[0] = (byte)(msg.Length & 0xFF); bytesToWrite[1] = (byte)((msg.Length >> 8) & 0xFF); - Span msgSlice = bytesToWrite.Slice(2, msg.Length); + bytesToWrite[2] = (byte)writeStatus; + Span msgSlice = bytesToWrite.Slice(3, msg.Length); msg.AsSpan().CopyTo(msgSlice); @@ -209,15 +237,20 @@ namespace Barotrauma.Networking } } - msgAvailable = msgsToWrite.TryDequeue(out msg); - while (msgAvailable) + while (errorsToWrite.TryDequeue(out var error)) { - writeMsg(); + msg = Encoding.UTF8.GetBytes(error); + writeMsg(WriteStatus.Crash); + shutDown = true; + } + + while (msgsToWrite.TryDequeue(out msg)) + { + writeMsg(WriteStatus.Success); if (shutDown) { break; } - - msgAvailable = msgsToWrite.TryDequeue(out msg); } + if (!shutDown) { writeManualResetEvent.Reset(); @@ -226,7 +259,7 @@ namespace Barotrauma.Networking if (shutDown) { return; } //heartbeat to keep the other end alive - msg = Array.Empty(); writeMsg(); + msg = Array.Empty(); writeMsg(WriteStatus.Heartbeat); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs index f411d9107..8865bf8dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs @@ -5,6 +5,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; +using Steamworks.ServerList; namespace Barotrauma { @@ -196,51 +197,60 @@ namespace Barotrauma private readonly Queue spawnQueue; private readonly Queue removeQueue; - public class SpawnOrRemove + public abstract class SpawnOrRemove : NetEntityEvent.IData { public readonly Entity Entity; + public UInt16 ID => Entity.ID; + + public readonly UInt16 InventoryID; - public readonly UInt16 OriginalID, OriginalInventoryID; - public readonly int OriginalSlotIndex; - - public readonly byte OriginalItemContainerIndex; - - public readonly bool Remove = false; + public readonly byte ItemContainerIndex; + public readonly int SlotIndex; public override string ToString() { return - (Remove ? "Remove" : "Spawn") + "(" + + "(" + ((Entity as MapEntity)?.Name ?? "[NULL]") + - $", {OriginalID}, {OriginalInventoryID})"; + $", {ID}, {InventoryID}, {SlotIndex})"; } - public SpawnOrRemove(Entity entity, bool remove) + protected SpawnOrRemove(Entity entity) { Entity = entity; - OriginalID = entity.ID; - if (entity is Item item && item.ParentInventory?.Owner != null) + if (!(entity is Item { ParentInventory: { Owner: { } } } item)) { return; } + + InventoryID = item.ParentInventory.Owner.ID; + SlotIndex = item.ParentInventory.FindIndex(item); + //find the index of the ItemContainer this item is inside to get the item to + //spawn in the correct inventory in multi-inventory items like fabricators + if (item.Container == null) { return; } + + foreach (ItemComponent component in item.Container.Components) { - OriginalInventoryID = item.ParentInventory.Owner.ID; - OriginalSlotIndex = item.ParentInventory.FindIndex(item); - //find the index of the ItemContainer this item is inside to get the item to - //spawn in the correct inventory in multi-inventory items like fabricators - if (item.Container != null) + if (component is ItemContainer container && + container.Inventory == item.ParentInventory) { - foreach (ItemComponent component in item.Container.Components) - { - if (component is ItemContainer container && - container.Inventory == item.ParentInventory) - { - OriginalItemContainerIndex = (byte)item.Container.GetComponentIndex(component); - break; - } - } + ItemContainerIndex = (byte)item.Container.GetComponentIndex(component); + break; } } - Remove = remove; } } + + public sealed class SpawnEntity : SpawnOrRemove + { + public SpawnEntity(Entity entity) : base(entity) { } + public override string ToString() + => $"Spawn {base.ToString()}"; + } + + public sealed class RemoveEntity : SpawnOrRemove + { + public RemoveEntity(Entity entity) : base(entity) { } + public override string ToString() + => $"Remove {base.ToString()}"; + } public EntitySpawner() : base(null, Entity.EntitySpawnerID) @@ -397,20 +407,19 @@ namespace Barotrauma public void Update(bool createNetworkEvents = true) { - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + if (GameMain.NetworkMember is { IsClient: true }) { return; } while (spawnQueue.Count > 0) { var entitySpawnInfo = spawnQueue.Dequeue(); var spawnedEntity = entitySpawnInfo.Spawn(); - if (spawnedEntity != null) - { - if (createNetworkEvents) - { - CreateNetworkEventProjSpecific(spawnedEntity, false); - } - entitySpawnInfo.OnSpawned(spawnedEntity); + if (spawnedEntity == null) { continue; } + + if (createNetworkEvents) + { + CreateNetworkEventProjSpecific(new SpawnEntity(spawnedEntity)); } + entitySpawnInfo.OnSpawned(spawnedEntity); } while (removeQueue.Count > 0) @@ -422,13 +431,13 @@ namespace Barotrauma } if (createNetworkEvents) { - CreateNetworkEventProjSpecific(removedEntity, true); + CreateNetworkEventProjSpecific(new RemoveEntity(removedEntity)); } removedEntity.Remove(); } } - partial void CreateNetworkEventProjSpecific(Entity entity, bool remove); + partial void CreateNetworkEventProjSpecific(SpawnOrRemove spawnOrRemove); public void Reset() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializable.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializable.cs index 9313fec02..1547b1b5a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializable.cs @@ -3,28 +3,41 @@ interface INetSerializable { } /// - /// Interface for entities that the clients can send information of to the server + /// Interface for entities that the clients can send events to the server /// interface IClientSerializable : INetSerializable { #if CLIENT - void ClientWrite(IWriteMessage msg, object[] extraData = null); + void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null); #endif #if SERVER - void ServerRead(ClientNetObject type, IReadMessage msg, Client c); + void ServerEventRead(IReadMessage msg, Client c); #endif } /// - /// Interface for entities that the server can send information of to the clients + /// Interface for entities that the server can send events to the clients /// interface IServerSerializable : INetSerializable { #if SERVER - void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null); + void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null); #endif #if CLIENT - void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime); + void ClientEventRead(IReadMessage msg, float sendingTime); +#endif + } + + /// + /// Interface for entities that handle ServerNetObject.ENTITY_POSITION + /// + interface IServerPositionSync : IServerSerializable + { +#if SERVER + void ServerWritePosition(IWriteMessage msg, Client c); +#endif +#if CLIENT + void ClientReadPosition(IReadMessage msg, float sendingTime); #endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs index a1631780f..950a58719 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; using Barotrauma.Networking; using Microsoft.Xna.Framework; @@ -42,6 +44,13 @@ namespace Barotrauma public int NumberOfBits = 8; public bool IncludeColorAlpha = false; public int ArrayMaxSize = ushort.MaxValue; + + public readonly int OrderKey; + + public NetworkSerialize([CallerLineNumber] int lineNumber = 0) + { + OrderKey = lineNumber; + } } /// @@ -52,6 +61,7 @@ namespace Barotrauma public readonly struct ReadWriteBehavior { public delegate dynamic? ReadDelegate(IReadMessage inc, Type type, NetworkSerialize attribute); + public delegate void WriteDelegate(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg); public readonly ReadDelegate ReadAction; @@ -64,6 +74,58 @@ namespace Barotrauma } } + public readonly struct CachedReflectedVariable + { + public delegate object? GetValueDelegate(object? obj); + + public delegate void SetValueDelegate(object? obj, object? value); + + public readonly Type Type; + public readonly ReadWriteBehavior Behavior; + public readonly NetworkSerialize Attribute; + public readonly SetValueDelegate SetValue; + public readonly GetValueDelegate GetValue; + public readonly bool HasOwnAttribute; + + public CachedReflectedVariable(MemberInfo info, ReadWriteBehavior behavior, Type baseClassType) + { + Behavior = behavior; + + switch (info) + { + case PropertyInfo pi: + Type = pi.PropertyType; + GetValue = pi.GetValue; + SetValue = pi.SetValue; + break; + case FieldInfo fi: + Type = fi.FieldType; + GetValue = fi.GetValue; + SetValue = fi.SetValue; + break; + default: + throw new ArgumentException($"Expected {nameof(FieldInfo)} or {nameof(PropertyInfo)} but found {info.GetType()}.", nameof(info)); + } + + if (info.GetCustomAttribute() is { } ownAttriute) + { + HasOwnAttribute = true; + Attribute = ownAttriute; + } + else if (baseClassType.GetCustomAttribute() is { } globalAttribute) + { + HasOwnAttribute = false; + Attribute = globalAttribute; + } + else + { + throw new InvalidOperationException($"Unable to serialize \"{Type}\" in \"{baseClassType}\" because it has no {nameof(NetworkSerialize)} attribute."); + } + } + } + + private static readonly Dictionary> CachedVariables = new Dictionary>(); + private static readonly ImmutableDictionary TypeBehaviors = new Dictionary { { typeof(Boolean), new ReadWriteBehavior(ReadBoolean, WriteDynamic) }, @@ -82,8 +144,6 @@ namespace Barotrauma { typeof(Vector2), new ReadWriteBehavior(ReadVector2, WriteVector2) } }.ToImmutableDictionary(); - private static readonly ReadWriteBehavior InvalidReadWriteBehavior = new ReadWriteBehavior(ReadInvalid, WriteInvalid); - private static readonly ImmutableDictionary, ReadWriteBehavior> TypePredicates = new Dictionary, ReadWriteBehavior> { // Arrays @@ -99,15 +159,18 @@ namespace Barotrauma { type => Nullable.GetUnderlyingType(type) != null, new ReadWriteBehavior(ReadNullable, WriteNullable) }, // Option - { type => type.GetGenericTypeDefinition() == typeof(Option<>), new ReadWriteBehavior(ReadOption, WriteOption) } + { type => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Option<>), new ReadWriteBehavior(ReadOption, WriteOption) } }.ToImmutableDictionary(); + private static readonly ReadWriteBehavior InvalidReadWriteBehavior = new ReadWriteBehavior(ReadInvalid, WriteInvalid); + private static readonly Dictionary cachedSomeCreateMethods = new Dictionary(); private static readonly Dictionary cachedNoneCreateMethod = new Dictionary(); - private static void WriteInvalid(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) => throw new InvalidOperationException($"Type {obj?.GetType()} cannot be serialized. Did you forget to implement INetSerializableStruct?"); + private static void WriteInvalid(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) => + throw new SerializationException($"Type {obj?.GetType()} cannot be serialized. Did you forget to implement {nameof(INetSerializableStruct)}?"); - private static dynamic ReadInvalid(IReadMessage inc, Type type, NetworkSerialize attribute) => throw new InvalidOperationException($"Type {type} cannot be deserialized. Did you forget to implement INetSerializableStruct?"); + private static dynamic ReadInvalid(IReadMessage inc, Type type, NetworkSerialize attribute) => throw new SerializationException($"Type {type} cannot be deserialized. Did you forget to implement {nameof(INetSerializableStruct)}?"); private static void WriteOption(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) { @@ -131,7 +194,7 @@ namespace Barotrauma } else { - throw new InvalidOperationException("Option type was neither None<> or Some<>"); + throw new ArgumentOutOfRangeException(nameof(obj), "Option type was neither None or Some"); } } @@ -147,7 +210,7 @@ namespace Barotrauma if (TryFindBehavior(underlyingType, out ReadWriteBehavior behavior)) { dynamic? value = behavior.ReadAction(inc, underlyingType, attribute); - return GetCreateMethod(typeof(Some<>), underlyingType, cachedSomeCreateMethods).Invoke(null, new []{ value }); + return GetCreateMethod(typeof(Some<>), underlyingType, cachedSomeCreateMethods).Invoke(null, new[] { value }); } throw new InvalidOperationException($"Could not find suitable behavior for type {underlyingType} in {nameof(ReadOption)}"); @@ -349,7 +412,7 @@ namespace Barotrauma private static dynamic ReadDouble(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadDouble(); private static dynamic ReadString(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadString(); - + private static dynamic ReadIdentifier(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadIdentifier(); private static dynamic ReadColor(IReadMessage inc, Type type, NetworkSerialize attribute) => attribute.IncludeColorAlpha ? inc.ReadColorR8G8B8A8() : inc.ReadColorR8G8B8(); @@ -411,7 +474,7 @@ namespace Barotrauma return new Range(values.Min(), values.Max()); } - public static bool TryFindBehavior(Type type, out ReadWriteBehavior behavior) + private static bool TryFindBehavior(Type type, out ReadWriteBehavior behavior) { if (TypeBehaviors.TryGetValue(type, out behavior)) { return true; } @@ -427,6 +490,46 @@ namespace Barotrauma behavior = InvalidReadWriteBehavior; return false; } + + public static ImmutableArray GetPropertiesAndFields(Type type, Type baseClassType) + { + if (CachedVariables.TryGetValue(type, out var cached)) { return cached; } + + List variables = new List(); + + IEnumerable propertyInfos = type.GetProperties().Where(HasAttribute); + IEnumerable fieldInfos = type.GetFields().Where(HasAttribute); + + foreach (PropertyInfo info in propertyInfos) + { + if (TryFindBehavior(info.PropertyType, out ReadWriteBehavior behavior)) + { + variables.Add(new CachedReflectedVariable(info, behavior, baseClassType)); + } + else + { + throw new SerializationException($"Unable to serialize type \"{type}\"."); + } + } + + foreach (FieldInfo info in fieldInfos) + { + if (TryFindBehavior(info.FieldType, out ReadWriteBehavior behavior)) + { + variables.Add(new CachedReflectedVariable(info, behavior, baseClassType)); + } + else + { + throw new SerializationException($"Unable to serialize type \"{type}\"."); + } + } + + ImmutableArray array = variables.All(v => v.HasOwnAttribute) ? variables.OrderBy(v => v.Attribute.OrderKey).ToImmutableArray() : variables.ToImmutableArray(); + CachedVariables.Add(type, array); + return array; + + bool HasAttribute(MemberInfo info) => (info.GetCustomAttribute() ?? baseClassType.GetCustomAttribute()) != null; + } } /// @@ -512,38 +615,11 @@ namespace Barotrauma object? newObject = Activator.CreateInstance(type); if (newObject is null) { return default!; } - PropertyInfo[] properties = type.GetProperties(); - foreach (PropertyInfo info in properties) + var properties = NetSerializableProperties.GetPropertiesAndFields(type, type); + foreach (NetSerializableProperties.CachedReflectedVariable property in properties) { - NetworkSerialize? attribute = GetAttribute(info, newObject); - if (attribute is null) { continue; } - - if (NetSerializableProperties.TryFindBehavior(info.PropertyType, out var behavior)) - { - object? value = behavior.ReadAction(inc, info.PropertyType, attribute); - info.SetValue(newObject, value); - } - else - { - DebugConsole.ThrowError($"Unsupported property type \"{info.PropertyType}\" in {newObject}!"); - } - } - - FieldInfo[] fields = type.GetFields(); - foreach (FieldInfo info in fields) - { - NetworkSerialize? attribute = GetAttribute(info, newObject); - if (attribute is null) { continue; } - - if (NetSerializableProperties.TryFindBehavior(info.FieldType, out var behavior)) - { - object? value = behavior.ReadAction(inc, info.FieldType, attribute); - info.SetValue(newObject, value); - } - else - { - DebugConsole.ThrowError($"Unsupported field type \"{info.FieldType}\" in {newObject}!"); - } + NetworkSerialize attribute = property.Attribute; + property.SetValue(newObject, property.Behavior.ReadAction(inc, property.Type, attribute)); } return newObject; @@ -575,39 +651,34 @@ namespace Barotrauma /// Outgoing network message public void Write(IWriteMessage msg) { - PropertyInfo[] properties = GetType().GetProperties(); - foreach (PropertyInfo info in properties) + Type type = GetType(); + var properties = NetSerializableProperties.GetPropertiesAndFields(type, type); + foreach (NetSerializableProperties.CachedReflectedVariable property in properties) { - NetworkSerialize? attribute = GetAttribute(info, this); - if (attribute is null) { continue; } - - if (NetSerializableProperties.TryFindBehavior(info.PropertyType, out var behavior)) - { - behavior.WriteAction(info.GetValue(this), attribute, msg); - } - else - { - throw new InvalidOperationException($"Unsupported property type \"{info.PropertyType}\" in {this}"); - } - } - - FieldInfo[] fields = GetType().GetFields(); - foreach (FieldInfo info in fields) - { - NetworkSerialize? attribute = GetAttribute(info, this); - if (attribute is null) { continue; } - - if (NetSerializableProperties.TryFindBehavior(info.FieldType, out var behavior)) - { - behavior.WriteAction(info.GetValue(this), attribute, msg); - } - else - { - throw new InvalidOperationException($"Unsupported field type \"{info.FieldType}\" in {this}"); - } + NetworkSerialize attribute = property.Attribute; + property.Behavior.WriteAction(property.GetValue(this), attribute, msg); } } + } - private static NetworkSerialize? GetAttribute(MemberInfo info, object baseClass) => info.GetCustomAttribute() ?? baseClass.GetType().GetCustomAttribute(); + public static class WriteOnlyMessageExtensions + { +#if CLIENT + public static IWriteMessage WithHeader(this IWriteMessage msg, ClientPacketHeader header) + { + msg.Write((byte)header); + return msg; + } +#elif SERVER + public static IWriteMessage WithHeader(this IWriteMessage msg, ServerPacketHeader header) + { + msg.Write((byte)header); + return msg; + } +#endif + public static void Write(this IWriteMessage msg, INetSerializableStruct serializableStruct) + { + serializableStruct.Write(msg); + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs index 8396c48a3..9ec8f57fc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs @@ -4,47 +4,16 @@ namespace Barotrauma.Networking { abstract class NetEntityEvent { - public enum Type - { - Invalid, - ComponentState, - InventoryState, - Status, - Treatment, - ApplyStatusEffect, - ChangeProperty, - Control, - UpdateSkills, - Combine, - SetAttackTarget, - ExecuteAttack, - Upgrade, - AssignCampaignInteraction, - TeamChange, - ObjectiveManagerState, - AddToCrew, - UpdateExperience, - UpdateTalents, - UpdateMoney, - UpdatePermanentStats, - } + public interface IData { } public readonly Entity Entity; public readonly UInt16 ID; - public UInt16 EntityID - { - get; - private set; - } + public UInt16 EntityID => Entity.ID; //arbitrary extra data that will be passed to the Write method of the serializable entity //(the index of an itemcomponent for example) - public object[] Data - { - get; - private set; - } + public IData Data { get; private set; } public bool Sent; @@ -52,43 +21,18 @@ namespace Barotrauma.Networking { this.ID = id; this.Entity = serializableEntity as Entity; - RefreshEntityID(); } - public void RefreshEntityID() - { - this.EntityID = this.Entity is Entity entity ? entity.ID : Entity.NullEntityID; - } - - public void SetData(object[] data) + public void SetData(IData data) { this.Data = data; } public bool IsDuplicate(NetEntityEvent other) { - if (other.Entity != this.Entity) return false; + if (other.Entity != this.Entity) { return false; } - if (Data != null && other.Data != null) - { - if (Data.Length != other.Data.Length) return false; - - for (int i = 0; i < Data.Length; i++) - { - if (Data[i] == null) - { - if (other.Data[i] != null) return false; - } - else - { - if (other.Data[i] == null) return false; - if (!Data[i].Equals(other.Data[i])) return false; - } - } - return true; - } - - return Data == other.Data; + return Equals(Data, other.Data); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs index f2bb95b69..0b154b322 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs @@ -38,7 +38,6 @@ namespace Barotrauma.Networking //(otherwise the clients might read the next event in the message and think its ID //is consecutive to the previous one, even though we skipped over this broken event) tempBuffer.Write(Entity.NullEntityID); - tempBuffer.WritePadBits(); eventCount++; continue; } @@ -53,7 +52,6 @@ namespace Barotrauma.Networking tempBuffer.Write(e.EntityID); tempBuffer.WriteVariableUInt32((uint)tempEventBuffer.LengthBytes); tempBuffer.Write(tempEventBuffer.Buffer, 0, tempEventBuffer.LengthBytes); - tempBuffer.WritePadBits(); sentEvents.Add(e); eventCount++; @@ -61,6 +59,7 @@ namespace Barotrauma.Networking if (eventCount > 0) { + msg.WritePadBits(); msg.Write(eventsToSync[0].ID); msg.Write((byte)eventCount); msg.Write(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index dd3b80645..90f123c95 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -6,7 +6,7 @@ using System.Linq; namespace Barotrauma.Networking { - enum ClientPacketHeader + public enum ClientPacketHeader { UPDATE_LOBBY, //update state in lobby UPDATE_INGAME, //update state ingame @@ -31,9 +31,10 @@ namespace Barotrauma.Networking ERROR, //tell the server that an error occurred CREW, //hiring UI MEDICAL, //medical clinic + MONEY, //wallet updates + REWARD_DISTRIBUTION, // wallet reward distribution READY_CHECK, READY_TO_SPAWN - } enum ClientNetObject { @@ -52,7 +53,7 @@ namespace Barotrauma.Networking MISSING_ENTITY //client can't find an entity of a certain ID } - enum ServerPacketHeader + public enum ServerPacketHeader { AUTH_RESPONSE, //tell the player if they require a password to log in AUTH_FAILURE, //the server won't authorize player yet, however connection is still alive @@ -82,6 +83,7 @@ namespace Barotrauma.Networking EVENTACTION, CREW, //anything related to managing bots in multiplayer MEDICAL, //medical clinic + MONEY, READY_CHECK //start, end and update a ready check } enum ServerNetObject @@ -169,7 +171,7 @@ namespace Barotrauma.Networking get { return false; } } - public abstract void CreateEntityEvent(INetSerializable entity, object[] extraData = null); + public abstract void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData = null); #if DEBUG public Dictionary messageCount = new Dictionary(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs index b39783e44..00711ed04 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs @@ -654,7 +654,7 @@ namespace Barotrauma.Networking { get { - return lengthBits / 8; + return (LengthBits + 7) / 8; } } @@ -867,7 +867,7 @@ namespace Barotrauma.Networking { get { - return (LengthBits + ((8 - (LengthBits % 8)) % 8)) / 8; + return (LengthBits + 7) / 8; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/PerformanceCounter.cs b/Barotrauma/BarotraumaShared/SharedSource/PerformanceCounter.cs index 6915d1dfc..bf0a96dd4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/PerformanceCounter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/PerformanceCounter.cs @@ -6,6 +6,8 @@ namespace Barotrauma { public class PerformanceCounter { + private readonly object mutex = new object(); + public double AverageFramesPerSecond { get; private set; } public double CurrentFramesPerSecond { get; private set; } @@ -13,34 +15,106 @@ namespace Barotrauma private readonly Queue sampleBuffer = new Queue(); + public class TickInfo + { + public Queue ElapsedTicks { get; set; } = new Queue(); + public long AvgTicksPerFrame { get; set; } + } + private readonly Dictionary> elapsedTicks = new Dictionary>(); private readonly Dictionary avgTicksPerFrame = new Dictionary(); + private readonly Dictionary> partialTickInfos = new Dictionary>(); #if CLIENT internal Graph UpdateTimeGraph = new Graph(500), DrawTimeGraph = new Graph(500); #endif - public IEnumerable GetSavedIdentifiers + private readonly List tempSavedIdentifiers = new List(); + public IReadOnlyList GetSavedIdentifiers { - get { return avgTicksPerFrame.Keys; } + get + { + lock (mutex) + { + tempSavedIdentifiers.Clear(); + tempSavedIdentifiers.AddRange(avgTicksPerFrame.Keys); + } + return tempSavedIdentifiers; + } + } + + private readonly List tempSavedPartialIdentifiers = new List(); + public IReadOnlyList GetSavedPartialIdentifiers(string parentIdentifier) + { + lock (mutex) + { + tempSavedPartialIdentifiers.Clear(); + if (partialTickInfos.TryGetValue(parentIdentifier, out var tickInfos)) + { + tempSavedPartialIdentifiers.AddRange(tickInfos.Keys); + } + } + return tempSavedPartialIdentifiers; } public void AddElapsedTicks(string identifier, long ticks) { - if (!elapsedTicks.ContainsKey(identifier)) elapsedTicks.Add(identifier, new Queue()); - elapsedTicks[identifier].Enqueue(ticks); - - if (elapsedTicks[identifier].Count > MaximumSamples) + lock (mutex) { - elapsedTicks[identifier].Dequeue(); - avgTicksPerFrame[identifier] = (long)elapsedTicks[identifier].Average(i => i); + if (!elapsedTicks.ContainsKey(identifier)) { elapsedTicks.Add(identifier, new Queue()); } + elapsedTicks[identifier].Enqueue(ticks); + + if (elapsedTicks[identifier].Count > MaximumSamples) + { + elapsedTicks[identifier].Dequeue(); + avgTicksPerFrame[identifier] = (long)elapsedTicks[identifier].Average(i => i); + } + } + } + + public void AddPartialElapsedTicks(string parentIdentifier, string identifier, long ticks) + { + lock (mutex) + { + if (!partialTickInfos.TryGetValue(parentIdentifier, out var tickInfos)) + { + tickInfos = new Dictionary(); + partialTickInfos.Add(parentIdentifier, tickInfos); + } + if (!tickInfos.TryGetValue(identifier, out var tickInfo)) + { + tickInfo = new TickInfo(); + tickInfos.Add(identifier, tickInfo); + } + tickInfo.ElapsedTicks.Enqueue(ticks); + if (tickInfo.ElapsedTicks.Count > MaximumSamples) + { + tickInfo.ElapsedTicks.Dequeue(); + tickInfo.AvgTicksPerFrame = (long)tickInfo.ElapsedTicks.Average(i => i); + } } } public float GetAverageElapsedMillisecs(string identifier) { - if (!avgTicksPerFrame.ContainsKey(identifier)) return 0.0f; - return avgTicksPerFrame[identifier] * 1000.0f / (float)Stopwatch.Frequency; + long ticksPerFrame = 0; + lock (mutex) + { + avgTicksPerFrame.TryGetValue(identifier, out ticksPerFrame); + } + return ticksPerFrame * 1000.0f / Stopwatch.Frequency; + } + + public float GetPartialAverageElapsedMillisecs(string parentIdentifier, string identifier) + { + long ticksPerFrame = 0; + lock (mutex) + { + if (!partialTickInfos.TryGetValue(parentIdentifier, out var tickInfos)) { return 0.0f; } + if (!tickInfos.TryGetValue(identifier, out var tickInfo)) { return 0.0f; } + ticksPerFrame = tickInfo.AvgTicksPerFrame; + } + return ticksPerFrame * 1000.0f / Stopwatch.Frequency; } public bool Update(double deltaTime) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs index 9b4584325..84690b56b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs @@ -133,10 +133,17 @@ namespace Barotrauma e.IsHighlighted = false; } - if (GameMain.GameSession != null) GameMain.GameSession.Update((float)deltaTime); #if CLIENT var sw = new System.Diagnostics.Stopwatch(); sw.Start(); +#endif + + GameMain.GameSession?.Update((float)deltaTime); + +#if CLIENT + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("GameSessionUpdate", sw.ElapsedTicks); + sw.Restart(); GameMain.ParticleManager.Update((float)deltaTime); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index 4809ed0ca..765a62374 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs @@ -100,7 +100,7 @@ namespace Barotrauma } case ConditionType.Attachable: { - return entity is Holdable holdable && holdable.Attachable; + return entity is Holdable holdable && holdable.Attachable && Screen.Selected == GameMain.SubEditorScreen; } } return false; @@ -949,8 +949,7 @@ namespace Barotrauma break; } } - - element.Attribute(property.Name)?.Remove(); + element.GetAttribute(property.Name)?.Remove(); element.SetAttributeValue(property.Name, stringValue); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index 43b51b551..dacc2c8a7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -143,8 +143,9 @@ namespace Barotrauma public static string GetAttributeString(this XElement element, string name, string defaultValue) { - if (element?.GetAttribute(name) == null) { return defaultValue; } - string str = GetAttributeString(element.GetAttribute(name), defaultValue); + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } + string str = GetAttributeString(attribute, defaultValue); #if DEBUG if (!str.IsNullOrEmpty() && (str.Contains("%ModDir", StringComparison.OrdinalIgnoreCase) @@ -160,8 +161,9 @@ namespace Barotrauma public static string GetAttributeStringUnrestricted(this XElement element, string name, string defaultValue) { #warning TODO: remove? - if (element?.GetAttribute(name) == null) { return defaultValue; } - return GetAttributeString(element.GetAttribute(name), defaultValue); + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } + return GetAttributeString(attribute, defaultValue); } public static bool DoesAttributeReferenceFileNameAlone(this XElement element, string name) @@ -190,14 +192,15 @@ namespace Barotrauma private static string GetAttributeString(XAttribute attribute, string defaultValue) { string value = attribute.Value; - return String.IsNullOrEmpty(value) ? defaultValue : value; + return string.IsNullOrEmpty(value) ? defaultValue : value; } public static string[] GetAttributeStringArray(this XElement element, string name, string[] defaultValue, bool trim = true, bool convertToLowerInvariant = false) { - if (element?.GetAttribute(name) == null) { return defaultValue; } + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } - string stringValue = element.GetAttribute(name).Value; + string stringValue = attribute.Value; if (string.IsNullOrEmpty(stringValue)) { return defaultValue; } string[] splitValue = stringValue.Split(',', ','); @@ -224,50 +227,15 @@ namespace Barotrauma foreach (string name in matchingAttributeName) { - if (element.GetAttribute(name) == null) { continue; } - - float val; - try - { - string strVal = element.GetAttribute(name).Value; - if (strVal.LastOrDefault() == 'f') - { - strVal = strVal.Substring(0, strVal.Length - 1); - } - val = float.Parse(strVal, CultureInfo.InvariantCulture); - } - catch (Exception e) - { - DebugConsole.ThrowError("Error in " + element + "!", e); - continue; - } - return val; + var attribute = element.GetAttribute(name); + if (attribute == null) { continue; } + return GetAttributeFloat(attribute, defaultValue); } return defaultValue; } - public static float GetAttributeFloat(this XElement element, string name, float defaultValue) - { - if (element?.GetAttribute(name) == null) { return defaultValue; } - - float val = defaultValue; - try - { - string strVal = element.GetAttribute(name).Value; - if (strVal.LastOrDefault() == 'f') - { - strVal = strVal.Substring(0, strVal.Length - 1); - } - val = float.Parse(strVal, CultureInfo.InvariantCulture); - } - catch (Exception e) - { - DebugConsole.ThrowError("Error in " + element + "!", e); - } - - return val; - } + public static float GetAttributeFloat(this XElement element, string name, float defaultValue) => GetAttributeFloat(element?.GetAttribute(name), defaultValue); public static float GetAttributeFloat(this XAttribute attribute, float defaultValue) { @@ -292,14 +260,16 @@ namespace Barotrauma return val; } - public static double GetAttributeDouble(this XElement element, string name, double defaultValue) + public static double GetAttributeDouble(this XElement element, string name, double defaultValue) => GetAttributeDouble(element?.GetAttribute(name), defaultValue); + + public static double GetAttributeDouble(this XAttribute attribute, double defaultValue) { - if (element?.Attribute(name) == null) { return defaultValue; } + if (attribute == null) { return defaultValue; } double val = defaultValue; try { - string strVal = element.Attribute(name).Value; + string strVal = attribute.Value; if (strVal.LastOrDefault() == 'f') { strVal = strVal.Substring(0, strVal.Length - 1); @@ -308,17 +278,19 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError("Error in " + element + "!", e); + DebugConsole.ThrowError("Error in " + attribute + "!", e); } return val; } + public static float[] GetAttributeFloatArray(this XElement element, string name, float[] defaultValue) { - if (element?.GetAttribute(name) == null) { return defaultValue; } + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } - string stringValue = element.GetAttribute(name).Value; + string stringValue = attribute.Value; if (string.IsNullOrEmpty(stringValue)) { return defaultValue; } string[] splitValue = stringValue.Split(','); @@ -345,13 +317,14 @@ namespace Barotrauma public static int GetAttributeInt(this XElement element, string name, int defaultValue) { - if (element?.GetAttribute(name) == null) { return defaultValue; } + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } int val = defaultValue; try { - if (!Int32.TryParse(element.GetAttribute(name).Value, NumberStyles.Any, CultureInfo.InvariantCulture, out val)) + if (!Int32.TryParse(attribute.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out val)) { val = (int)float.Parse(element.GetAttribute(name).Value, CultureInfo.InvariantCulture); } @@ -366,13 +339,14 @@ namespace Barotrauma public static uint GetAttributeUInt(this XElement element, string name, uint defaultValue) { - if (element?.GetAttribute(name) == null) { return defaultValue; } + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } uint val = defaultValue; try { - val = UInt32.Parse(element.GetAttribute(name).Value); + val = UInt32.Parse(attribute.Value); } catch (Exception e) { @@ -384,13 +358,14 @@ namespace Barotrauma public static UInt64 GetAttributeUInt64(this XElement element, string name, UInt64 defaultValue) { - if (element?.GetAttribute(name) == null) { return defaultValue; } + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } UInt64 val = defaultValue; try { - val = UInt64.Parse(element.GetAttribute(name).Value, NumberStyles.Any, CultureInfo.InvariantCulture); + val = UInt64.Parse(attribute.Value, NumberStyles.Any, CultureInfo.InvariantCulture); } catch (Exception e) { @@ -402,13 +377,14 @@ namespace Barotrauma public static Version GetAttributeVersion(this XElement element, string name, Version defaultValue) { - if (element?.GetAttribute(name) == null) return defaultValue; + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } Version val = defaultValue; try { - val = Version.Parse(element.GetAttribute(name).Value); + val = Version.Parse(attribute.Value); } catch (Exception e) { @@ -420,13 +396,14 @@ namespace Barotrauma public static UInt64 GetAttributeSteamID(this XElement element, string name, UInt64 defaultValue) { - if (element?.GetAttribute(name) == null) { return defaultValue; } + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } UInt64 val = defaultValue; try { - val = Steam.SteamManager.SteamIDStringToUInt64(element.GetAttribute(name).Value); + val = Steam.SteamManager.SteamIDStringToUInt64(attribute.Value); } catch (Exception e) { @@ -438,9 +415,10 @@ namespace Barotrauma public static int[] GetAttributeIntArray(this XElement element, string name, int[] defaultValue) { - if (element?.GetAttribute(name) == null) { return defaultValue; } + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } - string stringValue = element.GetAttribute(name).Value; + string stringValue = attribute.Value; if (string.IsNullOrEmpty(stringValue)) { return defaultValue; } string[] splitValue = stringValue.Split(','); @@ -462,9 +440,10 @@ namespace Barotrauma } public static ushort[] GetAttributeUshortArray(this XElement element, string name, ushort[] defaultValue) { - if (element?.GetAttribute(name) == null) { return defaultValue; } + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } - string stringValue = element.GetAttribute(name).Value; + string stringValue = attribute.Value; if (string.IsNullOrEmpty(stringValue)) { return defaultValue; } string[] splitValue = stringValue.Split(','); @@ -496,8 +475,9 @@ namespace Barotrauma public static bool GetAttributeBool(this XElement element, string name, bool defaultValue) { - if (element?.GetAttribute(name) == null) { return defaultValue; } - return element.GetAttribute(name).GetAttributeBool(defaultValue); + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } + return attribute.GetAttributeBool(defaultValue); } public static bool GetAttributeBool(this XAttribute attribute, bool defaultValue) @@ -520,45 +500,52 @@ namespace Barotrauma public static Point GetAttributePoint(this XElement element, string name, Point defaultValue) { - if (element?.GetAttribute(name) == null) { return defaultValue; } - return ParsePoint(element.GetAttribute(name).Value); + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } + return ParsePoint(attribute.Value); } public static Vector2 GetAttributeVector2(this XElement element, string name, Vector2 defaultValue) { - if (element?.GetAttribute(name) == null) { return defaultValue; } - return ParseVector2(element.GetAttribute(name).Value); + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } + return ParseVector2(attribute.Value); } public static Vector3 GetAttributeVector3(this XElement element, string name, Vector3 defaultValue) { - if (element == null || element.GetAttribute(name) == null) { return defaultValue; } - return ParseVector3(element.GetAttribute(name).Value); + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } + return ParseVector3(attribute.Value); } public static Vector4 GetAttributeVector4(this XElement element, string name, Vector4 defaultValue) { - if (element == null || element.GetAttribute(name) == null) { return defaultValue; } - return ParseVector4(element.GetAttribute(name).Value); + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } + return ParseVector4(attribute.Value); } public static Color GetAttributeColor(this XElement element, string name, Color defaultValue) { - if (element == null || element.GetAttribute(name) == null) { return defaultValue; } - return ParseColor(element.GetAttribute(name).Value); + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } + return ParseColor(attribute.Value); } public static Color? GetAttributeColor(this XElement element, string name) { - if (element == null || element.GetAttribute(name) == null) { return null; } - return ParseColor(element.GetAttribute(name).Value); + var attribute = element?.GetAttribute(name); + if (attribute == null) { return null; } + return ParseColor(attribute.Value); } public static Color[] GetAttributeColorArray(this XElement element, string name, Color[] defaultValue) { - if (element?.GetAttribute(name) == null) { return defaultValue; } + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } - string stringValue = element.GetAttribute(name).Value; + string stringValue = attribute.Value; if (string.IsNullOrEmpty(stringValue)) { return defaultValue; } string[] splitValue = stringValue.Split(';'); @@ -602,8 +589,9 @@ namespace Barotrauma public static Rectangle GetAttributeRect(this XElement element, string name, Rectangle defaultValue) { - if (element == null || element.GetAttribute(name) == null) { return defaultValue; } - return ParseRect(element.GetAttribute(name).Value, false); + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } + return ParseRect(attribute.Value, false); } //TODO: nested tuples and and n-uples where n!=2 are unsupported @@ -617,9 +605,10 @@ namespace Barotrauma public static (T1, T2)[] GetAttributeTupleArray(this XElement element, string name, (T1, T2)[] defaultValue) { - if (element?.Attribute(name) == null) { return defaultValue; } + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } - string stringValue = element.Attribute(name).Value; + string stringValue = attribute.Value; if (string.IsNullOrEmpty(stringValue)) { return defaultValue; } return stringValue.Split(';').Select(s => ParseTuple(s, default)).ToArray(); @@ -918,6 +907,8 @@ namespace Barotrauma public static XAttribute GetAttribute(this XElement element, string name, StringComparison comparisonMethod = StringComparison.OrdinalIgnoreCase) => element.GetAttribute(a => a.Name.ToString().Equals(name, comparisonMethod)); + public static void SetAttributeValue(this XElement element, string name, object value, StringComparison comparisonMethod = StringComparison.OrdinalIgnoreCase) => GetAttribute(element, name, comparisonMethod)?.SetValue(value); + public static XAttribute GetAttribute(this XElement element, Identifier name) => element.GetAttribute(name.Value, StringComparison.OrdinalIgnoreCase); public static XAttribute GetAttribute(this XElement element, Func predicate) => element.Attributes().FirstOrDefault(predicate); diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index bc1da8b5c..688ebbfbb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; +using Barotrauma.Networking; namespace Barotrauma { @@ -1227,7 +1228,15 @@ namespace Barotrauma { for (int i = 0; i < targets.Count; i++) { - if (targets[i] is Character character) { Entity.Spawner?.AddEntityToRemoveQueue(character); } + var target = targets[i]; + if (target is Character character) + { + Entity.Spawner?.AddEntityToRemoveQueue(character); + } + else if (target is Limb limb) + { + Entity.Spawner?.AddEntityToRemoveQueue(limb.character); + } } } if (breakLimb || hideLimb) @@ -1848,11 +1857,11 @@ namespace Barotrauma float prevVitality = targetCharacter.Vitality; if (targetLimb != null) { - targetCharacter.CharacterHealth.ReduceAfflictionOnLimb(targetLimb, affliction, reduceAmount * deltaTime, treatmentAction: actionType); + targetCharacter.CharacterHealth.ReduceAfflictionOnLimb(targetLimb, affliction, reduceAmount, treatmentAction: actionType); } else { - targetCharacter.CharacterHealth.ReduceAfflictionOnAllLimbs(affliction, reduceAmount * deltaTime, treatmentAction: actionType); + targetCharacter.CharacterHealth.ReduceAfflictionOnAllLimbs(affliction, reduceAmount, treatmentAction: actionType); } if (element.User != null && element.User != targetCharacter) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs index 4a5731b4a..3963df1ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs @@ -30,8 +30,6 @@ namespace Barotrauma public readonly HashSet EnteredCrushDepth = new HashSet(); public readonly HashSet ReactorMeltdown = new HashSet(); - public readonly HashSet Casualties = new HashSet(); - public bool SubWasDamaged; } @@ -269,8 +267,6 @@ namespace Barotrauma } #endif - roundData?.Casualties.Add(character); - UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName}".ToIdentifier()); if (character.CurrentHull != null) { @@ -406,7 +402,7 @@ namespace Barotrauma //made it to the destination if (gameSession.Submarine.AtEndExit) { - bool noDamageRun = !roundData.SubWasDamaged && !roundData.Casualties.Any(c => !(c.AIController is EnemyAIController)); + bool noDamageRun = !roundData.SubWasDamaged && !gameSession.Casualties.Any(); #if SERVER if (GameMain.Server != null) @@ -434,8 +430,8 @@ namespace Barotrauma if (charactersInSub.Count == 1) { - //there must be some non-enemy casualties to get the last mant standing achievement - if (roundData.Casualties.Any(c => !(c.AIController is EnemyAIController) && c.TeamID == charactersInSub[0].TeamID)) + //there must be some casualties to get the last mant standing achievement + if (gameSession.Casualties.Any()) { UnlockAchievement(charactersInSub[0], "lastmanstanding".ToIdentifier()); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/RichString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/RichString.cs index 30e3678e7..ff86da1d4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/RichString.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/RichString.cs @@ -32,7 +32,7 @@ namespace Barotrauma #if CLIENT private readonly GUIFont? font; private readonly GUIComponentStyle? componentStyle; - private bool forceUpperCase = false; + private readonly bool forceUpperCase = false; private bool fontOrStyleForceUpperCase => font is { ForceUpperCase: true } || componentStyle is { ForceUpperCase: true }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/NamedEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/NamedEvent.cs new file mode 100644 index 000000000..7b46b9467 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/NamedEvent.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; + +namespace Barotrauma +{ + internal sealed class NamedEvent : IDisposable + { + private readonly Dictionary> events = new Dictionary>(); + + ~NamedEvent() + { + ReleaseUnmanagedResources(); + } + + public void Register(Identifier identifier, Action action) + { + if (HasEvent(identifier)) + { + throw new ArgumentException($"Event with the identifier \"{identifier}\" has already been registered.", nameof(identifier)); + } + + events.Add(identifier, action); + } + + public void RegisterOverwriteExisting(Identifier identifier, Action action) + { + if (HasEvent(identifier)) + { + Deregister(identifier); + } + + Register(identifier, action); + } + + public void Deregister(Identifier identifier) + { + events.Remove(identifier); + } + + public bool HasEvent(Identifier identifier) => events.ContainsKey(identifier); + + public void Invoke(T data) + { + foreach (var (_, action) in events) + { + action?.Invoke(data); + } + } + + private void ReleaseUnmanagedResources() + { + events.Clear(); + } + + public void Dispose() + { + ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs index 1bdc94160..c9152e8c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs @@ -1,3 +1,5 @@ +using System; + namespace Barotrauma { /// @@ -10,5 +12,15 @@ namespace Barotrauma { public static Option Some(T value) => Some.Create(value); public static Option None() => None.Create(); + public bool IsNone() => this is None; + public bool IsSome() => this is Some; + + public Option Select(Func selector) => + this switch + { + Some { Value: var value } => Option.Some(selector.Invoke(value)), + None _ => Option.None(), + _ => throw new ArgumentOutOfRangeException() + }; } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/RichTextData.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/RichTextData.cs index 47445df31..2522ce1ee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/RichTextData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/RichTextData.cs @@ -49,7 +49,7 @@ namespace Barotrauma string[] attributes = segments[i].Split(attributeSeparator); for (int j = 0; j < attributes.Length; j++) { - if (attributes[j].Contains(endDefinition)) + if (attributes[j].Contains(endDefinition, System.StringComparison.OrdinalIgnoreCase)) { if (tempData != null) { @@ -59,7 +59,7 @@ namespace Barotrauma } tempData = null; } - else if (attributes[j].StartsWith(colorDefinition)) + else if (attributes[j].StartsWith(colorDefinition, System.StringComparison.OrdinalIgnoreCase)) { if (tempData == null) { tempData = new RichTextData(); } string valueStr = attributes[j].Substring(attributes[j].IndexOf(keyValueSeparator) + 1); @@ -72,7 +72,7 @@ namespace Barotrauma tempData.Color = XMLExtensions.ParseColor(valueStr); } } - else if (attributes[j].StartsWith(metadataDefinition)) + else if (attributes[j].StartsWith(metadataDefinition, System.StringComparison.OrdinalIgnoreCase)) { if (tempData == null) { tempData = new RichTextData(); } tempData.Metadata = attributes[j].Substring(attributes[j].IndexOf(keyValueSeparator) + 1); diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index e863904a0..a24e889aa 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,87 @@ +--------------------------------------------------------------------------------------------------------- +v0.17.1.0 +--------------------------------------------------------------------------------------------------------- + +Changes: +- Added personal wallets. Everyone in the crew now has their personal wallet that they can use to purchase whatever they wish in outposts. The host (or people with campaign management permissions) can distribute a portion of the mission rewards to the crew or transfer money from the shared funds to the players. +- Made supercapacitors take some damage when they're being charged faster than 70%. +- Increased Orca 2's reactor output. +- Adjustments to mission distribution: only easy missions at the beginning of the campaign, moved some of the more difficult missions later into the campaign. +- Made Not Component's ContinuousOutput property editable in-game. +- Show warnings when saving a sub in the sub editor if any of the entity counts (walls, items, lights, etc) are very high, don't allow saving if the light counts are above the upper limits. +- Visual/layout mprovements to the Workshop menu. +- Added "power_value_out" and "load_value_out" connections to relays, batteries and supercapacitors. +- Increased SMG Magazine's capacity to 21 to make it divisible with the Deadeye Carbine's 3-bullet bursts (unstable only). +- Made molochs, abyss monsters and fractal guardians immune to poisons. +- Boosted the structure damage from the small crawler eggs from 150 to 200. +- Adjusted structure and item damage for coilgun ammunition: piercing 50% less damage, exploding 100% more damage (from explosions), physicorium 50% more damage. +- Don't populate the abyss in difficulty levels 0 to 10 in single mission mode. +- Revisited endworm: the armor now breaks less easily, reduced the change of cutting the worm towards the head, adjusted the bleeding speed. + +Fixes: +- Attempt to fix occasional performance drops in the store interface. +- Fixed clients seeing a blank server lobby if they join when a round is running with respawning disabled. +- Fixed arithmetic and trigonometric components not passing the sender of the signal forwards, preventing e.g. helm skill from boosting engines if the signal goes through a component. +- Fixed occasional crashes when transitioning between levels with showperf enabled in multiplayer campaign. +- Fixed wearables not affecting movement speed when godmode is on. +- Fixed disconnected players preventing talents that require everyone to survive from working (e.g. "Field Medic", "Bootcamp"). +- Fixed workshop menu's "popular mods" and "publish" tabs being usable when not connected to Steam (unstable only). +- Fixed text displays' depth being forced to 0.85, causing draw order issues in older subs (unstable only). +- Fixes periscopes not focusing on turrets if there's certain components (e.g. arithmetic components) between them. +- Fixed crashing when trying to use the "dumpeventtexts" command with no arguments or a disallowed path. +- Made smoke detectors a little more sensitive (should fix small fires sometimes not being detected even if they're in the same room). +- Fixed reactor sometimes not catching fire again if you start overheating it again immediately after a fire. +- Fixed broken batteries consuming power (unstable only). +- Fixed sub editor test mode spawning the crew in an inconsistent order, sometimes causing the player to start as someone else than the captain (unstable only). +- Fixed recycling a non-empty SMG magazine dropping the bullet inside it on the floor. +- Fixed "Trusted Captain" and "Esteemed Captain" giving medals even when no missions have been completed. +- Fixed small pump's ballast flora infection sprite (unstable only). +- Fixed text colors being broken in campaign map tooltips (unstable only). +- Fixed text colors not working in texts forced to upper case (e.g. reputation texts in the round summary). Unstable only. +- Fixed EntitySpawnerComponent's SpawnAreaOffset.Y being inverted. +- Fixed gaps generating incorrectly on "Shell A 70 Degrees". +- Fixed items powered by battery cells not working correctly (certain devices like handheld sonar beacons never powering up, and items staying powered indefinitely when you put in a battery and take it out). Unstable only. +- Fixed turret lights starting in an incorrect rotation in the sub editor. +- Fixed "commander" talent still not correctly giving the buff (giving orders that a character already had didn't move the buff). Unstable only. +- Fixed nav terminals sometimes determining which docking port the docking button controls incorrectly (specifically, when the correct docking port is also connected to other ports). +- Fixed campaign saves being considered incompatible if you install some new custom subs (unstable only). +- Fixed an exploit in the depleted fuel SMG magazine recipe. +- Fixed reactor gauges' background sticking out from the gauges when selecting a reactor in the editor. +- Fixed projectiles bouncing off subs without doing any damage (unstable only). +- Fixed crashing when clicking a turret in the sub upgrade menu's submarine preview (unstable only). +- Fixed character variants failing to load animations from the base character (unstable only). +- Fixed deltatime being applied twice to affliction reductions, making meds do practically nothing (unstable only). +- Fixed order of the command menu changing on each launch (unstable only). +- Fixed talent options switching places on each launch (unstable only). +- Fixed submarine upgrades and factions in the round summary switching places on each launch (unstable only). +- Fixed texture compression causing the preview image saving to fail when dragging and dropping the preview image into the save screen via file explorer (unstable only). +- Fixed disguises not working in multiplayer (unstable only). +- Fixed server host getting stuck in a lobby creation loop if their server crashes (unstable only). +- Fixed fractal guardians fleeing to the shelter immediatedly after taking some damage when they have targeted the guardian pod once and have not changed the target yet (e.g. if you shoot a guardian that is returning from the pod and if it has not yet spotted you). +- Fixed Crawler Broodmother regenerating really fast while eating. Broodmother no longer eats her own eggs. +- Fixed monsters not being able to target items (unstable only). +- Fixed spinelings accidentally killing each other with their spikes. +- Fixed "error loading a previously saved order" errors when transitioning between levels (unstable only). +- Fixed burn being ignored in the damagemodifier of Charybdis' head. The jaw worked correctly. Affects pulse laser damage for example. + +AI: +- Adjusted bot behavior around ballast flora: priority of some objectives now drops to 0 when the target's been claimed by ballast flora, items claimed by ballast flora aren't valid targets for some objectives anymore. +- Made bot healing dialog reflect if CPR was performed or not. +- Fixed security from the player's own crew attacking the player in multiplayer when the player attacks someone in an outpost. +- Fix bots not ignoring items marked to be "Hidden In Game". +- Bots prefer not to take diving suits off in rooms marged with the "IsWetRoom" flag. +- Docking ports are now automatically considered as "wet rooms". +- Fixed bots trying to target through doors and walls even though there's no line of sight between the end node and the target. +- Added some dialogue to bots when they get infected with the husk infection. + +Modding: +- Fixed clients not gaining control of the final stage of a husk affliction when "controlhusk" is enabled. +- When using a mod that doesn't set the InitialCount of any job, choose the first 3 jobs as the starting crew. Otherwise the crew customization menu will be empty and starting the campaign will lead to an immediate game over. +- Made it possible for attack StatusEffects to target the character doing the attack instead of the limb by using "Parent" as the target type. +- Using RemoveCharacter on a limb removes the character that limb belongs to. +- Fixed editing human character in the character editor sometimes making the inventory inaccessible. +- Fixed character editor crashing when trying to copy a character (unstable only). + --------------------------------------------------------------------------------------------------------- v0.17.0.0 ---------------------------------------------------------------------------------------------------------