diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs index 74548b53d..eacea79b3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs @@ -11,7 +11,7 @@ namespace Barotrauma { if (Character.IsUnconscious || !Character.Enabled || !Enabled) { return; } - Vector2 pos = Character.WorldPosition; + Vector2 pos = Character.DrawPosition; pos.Y = -pos.Y; if (State == AIState.Idle && PreviousState == AIState.Attack) @@ -31,7 +31,7 @@ namespace Barotrauma } else if (SelectedAiTarget?.Entity != null) { - Vector2 targetPos = SelectedAiTarget.WorldPosition; + Vector2 targetPos = SelectedAiTarget.Entity.DrawPosition; if (State == AIState.Attack) { targetPos = attackWorldPos; @@ -72,7 +72,7 @@ namespace Barotrauma } GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 80.0f, State.ToString(), stateColor, Color.Black); - if (LatchOntoAI != null) + if (LatchOntoAI != null && (State == AIState.Idle || LatchOntoAI.IsAttachedToSub)) { foreach (Joint attachJoint in LatchOntoAI.AttachJoints) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Objectives/AIObjective.cs index db9309bbe..0c21b2956 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Objectives/AIObjective.cs @@ -1,7 +1,11 @@ -namespace Barotrauma +using Microsoft.Xna.Framework; + +namespace Barotrauma { abstract partial class AIObjective { + public static Color ObjectiveIconColor => Color.LightGray; + public static Sprite GetSprite(string identifier, string option, Entity targetEntity) { if (string.IsNullOrEmpty(identifier)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index f1dd4470c..89ca9a2f7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -154,7 +154,7 @@ namespace Barotrauma public bool PlaySound; - public GUIMessage(string rawText, Color color, float delay, string identifier = null, int? value = null) + public GUIMessage(string rawText, Color color, float delay, string identifier = null, int? value = null, float lifeTime = 3.0f) { RawText = Text = rawText; if (value.HasValue) @@ -166,7 +166,7 @@ namespace Barotrauma Size = GUI.Font.MeasureString(Text); Color = color; Identifier = identifier; - Lifetime = 3.0f; + Lifetime = lifeTime; } } @@ -312,11 +312,7 @@ namespace Barotrauma } } - cursorPosition = cam.ScreenToWorld(PlayerInput.MousePosition); - if (AnimController.CurrentHull?.Submarine != null) - { - cursorPosition -= AnimController.CurrentHull.Submarine.Position; - } + UpdateLocalCursor(cam); Vector2 mouseSimPos = ConvertUnits.ToSimUnits(cursorPosition); if (GUI.PauseMenuOpen) @@ -393,6 +389,15 @@ namespace Barotrauma DisableControls = false; } + public void UpdateLocalCursor(Camera cam) + { + cursorPosition = cam.ScreenToWorld(PlayerInput.MousePosition); + if (AnimController.CurrentHull?.Submarine != null) + { + cursorPosition -= AnimController.CurrentHull.Submarine.DrawPosition; + } + } + partial void UpdateControlled(float deltaTime, Camera cam) { if (controlled != this) return; @@ -997,7 +1002,7 @@ namespace Barotrauma return nameColor; } - public void AddMessage(string rawText, Color color, bool playSound, string identifier = null, int? value = null) + public void AddMessage(string rawText, Color color, bool playSound, string identifier = null, int? value = null, float lifetime = 3.0f) { GUIMessage existingMessage = null; @@ -1026,7 +1031,7 @@ namespace Barotrauma } if (existingMessage == null || !value.HasValue) { - var newMessage = new GUIMessage(rawText, color, delay, identifier, value); + var newMessage = new GUIMessage(rawText, color, delay, identifier, value, lifetime); guiMessages.Insert(0, newMessage); if (playSound) { @@ -1156,9 +1161,9 @@ namespace Barotrauma } } - partial void OnTalentGiven(string talentIdentifier) + partial void OnTalentGiven(TalentPrefab talentPrefab) { - AddMessage(TextManager.Get("talentname." + talentIdentifier.ToString()), GUI.Style.Yellow, playSound: this == Controlled); + AddMessage(TextManager.Get("talentname." + talentPrefab.Identifier), GUI.Style.Yellow, playSound: this == Controlled); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index 26704a982..0455691f9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -113,7 +113,7 @@ namespace Barotrauma return character?.Inventory != null && - character.AllowInput && + !character.Removed && !character.IsKnockedDown && (controller?.User != character || !controller.HideHUD) && !IsCampaignInterfaceOpen && !ConversationAction.FadeScreenToBlack; @@ -406,6 +406,7 @@ namespace Barotrauma if (npc.CampaignInteractionType == CampaignMode.InteractionType.None || npc.Submarine != character.Submarine || npc.IsDead || npc.IsIncapacitated) { continue; } var iconStyle = GUI.Style.GetComponentStyle("CampaignInteractionIcon." + npc.CampaignInteractionType); + if (iconStyle == null) { continue; } Range visibleRange = new Range(npc.CurrentHull == Character.Controlled.CurrentHull ? 500.0f : 100.0f, float.PositiveInfinity); if (npc.CampaignInteractionType == CampaignMode.InteractionType.Examine) { @@ -431,7 +432,7 @@ namespace Barotrauma foreach (Item item in Item.ItemList) { if (item.IconStyle is null || item.Submarine != character.Submarine) { continue; } - if (Vector2.DistanceSquared(character.Position, item.Position) > 500f*500f) { continue; } + if (Vector2.DistanceSquared(character.Position, item.Position) > 500f * 500f) { continue; } var body = Submarine.CheckVisibility(character.SimPosition, item.SimPosition, ignoreLevel: true); if (body != null && body.UserData as Item != item) { continue; } GUI.DrawIndicator(spriteBatch, item.WorldPosition + new Vector2(0f, item.RectHeight * 0.65f), cam, new Range(-100f, 500.0f), item.IconStyle.GetDefaultSprite(), item.IconStyle.Color, createOffset: false); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index 3d42e6e3f..025aadcd0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -16,6 +16,7 @@ namespace Barotrauma private static Sprite infoAreaPortraitBG; public bool LastControlled; + public int CrewListIndex { get; set; } = -1; #warning TODO: Refactor private Sprite disguisedPortrait; @@ -521,6 +522,7 @@ namespace Barotrauma ch.SkinColor = skinColor; ch.HairColor = hairColor; ch.FacialHairColor = facialHairColor; + ch.SetPersonalityTrait(); if (ch.Job != null) { foreach (KeyValuePair skill in skillLevels) @@ -830,7 +832,7 @@ namespace Barotrauma }; new GUIFrame( - new RectTransform(new Vector2(1.25f, 1.25f), HeadSelectionList.RectTransform, Anchor.Center), + new RectTransform(new Vector2(1.25f, 1.25f), HeadSelectionList.ContentBackground.RectTransform, Anchor.Center), style: "OuterGlow", color: Color.Black) { UserData = "outerglow", @@ -965,10 +967,15 @@ namespace Barotrauma foreach (Sprite sprite in characterSprites) { sprite.Remove(); } characterSprites.Clear(); } - + public void Dispose() { ClearSprites(); + if (HeadSelectionList != null) + { + HeadSelectionList.RectTransform.Parent = null; + HeadSelectionList = null; + } } ~AppearanceCustomizationMenu() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs index f4a5f282c..e5bd89ebb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs @@ -2,12 +2,20 @@ { 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"), GUI.Style.Red); break; case InfectionState.Transition: @@ -23,6 +31,7 @@ default: break; } + prevDisplayedMessage = State; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionPsychosis.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionPsychosis.cs index e08502aa9..20249f49b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionPsychosis.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionPsychosis.cs @@ -143,7 +143,7 @@ namespace Barotrauma Hull fireHull = Hull.hullList.GetRandom(h => h.Submarine == character.Submarine); if (fireHull != null) { - var fakeFire = new DummyFireSource(Vector2.One * 500.0f, new Vector2(Rand.Range(fireHull.WorldRect.X, fireHull.WorldRect.Right), fireHull.WorldPosition.Y), fireHull, isNetworkMessage: true) + var fakeFire = new DummyFireSource(Vector2.One * 500.0f, new Vector2(Rand.Range(fireHull.WorldRect.X, fireHull.WorldRect.Right), fireHull.WorldPosition.Y + 1), fireHull, isNetworkMessage: true) { CausedByPsychosis = true, DamagesItems = false, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 2660e8a10..d3773294f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -531,6 +531,8 @@ namespace Barotrauma bloodParticleTimer -= deltaTime * (affliction.Strength / 10.0f); if (bloodParticleTimer <= 0.0f) { + Limb limb = targetLimb ?? Character.AnimController.MainLimb; + bool inWater = Character.AnimController.InWater; var drawTarget = inWater ? Particles.ParticlePrefab.DrawTargetType.Water : Particles.ParticlePrefab.DrawTargetType.Air; var emitter = Character.BloodEmitters.FirstOrDefault(e => e.Prefab.ParticlePrefab.DrawTarget == drawTarget || e.Prefab.ParticlePrefab.DrawTarget == Particles.ParticlePrefab.DrawTargetType.Both); @@ -543,13 +545,13 @@ namespace Barotrauma if (!inWater) { bloodParticleSize *= 2.0f; - velocity = targetLimb.LinearVelocity * 100.0f; + velocity = limb.LinearVelocity * 100.0f; } // TODO: use the blood emitter? var blood = GameMain.ParticleManager.CreateParticle( inWater ? Character.Params.BleedParticleWater : Character.Params.BleedParticleAir, - targetLimb.WorldPosition, velocity, 0.0f, Character.AnimController.CurrentHull); + limb.WorldPosition, velocity, 0.0f, Character.AnimController.CurrentHull); if (blood != null && !inWater) { @@ -954,7 +956,7 @@ namespace Barotrauma public void DrawHUD(SpriteBatch spriteBatch) { - if (GUI.DisableHUD) { return; } + if (GUI.DisableHUD || Character.Removed) { return; } if (GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y || Math.Abs(inventoryScale - Inventory.UIScale) > 0.01f || @@ -995,14 +997,16 @@ namespace Barotrauma { healthBar.RectTransform.ScreenSpaceOffset = healthBarShadow.RectTransform.ScreenSpaceOffset = Point.Zero; } - - // If manning a turret the portrait doesn't get rendered so we push the health bar to remove the empty gap - healthBarHolder.RectTransform.ScreenSpaceOffset = Character.ShouldLockHud() ? new Point(0, HUDLayoutSettings.PortraitArea.Height) : Point.Zero; + + if (healthBarHolder != null) + { + // If manning a turret the portrait doesn't get rendered so we push the health bar to remove the empty gap + healthBarHolder.RectTransform.ScreenSpaceOffset = Character.ShouldLockHud() ? new Point(0, HUDLayoutSettings.PortraitArea.Height) : Point.Zero; + } DrawStatusHUD(spriteBatch); } - private (Affliction affliction, string text)? highlightedAfflictionIcon; public void DrawStatusHUD(SpriteBatch spriteBatch) { @@ -1122,23 +1126,24 @@ namespace Barotrauma } public static Color GetAfflictionIconColor(AfflictionPrefab prefab, Affliction affliction) + { + return GetAfflictionIconColor(prefab, affliction.Strength); + } + + public static Color GetAfflictionIconColor(AfflictionPrefab prefab, float afflictionStrength) { // No specific colors, use generic if (prefab.IconColors == null) { if (prefab.IsBuff) { - return ToolBox.GradientLerp(affliction.Strength / prefab.MaxStrength, GUI.Style.BuffColorLow, GUI.Style.BuffColorMedium, GUI.Style.BuffColorHigh); - } - else - { - return ToolBox.GradientLerp(affliction.Strength / prefab.MaxStrength, GUI.Style.DebuffColorLow, GUI.Style.DebuffColorMedium, GUI.Style.DebuffColorHigh); + return ToolBox.GradientLerp(afflictionStrength / prefab.MaxStrength, GUI.Style.BuffColorLow, GUI.Style.BuffColorMedium, GUI.Style.BuffColorHigh); } + + return ToolBox.GradientLerp(afflictionStrength / prefab.MaxStrength, GUI.Style.DebuffColorLow, GUI.Style.DebuffColorMedium, GUI.Style.DebuffColorHigh); } - else - { - return ToolBox.GradientLerp(affliction.Strength / prefab.MaxStrength, prefab.IconColors); - } + + return ToolBox.GradientLerp(afflictionStrength / prefab.MaxStrength, prefab.IconColors); } public static Color GetAfflictionIconColor(Affliction affliction) => GetAfflictionIconColor(affliction.Prefab, affliction); @@ -1153,7 +1158,7 @@ namespace Barotrauma return; } - if (afflictionsDirty()) + if (afflictionsDirty() || selectedLimb != currentDisplayedLimb) { var currentAfflictions = afflictions.Where(a => ShouldDisplayAfflictionOnLimb(a, selectedLimb)).Select(a => a.Key); CreateAfflictionInfos(currentAfflictions); @@ -2015,6 +2020,27 @@ namespace Barotrauma limbIndicatorOverlay?.Remove(); limbIndicatorOverlay = null; + + if (healthWindow != null) + { + healthWindow.RectTransform.Parent = null; + healthWindow = null; + } + if (healthBarHolder != null) + { + healthBarHolder.RectTransform.Parent = null; + healthBarHolder = null; + } + if (SuicideButton != null) + { + SuicideButton.RectTransform.Parent = null; + SuicideButton = null; + } + if (afflictionTooltip != null) + { + afflictionTooltip.RectTransform.Parent = null; + afflictionTooltip = null; + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index 8f5f6b791..15bd29bf4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -73,7 +73,6 @@ namespace Barotrauma public void Draw(SpriteBatch spriteBatch) { - // TODO: move this into the character editor //var mouthPos = ragdoll.GetMouthPosition(); //if (mouthPos != null) //{ @@ -173,6 +172,7 @@ namespace Barotrauma public float DefaultSpriteDepth { get; private set; } + public WearableSprite HairWithHatSprite { get; set; } public WearableSprite HuskSprite { get; private set; } public WearableSprite HerpesSprite { get; private set; } @@ -236,8 +236,8 @@ namespace Barotrauma public string HitSoundTag => Params?.Sound?.Tag; - private List wearableTypeHidingSprites = new List(); - private List wearableTypesToHide = new List(); + private readonly List wearableTypeHidingSprites = new List(); + private readonly HashSet wearableTypesToHide = new HashSet(); private bool enableHuskSprite; public bool EnableHuskSprite { @@ -895,7 +895,22 @@ namespace Barotrauma foreach (WearableSprite wearable in OtherWearables) { if (wearable.Type == WearableType.Husk) { continue; } - if (wearableTypesToHide.Contains(wearable.Type)) { continue; } + if (wearableTypesToHide.Contains(wearable.Type)) + { + if (wearable.Type == WearableType.Hair) + { + if (HairWithHatSprite != null) + { + DrawWearable(HairWithHatSprite, depthStep, spriteBatch, blankColor, alpha: color.A / 255f, spriteEffect); + depthStep += step; + continue; + } + } + else + { + continue; + } + } DrawWearable(wearable, depthStep, spriteBatch, blankColor, alpha: color.A / 255f, spriteEffect); //if there are multiple sprites on this limb, make the successive ones be drawn in front depthStep += step; @@ -1195,6 +1210,9 @@ namespace Barotrauma HuskSprite?.Sprite.Remove(); HuskSprite = null; + HairWithHatSprite?.Sprite.Remove(); + HairWithHatSprite = null; + HerpesSprite?.Sprite.Remove(); HerpesSprite = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index fd1733473..c742cf0e5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -32,7 +32,8 @@ namespace Barotrauma public void ClientExecute(string[] args) { - if (!CheatsEnabled && IsCheat) + bool allowCheats = GameMain.NetworkMember == null && (GameMain.GameSession?.GameMode is TestGameMode || Screen.Selected is EditorScreen); + 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 @@ -743,7 +744,7 @@ namespace Barotrauma AssignOnExecute("explosion", (string[] args) => { Vector2 explosionPos = GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition); - float range = 500, force = 10, damage = 50, structureDamage = 10, itemDamage = 100, empStrength = 0.0f, ballastFloraStrength = 50f; + float range = 500, force = 10, damage = 50, structureDamage = 20, itemDamage = 100, empStrength = 0.0f, ballastFloraStrength = 50f; if (args.Length > 0) float.TryParse(args[0], out range); if (args.Length > 1) float.TryParse(args[1], out force); if (args.Length > 2) float.TryParse(args[2], out damage); @@ -1120,7 +1121,7 @@ namespace Barotrauma return; } - if (Submarine.MainSub.SaveAs(Barotrauma.IO.Path.Combine(SubmarineInfo.SavePath, fileName + ".sub"))) + if (Submarine.MainSub.TrySaveAs(Barotrauma.IO.Path.Combine(SubmarineInfo.SavePath, fileName + ".sub"))) { NewMessage("Sub saved", Color.Green); } @@ -1894,7 +1895,12 @@ namespace Barotrauma ThrowError($"\"{args[0]}\" is not a valid Level.PositionType. Available options are: {string.Join(", ", enums)}"); return; } - debugLines = EventSet.GetDebugStatistics(filter: monsterEvent => monsterEvent.SpawnPosType.HasFlag(spawnType)); + bool fullLog = false; + if (args.Length > 1) + { + bool.TryParse(args[1], out fullLog); + } + debugLines = EventSet.GetDebugStatistics(filter: monsterEvent => monsterEvent.SpawnPosType.HasFlag(spawnType), fullLog: fullLog); } else { @@ -2392,8 +2398,9 @@ namespace Barotrauma commands.Add(new Command("querylobbies", "Queries all SteamP2P lobbies", (args) => { TaskPool.Add("DebugQueryLobbies", - SteamManager.LobbyQueryRequest(), (t) => { - var lobbies = ((Task>)t).Result; + SteamManager.LobbyQueryRequest(), (t) => + { + t.TryGetResult(out List lobbies); foreach (var lobby in lobbies) { NewMessage(lobby.GetData("name") + ", " + lobby.GetData("lobbyowner"), Color.Yellow); @@ -2408,7 +2415,7 @@ namespace Barotrauma TextManager.CheckForDuplicates(args[0]); })); - commands.Add(new Command("writetocsv", "Writes the default language (English) to a .csv file.", (string[] args) => + commands.Add(new Command("writetocsv|xmltocsv", "Writes the default language (English) to a .csv file.", (string[] args) => { TextManager.WriteToCSV(); NPCConversation.WriteToCSV(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs index d5161fc4d..072e0615a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs @@ -1,3 +1,4 @@ +using Barotrauma.Extensions; using Barotrauma.Networking; namespace Barotrauma @@ -20,6 +21,9 @@ namespace Barotrauma } } + public override bool DisplayAsCompleted => State > 0 && requireRescue.None(); + public override bool DisplayAsFailed => State == HostagesKilledState; + public override void ClientReadInitial(IReadMessage msg) { base.ClientReadInitial(msg); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AlienRuinMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AlienRuinMission.cs index b943081b2..dbfd01ecc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AlienRuinMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AlienRuinMission.cs @@ -4,6 +4,9 @@ namespace Barotrauma { partial class AlienRuinMission : Mission { + public override bool DisplayAsCompleted => State > 0; + public override bool DisplayAsFailed => false; + public override void ClientReadInitial(IReadMessage msg) { base.ClientReadInitial(msg); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/BeaconMission.cs new file mode 100644 index 000000000..4ee3c5a9d --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/BeaconMission.cs @@ -0,0 +1,8 @@ +namespace Barotrauma +{ + partial class BeaconMission : Mission + { + public override bool DisplayAsCompleted => State > 0; + public override bool DisplayAsFailed => false; + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs index 23b228922..e4de29441 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs @@ -5,6 +5,9 @@ namespace Barotrauma { partial class CargoMission : Mission { + public override bool DisplayAsCompleted => false; + public override bool DisplayAsFailed => false; + public override string GetMissionRewardText(Submarine sub) { string rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CombatMission.cs index 9fb2b490b..401f5278f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CombatMission.cs @@ -20,5 +20,8 @@ namespace Barotrauma return descriptions[GameMain.Client.Character.TeamID == CharacterTeamType.Team1 ? 1 : 2]; } } + + public override bool DisplayAsCompleted => false; + public override bool DisplayAsFailed => false; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EscortMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EscortMission.cs index 230570e5e..99804c87a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EscortMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EscortMission.cs @@ -4,6 +4,9 @@ namespace Barotrauma { partial class EscortMission : Mission { + public override bool DisplayAsCompleted => false; + public override bool DisplayAsFailed => State == 1; + public override void ClientReadInitial(IReadMessage msg) { base.ClientReadInitial(msg); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/GoToMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/GoToMission.cs new file mode 100644 index 000000000..6036c0586 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/GoToMission.cs @@ -0,0 +1,8 @@ +namespace Barotrauma +{ + partial class GoToMission : Mission + { + public override bool DisplayAsCompleted => false; + public override bool DisplayAsFailed => false; + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs index 3ce80fd05..f97da96d5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs @@ -1,15 +1,14 @@ -using Barotrauma.Extensions; -using Barotrauma.Items.Components; +using Barotrauma.Items.Components; using Barotrauma.Networking; -using Microsoft.Xna.Framework; -using System; using System.Collections.Generic; -using System.Linq; namespace Barotrauma { partial class MineralMission : Mission { + public override bool DisplayAsCompleted => false; + public override bool DisplayAsFailed => false; + public override void ClientReadInitial(IReadMessage msg) { base.ClientReadInitial(msg); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs index 0dc6284b6..ab7a3a6f0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs @@ -19,6 +19,15 @@ namespace Barotrauma public virtual IEnumerable HudIconTargets => Enumerable.Empty(); + /// + /// Is the mission at a state at which the only thing left to do is to reach the end of the level? + /// + public abstract bool DisplayAsCompleted { get; } + /// + /// Is the mission at a state at which the mission cannot be completed anymore? + /// + public abstract bool DisplayAsFailed { get; } + public Color GetDifficultyColor() { int v = Difficulty ?? MissionPrefab.MinDifficulty; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MonsterMission.cs index e5aab9f41..3033769f1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MonsterMission.cs @@ -4,6 +4,9 @@ namespace Barotrauma { partial class MonsterMission : Mission { + public override bool DisplayAsCompleted => State > 0; + public override bool DisplayAsFailed => false; + public override void ClientReadInitial(IReadMessage msg) { base.ClientReadInitial(msg); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/NestMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/NestMission.cs index dcc6ee187..f9538a0a3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/NestMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/NestMission.cs @@ -6,6 +6,9 @@ namespace Barotrauma { partial class NestMission : Mission { + public override bool DisplayAsCompleted => State > 0 && !requireDelivery; + public override bool DisplayAsFailed => false; + public override void ClientReadInitial(IReadMessage msg) { base.ClientReadInitial(msg); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/PirateMission.cs index d111e664c..3de30bdd8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/PirateMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/PirateMission.cs @@ -4,6 +4,9 @@ namespace Barotrauma { partial class PirateMission : Mission { + public override bool DisplayAsCompleted => State > 1; + public override bool DisplayAsFailed => false; + public override void ClientReadInitial(IReadMessage msg) { base.ClientReadInitial(msg); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs index 6479ddb90..12c80c496 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs @@ -5,6 +5,9 @@ namespace Barotrauma { partial class SalvageMission : Mission { + public override bool DisplayAsCompleted => false; + public override bool DisplayAsFailed => false; + public override void ClientReadInitial(IReadMessage msg) { base.ClientReadInitial(msg); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/ScanMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/ScanMission.cs index 93dadacc4..291de4759 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/ScanMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/ScanMission.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; @@ -23,6 +22,9 @@ namespace Barotrauma } } + public override bool DisplayAsCompleted => false; + public override bool DisplayAsFailed => false; + public override void ClientReadInitial(IReadMessage msg) { base.ClientReadInitial(msg); @@ -60,7 +62,16 @@ namespace Barotrauma ushort id = msg.ReadUInt16(); bool scanned = msg.ReadBoolean(); Entity entity = Entity.FindEntityByID(id); - scanTargets.Add(entity as WayPoint, scanned); + if (!(entity is WayPoint wayPoint)) + { + string errorMsg = $"Failed to find a waypoint in ScanMission.ClientReadScanTargetStatus. Entity {id} was {(entity?.ToString() ?? null)}"; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("ScanMission.ClientReadScanTargetStatus", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + } + else + { + scanTargets.Add(wayPoint, scanned); + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs index aff1e4c05..b2540f17c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs @@ -1,8 +1,10 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using SharpFont; using System; using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; namespace Barotrauma @@ -130,7 +132,7 @@ namespace Barotrauma /// Character ranges between each even element with their corresponding odd element. Default is 0x20 to 0xFFFF. /// Texture dimensions. Default is 512x512. /// Base character used to shift all other characters downwards when rendering. Defaults to T. - public void RenderAtlas(GraphicsDevice gd, uint[] charRanges = null, int texDims = 1024, uint baseChar = 0x54) + private void RenderAtlas(GraphicsDevice gd, uint[] charRanges = null, int texDims = 1024, uint baseChar = 0x54) { if (DynamicLoading) { return; } @@ -253,13 +255,19 @@ namespace Barotrauma } } - public void DynamicRenderAtlas(GraphicsDevice gd, uint character, int texDims = 1024, uint baseChar = 0x54) + private void DynamicRenderAtlas(GraphicsDevice gd, uint character, int texDims = 1024, uint baseChar = 0x54) + => DynamicRenderAtlas(gd, character.ToEnumerable(), texDims, baseChar); + + private void DynamicRenderAtlas(GraphicsDevice gd, string str, int texDims = 1024, uint baseChar = 0x54) + => DynamicRenderAtlas(gd, str.Distinct().Select(c => (uint)c), texDims, baseChar); + + private void DynamicRenderAtlas(GraphicsDevice gd, IEnumerable characters, int texDims = 1024, uint baseChar = 0x54) { if (System.Threading.Thread.CurrentThread != GameMain.MainThread) { CrossThread.RequestExecutionOnMainThread(() => { - DynamicRenderAtlas(gd, character, texDims, baseChar); + DynamicRenderAtlas(gd, characters, texDims, baseChar); }); return; } @@ -271,7 +279,6 @@ namespace Barotrauma lock (mutex) { - if (texCoords.ContainsKey(character)) { return; } if (textures.Count == 0) { this.texDims = texDims; @@ -282,79 +289,90 @@ namespace Barotrauma textures.Add(new Texture2D(gd, texDims, texDims, false, SurfaceFormat.Color)); } - uint glyphIndex = face.GetCharIndex(character); - if (glyphIndex == 0) { return; } - - face.SetPixelSizes(0, size); - face.LoadGlyph(glyphIndex, LoadFlags.Default, LoadTarget.Normal); - if (face.Glyph.Metrics.Width == 0 || face.Glyph.Metrics.Height == 0) + bool anyChanges = false; + bool firstChar = true; + foreach (var character in characters) { - if (face.Glyph.Metrics.HorizontalAdvance > 0) + if (texCoords.ContainsKey(character)) { continue; } + + uint glyphIndex = face.GetCharIndex(character); + if (glyphIndex == 0) { continue; } + + face.SetPixelSizes(0, size); + face.LoadGlyph(glyphIndex, LoadFlags.Default, LoadTarget.Normal); + if (face.Glyph.Metrics.Width == 0 || face.Glyph.Metrics.Height == 0) { - //glyph is empty, but char still applies advance - GlyphData blankData = new GlyphData( - advance: (float)face.Glyph.Metrics.HorizontalAdvance, - texIndex: -1); //indicates no texture because the glyph is empty - texCoords.Add(character, blankData); + if (face.Glyph.Metrics.HorizontalAdvance > 0) + { + //glyph is empty, but char still applies advance + GlyphData blankData = new GlyphData( + advance: (float)face.Glyph.Metrics.HorizontalAdvance, + texIndex: -1); //indicates no texture because the glyph is empty + texCoords.Add(character, blankData); + } + continue; } - return; - } - //stacktrace doesn't really work that well when RenderGlyph throws an exception - face.Glyph.RenderGlyph(RenderMode.Normal); - bitmap = (byte[])face.Glyph.Bitmap.BufferData.Clone(); - glyphWidth = face.Glyph.Bitmap.Width; - glyphHeight = bitmap.Length / glyphWidth; - horizontalAdvance = face.Glyph.Metrics.HorizontalAdvance; - drawOffset = new Vector2(face.Glyph.BitmapLeft, baseHeight * 14 / 10 - face.Glyph.BitmapTop); - - if (glyphWidth > texDims - 1 || glyphHeight > texDims - 1) - { - throw new Exception(filename + ", " + size.ToString() + ", " + (char)character + "; Glyph dimensions exceed texture atlas dimensions"); - } - - currentDynamicAtlasNextY = Math.Max(currentDynamicAtlasNextY, glyphHeight + 2); - if (currentDynamicAtlasCoords.X + glyphWidth + 2 > texDims - 1) - { - currentDynamicAtlasCoords.X = 0; - currentDynamicAtlasCoords.Y += currentDynamicAtlasNextY; - currentDynamicAtlasNextY = 0; - } - //no more room in current texture atlas, create a new one - if (currentDynamicAtlasCoords.Y + glyphHeight + 2 > texDims - 1) - { - currentDynamicAtlasCoords.X = 0; - currentDynamicAtlasCoords.Y = 0; - currentDynamicAtlasNextY = 0; - textures.Add(new Texture2D(gd, texDims, texDims, false, SurfaceFormat.Color)); - currentDynamicPixelBuffer = null; - } + //stacktrace doesn't really work that well when RenderGlyph throws an exception + face.Glyph.RenderGlyph(RenderMode.Normal); + bitmap = (byte[])face.Glyph.Bitmap.BufferData.Clone(); + glyphWidth = face.Glyph.Bitmap.Width; + glyphHeight = bitmap.Length / glyphWidth; + horizontalAdvance = face.Glyph.Metrics.HorizontalAdvance; + drawOffset = new Vector2(face.Glyph.BitmapLeft, baseHeight * 14 / 10 - face.Glyph.BitmapTop); - GlyphData newData = new GlyphData( - advance: (float)horizontalAdvance, - texIndex: textures.Count - 1, - texCoords: new Rectangle((int)currentDynamicAtlasCoords.X, (int)currentDynamicAtlasCoords.Y, glyphWidth, glyphHeight), - drawOffset: drawOffset - ); - texCoords.Add(character, newData); - - if (currentDynamicPixelBuffer == null) - { - currentDynamicPixelBuffer = new uint[texDims * texDims]; - textures[newData.TexIndex].GetData(currentDynamicPixelBuffer, 0, texDims * texDims); - } - - for (int y = 0; y < glyphHeight; y++) - { - for (int x = 0; x < glyphWidth; x++) + if (glyphWidth > texDims - 1 || glyphHeight > texDims - 1) { - byte byteColor = bitmap[x + y * glyphWidth]; - currentDynamicPixelBuffer[((int)currentDynamicAtlasCoords.X + x) + ((int)currentDynamicAtlasCoords.Y + y) * texDims] = (uint)(byteColor << 24 | 0x00ffffff); + throw new Exception(filename + ", " + size.ToString() + ", " + (char)character + "; Glyph dimensions exceed texture atlas dimensions"); } - } - textures[newData.TexIndex].SetData(currentDynamicPixelBuffer); - currentDynamicAtlasCoords.X += glyphWidth + 2; + currentDynamicAtlasNextY = Math.Max(currentDynamicAtlasNextY, glyphHeight + 2); + if (currentDynamicAtlasCoords.X + glyphWidth + 2 > texDims - 1) + { + currentDynamicAtlasCoords.X = 0; + currentDynamicAtlasCoords.Y += currentDynamicAtlasNextY; + currentDynamicAtlasNextY = 0; + } + //no more room in current texture atlas, create a new one + if (currentDynamicAtlasCoords.Y + glyphHeight + 2 > texDims - 1) + { + if (!firstChar) { textures[^1].SetData(currentDynamicPixelBuffer); } + currentDynamicAtlasCoords.X = 0; + currentDynamicAtlasCoords.Y = 0; + currentDynamicAtlasNextY = 0; + textures.Add(new Texture2D(gd, texDims, texDims, false, SurfaceFormat.Color)); + currentDynamicPixelBuffer = null; + } + + GlyphData newData = new GlyphData( + advance: (float)horizontalAdvance, + texIndex: textures.Count - 1, + texCoords: new Rectangle((int)currentDynamicAtlasCoords.X, (int)currentDynamicAtlasCoords.Y, glyphWidth, glyphHeight), + drawOffset: drawOffset + ); + texCoords.Add(character, newData); + + if (currentDynamicPixelBuffer == null) + { + currentDynamicPixelBuffer = new uint[texDims * texDims]; + textures[newData.TexIndex].GetData(currentDynamicPixelBuffer, 0, texDims * texDims); + } + + for (int y = 0; y < glyphHeight; y++) + { + for (int x = 0; x < glyphWidth; x++) + { + byte byteColor = bitmap[x + y * glyphWidth]; + currentDynamicPixelBuffer[((int)currentDynamicAtlasCoords.X + x) + ((int)currentDynamicAtlasCoords.Y + y) * texDims] = (uint)(byteColor << 24 | 0x00ffffff); + } + } + + currentDynamicAtlasCoords.X += glyphWidth + 2; + firstChar = false; + anyChanges = true; + } + + if (anyChanges) { textures[^1].SetData(currentDynamicPixelBuffer); } } } @@ -374,6 +392,10 @@ namespace Barotrauma public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth) { if (textures.Count == 0 && !DynamicLoading) { return; } + if (DynamicLoading) + { + DynamicRenderAtlas(graphicsDevice, text); + } int lineNum = 0; Vector2 currentPos = position; @@ -390,10 +412,6 @@ namespace Barotrauma } uint charIndex = text[i]; - if (DynamicLoading) - { - DynamicRenderAtlas(graphicsDevice, charIndex); - } GlyphData gd = GetGlyphData(charIndex); if (gd.TexIndex >= 0) @@ -417,6 +435,10 @@ namespace Barotrauma public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color) { if (textures.Count == 0 && !DynamicLoading) { return; } + if (DynamicLoading) + { + DynamicRenderAtlas(graphicsDevice, text); + } Vector2 currentPos = position; for (int i = 0; i < text.Length; i++) @@ -429,10 +451,6 @@ namespace Barotrauma } uint charIndex = text[i]; - if (DynamicLoading) - { - DynamicRenderAtlas(graphicsDevice, charIndex); - } GlyphData gd = GetGlyphData(charIndex); if (gd.TexIndex >= 0) @@ -452,6 +470,10 @@ namespace Barotrauma public void DrawStringWithColors(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth, List richTextData, int rtdOffset = 0) { if (textures.Count == 0 && !DynamicLoading) { return; } + if (DynamicLoading) + { + DynamicRenderAtlas(graphicsDevice, text); + } int lineNum = 0; Vector2 currentPos = position; @@ -472,10 +494,6 @@ namespace Barotrauma } uint charIndex = text[i]; - if (DynamicLoading && !texCoords.ContainsKey(charIndex)) - { - DynamicRenderAtlas(graphicsDevice, charIndex); - } Color currentTextColor; @@ -626,6 +644,10 @@ namespace Barotrauma { retVal.Y = baseHeight; } + if (DynamicLoading) + { + DynamicRenderAtlas(graphicsDevice, text); + } for (int i = 0; i < text.Length; i++) { @@ -636,10 +658,6 @@ namespace Barotrauma continue; } uint charIndex = text[i]; - if (DynamicLoading && !texCoords.ContainsKey(charIndex)) - { - DynamicRenderAtlas(graphicsDevice, charIndex); - } GlyphData gd = GetGlyphData(charIndex); currentLineX += gd.Advance; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index 0ca71144c..116f2b857 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -44,12 +44,12 @@ namespace Barotrauma Waiting, // Hourglass WaitingBackground // Cursor + Hourglass } - + public static class GUI { public static GUICanvas Canvas => GUICanvas.Instance; public static CursorState MouseCursor = CursorState.Default; - + public static readonly SamplerState SamplerState = new SamplerState() { Filter = TextureFilter.Linear, @@ -116,14 +116,14 @@ namespace Barotrauma public static float SlicedSpriteScale { - get + get { - if (Math.Abs(1.0f - Scale) < 0.1f) - { + if (Math.Abs(1.0f - Scale) < 0.1f) + { //don't scale if very close to the "reference resolution" - return 1.0f; + return 1.0f; } - return Scale; + return Scale; } } @@ -306,7 +306,7 @@ namespace Barotrauma t = new Texture2D(GraphicsDevice, 1, 1); t.SetData(new Color[] { Color.White });// fill the texture with white }); - + SubmarineIcon = new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(452, 385, 182, 81), new Vector2(0.5f, 0.5f)); arrow = new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(393, 393, 49, 45), new Vector2(0.5f, 0.5f)); SpeechBubbleIcon = new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(385, 449, 66, 60), new Vector2(0.5f, 0.5f)); @@ -314,7 +314,7 @@ namespace Barotrauma } /// - /// By default, all the gui elements are drawn automatically in the same order they appear on the update list. + /// By default, all the gui elements are drawn automatically in the same order they appear on the update list. /// public static void Draw(Camera cam, SpriteBatch spriteBatch) { @@ -389,6 +389,12 @@ namespace Barotrauma DrawString(spriteBatch, new Vector2(10, 10), "FPS: " + Math.Round(GameMain.PerformanceCounter.AverageFramesPerSecond), Color.White, Color.Black * 0.5f, 0, SmallFont); + if (GameMain.GameSession != null && Timing.TotalTime > GameMain.GameSession.RoundStartTime + 1.0) + { + DrawString(spriteBatch, new Vector2(10, 25), + $"Physics: {GameMain.CurrentUpdateRate}", + (GameMain.CurrentUpdateRate < Timing.FixedUpdateRate) ? Color.Red : Color.White, Color.Black * 0.5f, 0, SmallFont); + } } if (GameMain.ShowPerf) @@ -397,7 +403,7 @@ namespace Barotrauma DrawString(spriteBatch, new Vector2(300, y), "Draw - Avg: " + GameMain.PerformanceCounter.DrawTimeGraph.Average().ToString("0.00") + " ms" + " Max: " + GameMain.PerformanceCounter.DrawTimeGraph.LargestValue().ToString("0.00") + " ms", - GUI.Style.Green, Color.Black * 0.8f, font: SmallFont); + Style.Green, Color.Black * 0.8f, font: SmallFont); y += 15; GameMain.PerformanceCounter.DrawTimeGraph.Draw(spriteBatch, new Rectangle(300, y, 170, 50), color: Style.Green); y += 50; @@ -408,7 +414,6 @@ namespace Barotrauma Color.LightBlue, Color.Black * 0.8f, font: SmallFont); y += 15; GameMain.PerformanceCounter.UpdateTimeGraph.Draw(spriteBatch, new Rectangle(300, y, 170, 50), color: Color.LightBlue); - GameMain.PerformanceCounter.UpdateIterationsGraph.Draw(spriteBatch, new Rectangle(300, y, 170, 50), maxValue: 20, color: Style.Red); y += 50; foreach (string key in GameMain.PerformanceCounter.GetSavedIdentifiers) { @@ -431,7 +436,7 @@ namespace Barotrauma } } - if (GameMain.DebugDraw) + if (GameMain.DebugDraw && !Submarine.Unloading && !(Screen.Selected is RoundSummaryScreen)) { DrawString(spriteBatch, new Vector2(10, 25), "Physics: " + GameMain.World.UpdateTime, @@ -706,11 +711,11 @@ namespace Barotrauma spriteBatch.Begin(SpriteSortMode.Immediate, effect: GameMain.GameScreen.PostProcessEffect); float scale = Math.Max( - (float)GameMain.GraphicsWidth / backgroundSprite.SourceRect.Width, + (float)GameMain.GraphicsWidth / backgroundSprite.SourceRect.Width, (float)GameMain.GraphicsHeight / backgroundSprite.SourceRect.Height) * 1.1f; float paddingX = backgroundSprite.SourceRect.Width * scale - GameMain.GraphicsWidth; float paddingY = backgroundSprite.SourceRect.Height * scale - GameMain.GraphicsHeight; - + double noiseT = (Timing.TotalTime * 0.02f); Vector2 pos = new Vector2((float)PerlinNoise.CalculatePerlin(noiseT, noiseT, 0) - 0.5f, (float)PerlinNoise.CalculatePerlin(noiseT, noiseT, 0.5f) - 0.5f); pos = new Vector2(pos.X * paddingX, pos.Y * paddingY); @@ -719,7 +724,7 @@ namespace Barotrauma new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) / 2 + pos, null, Color.White, 0.0f, backgroundSprite.size / 2, scale, SpriteEffects.None, 0.0f); - + spriteBatch.End(); } @@ -759,8 +764,8 @@ namespace Barotrauma else { additions.Enqueue(component); - } - } + } + } } /// @@ -786,7 +791,7 @@ namespace Barotrauma component.Children.ForEach(c => RemoveFromUpdateList(c)); } } - } + } } public static void ClearUpdateList() @@ -900,7 +905,7 @@ namespace Barotrauma { GUIMessageBox.VisibleBox.AddToGUIUpdateList(); } - } + } } #endregion @@ -941,7 +946,7 @@ namespace Barotrauma inventoryIndex = updateList.IndexOf(CharacterHUD.HUDFrame); } - if ((!PlayerInput.PrimaryMouseButtonHeld() && !PlayerInput.PrimaryMouseButtonClicked()) || + if ((!PlayerInput.PrimaryMouseButtonHeld() && !PlayerInput.PrimaryMouseButtonClicked()) || (prevMouseOn == null && !PlayerInput.SecondaryMouseButtonHeld() && !Inventory.DraggingItems.Any())) { for (var i = updateList.Count - 1; i > inventoryIndex; i--) @@ -967,7 +972,7 @@ namespace Barotrauma return MouseOn; } } - + private static CursorState UpdateMouseCursorState(GUIComponent c) { lock (mutex) @@ -994,7 +999,7 @@ namespace Barotrauma } if (Wire.DraggingWire != null) { return CursorState.Dragging; } } - + if (c == null || c is GUICustomComponent) { switch (Screen.Selected) @@ -1027,7 +1032,7 @@ namespace Barotrauma } } } - + if (c != null && c.Visible) { if (c.AlwaysOverrideCursor) { return c.HoverCursor; } @@ -1036,20 +1041,20 @@ namespace Barotrauma // And this is of course picked up as clickable area. // There has to be a better way of checking this but for now this works. var monitorRect = new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight); - + var parent = FindInteractParent(c); - + if (c.Enabled) { // Some parent elements take priority // but not when the child is a GUIButton or GUITickBox - if (!(parent is GUIButton) && !(parent is GUIListBox) || + if (!(parent is GUIButton) && !(parent is GUIListBox) || (c is GUIButton) || (c is GUITickBox)) { if (!c.Rect.Equals(monitorRect)) { return c.HoverCursor; } } } - + // Children in list boxes can be interacted with despite not having // a GUIButton inside of them so instead of hard coding we check if // the children can be interacted with by checking their hover state @@ -1084,7 +1089,7 @@ namespace Barotrauma { // Health menus if (character.CharacterHealth.MouseOnElement) { return CursorState.Hand; } - + if (character.SelectedCharacter != null) { if (character.SelectedCharacter.CharacterHealth.MouseOnElement) @@ -1096,7 +1101,7 @@ namespace Barotrauma // Character is hovering over an item placed in the world if (character.FocusedItem != null) { return CursorState.Hand; } } - + return CursorState.Default; static GUIComponent FindInteractParent(GUIComponent component) @@ -1130,7 +1135,7 @@ namespace Barotrauma } } } - + static bool ContainsMouse(GUIComponent component) { // If component has a mouse rectangle then use that, if not use it's physical rect @@ -1138,7 +1143,7 @@ namespace Barotrauma component.MouseRect.Contains(PlayerInput.MousePosition) : component.Rect.Contains(PlayerInput.MousePosition); } - } + } } /// @@ -1153,8 +1158,8 @@ namespace Barotrauma { MouseCursor = CursorState.Waiting; var timeOut = DateTime.Now + new TimeSpan(0, 0, waitSeconds); - while (DateTime.Now < timeOut) - { + while (DateTime.Now < timeOut) + { if (endCondition != null) { try @@ -1163,13 +1168,13 @@ namespace Barotrauma } catch { break; } } - yield return CoroutineStatus.Running; + yield return CoroutineStatus.Running; } if (MouseCursor == CursorState.Waiting) { MouseCursor = CursorState.Default; } yield return CoroutineStatus.Success; } } - + public static void ClearCursorWait() { lock (mutex) @@ -1208,7 +1213,7 @@ namespace Barotrauma { debugDrawMetadataOffset--; } - + if (PlayerInput.KeyHit(Keys.Down)) { debugDrawMetadataOffset++; @@ -1240,17 +1245,20 @@ namespace Barotrauma debugDrawMetadataOffset = 0; } } - + } HandlePersistingElements(deltaTime); RefreshUpdateList(); UpdateMouseOn(); Debug.Assert(updateList.Count == updateListSet.Count); - updateList.ForEach(c => c.UpdateAuto(deltaTime)); + foreach (var c in updateList) + { + c.UpdateAuto(deltaTime); + } UpdateMessages(deltaTime); UpdateSavingIndicator(deltaTime); - } + } } private static void UpdateMessages(float deltaTime) @@ -1281,17 +1289,16 @@ namespace Barotrauma //only the first message (the currently visible one) is updated at a time break; } - + foreach (GUIMessage msg in messages) { if (!msg.WorldSpace) { continue; } - msg.Timer -= deltaTime; - msg.Pos += msg.Velocity * deltaTime; + msg.Timer -= deltaTime; + msg.Pos += msg.Velocity * deltaTime; } messages.RemoveAll(m => m.Timer <= 0.0f); } - } private static void UpdateSavingIndicator(float deltaTime) @@ -1349,7 +1356,7 @@ namespace Barotrauma #region Element drawing - private static List usedIndicatorAngles = new List(); + private static readonly List usedIndicatorAngles = new List(); /// Should the indicator move based on the camera position? /// Override the distance-based alpha value with the specified alpha value @@ -1628,7 +1635,7 @@ namespace Barotrauma private static void DrawMessages(SpriteBatch spriteBatch, Camera cam) { - if (messages.Count == 0) return; + if (messages.Count == 0) { return; } bool useScissorRect = messages.Any(m => !m.WorldSpace); Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle; @@ -1647,7 +1654,7 @@ namespace Barotrauma msg.Font.DrawString(spriteBatch, msg.Text, drawPos + msg.DrawPos + Vector2.One, Color.Black, 0, msg.Origin, 1.0f, SpriteEffects.None, 0); msg.Font.DrawString(spriteBatch, msg.Text, drawPos + msg.DrawPos, msg.Color, 0, msg.Origin, 1.0f, SpriteEffects.None, 0); - break; + break; } if (useScissorRect) @@ -1656,11 +1663,11 @@ namespace Barotrauma spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect; spriteBatch.Begin(SpriteSortMode.Deferred); } - + foreach (GUIMessage msg in messages) { if (!msg.WorldSpace) { continue; } - + if (cam != null) { float alpha = 1.0f; @@ -1669,7 +1676,7 @@ namespace Barotrauma Vector2 drawPos = cam.WorldToScreen(msg.DrawPos); msg.Font.DrawString(spriteBatch, msg.Text, drawPos + Vector2.One, Color.Black * alpha, 0, msg.Origin, 1.0f, SpriteEffects.None, 0); msg.Font.DrawString(spriteBatch, msg.Text, drawPos, msg.Color * alpha, 0, msg.Origin, 1.0f, SpriteEffects.None, 0); - } + } } messages.RemoveAll(m => m.Timer <= 0.0f); @@ -1770,7 +1777,7 @@ namespace Barotrauma { int textureWidth = Math.Max(radius * 2, 1); int textureHeight = Math.Max(height + radius * 2, 1); - + Color[] data = new Color[textureWidth * textureHeight]; // Colour the entire texture transparent first. @@ -1880,9 +1887,9 @@ namespace Barotrauma /// Creates multiple elements with relative size and positions them automatically. /// public static List CreateElements(int count, Vector2 relativeSize, RectTransform parent, Func constructor, - Anchor anchor = Anchor.TopLeft, Pivot? pivot = null, Point? minSize = null, Point? maxSize = null, - int absoluteSpacing = 0, float relativeSpacing = 0, Func extraSpacing = null, - int startOffsetAbsolute = 0, float startOffsetRelative = 0, bool isHorizontal = false) + Anchor anchor = Anchor.TopLeft, Pivot? pivot = null, Point? minSize = null, Point? maxSize = null, + int absoluteSpacing = 0, float relativeSpacing = 0, Func extraSpacing = null, + int startOffsetAbsolute = 0, float startOffsetRelative = 0, bool isHorizontal = false) where T : GUIComponent { return CreateElements(count, parent, constructor, relativeSize, null, anchor, pivot, minSize, maxSize, absoluteSpacing, relativeSpacing, extraSpacing, startOffsetAbsolute, startOffsetRelative, isHorizontal); @@ -1891,8 +1898,8 @@ namespace Barotrauma /// /// Creates multiple elements with absolute size and positions them automatically. /// - public static List CreateElements(int count, Point absoluteSize, RectTransform parent, Func constructor, - Anchor anchor = Anchor.TopLeft, Pivot? pivot = null, + public static List CreateElements(int count, Point absoluteSize, RectTransform parent, Func constructor, + Anchor anchor = Anchor.TopLeft, Pivot? pivot = null, int absoluteSpacing = 0, float relativeSpacing = 0, Func extraSpacing = null, int startOffsetAbsolute = 0, float startOffsetRelative = 0, bool isHorizontal = false) where T : GUIComponent @@ -1991,7 +1998,7 @@ namespace Barotrauma if (i == 0) numberInput.IntValue = value.X; else - numberInput.IntValue = value.Y; + numberInput.IntValue = value.Y; } return frame; } @@ -2028,6 +2035,16 @@ namespace Barotrauma return frame; } + public static void NotifyPrompt(string header, string body) + { + GUIMessageBox msgBox = new GUIMessageBox(header, body, new[] { TextManager.Get("Ok") }, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175)); + msgBox.Buttons[0].OnClicked = delegate + { + msgBox.Close(); + return true; + }; + } + public static GUIMessageBox AskForConfirmation(string header, string body, Action onConfirm, Action onDeny = null) { string[] buttons = { TextManager.Get("Ok"), TextManager.Get("Cancel") }; @@ -2051,6 +2068,32 @@ namespace Barotrauma return msgBox; } + public static GUIMessageBox PromptTextInput(string header, string body, Action onConfirm) + { + string[] buttons = { TextManager.Get("Ok"), TextManager.Get("Cancel") }; + GUIMessageBox msgBox = new GUIMessageBox(header, string.Empty, buttons, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175)); + GUITextBox textBox = new GUITextBox(new RectTransform(Vector2.One, msgBox.Content.RectTransform), text: body) + { + OverflowClip = true + }; + + // Cancel button + msgBox.Buttons[1].OnClicked = delegate + { + msgBox.Close(); + return true; + }; + + // Ok button + msgBox.Buttons[0].OnClicked = delegate + { + onConfirm.Invoke(textBox.Text); + msgBox.Close(); + return true; + }; + return msgBox; + } + #endregion #region Element positioning @@ -2210,7 +2253,7 @@ namespace Barotrauma if (disallowedAreas == null) { continue; } foreach (Rectangle rect2 in disallowedAreas) { - if (!rect1.Intersects(rect2)) { continue; } + if (!rect1.Intersects(rect2)) { continue; } intersections = true; Point centerDiff = rect1.Center - rect2.Center; @@ -2330,8 +2373,8 @@ namespace Barotrauma }); } - CreateButton(GameMain.GameSession.GameMode is CampaignMode ? "ReturnToServerlobby" : "EndRound", buttonContainer, - verificationTextTag: GameMain.GameSession.GameMode is CampaignMode ? "PauseMenuReturnToServerLobbyVerification" : "EndRoundSubNotAtLevelEnd", + CreateButton(GameMain.GameSession.GameMode is CampaignMode ? "ReturnToServerlobby" : "EndRound", buttonContainer, + verificationTextTag: GameMain.GameSession.GameMode is CampaignMode ? "PauseMenuReturnToServerLobbyVerification" : "EndRoundSubNotAtLevelEnd", action: () => { GameMain.Client?.RequestRoundEnd(save: false); @@ -2397,6 +2440,11 @@ namespace Barotrauma private static bool TogglePauseMenu(GUIButton button, object obj) { pauseMenuOpen = !pauseMenuOpen; + if (!pauseMenuOpen && PauseMenu != null) + { + PauseMenu.RectTransform.Parent = null; + PauseMenu = null; + } return true; } @@ -2448,7 +2496,10 @@ namespace Barotrauma public static void ClearMessages() { - messages.Clear(); + lock (mutex) + { + messages.Clear(); + } } public static bool IsFourByThree() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs index 94a503f1e..52b34ece5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs @@ -219,12 +219,6 @@ namespace Barotrauma GUI.Style.ButtonPulse.Draw(spriteBatch, expandRect, ToolBox.GradientLerp(pulseExpand, Color.White, Color.White, Color.Transparent)); } - - if (UserData is string s && s == "ReadyCheckButton" && ReadyCheck.lastReadyCheck > DateTime.Now) - { - float progress = (ReadyCheck.lastReadyCheck - DateTime.Now).Seconds / 60.0f; - Frame.Color = ToolBox.GradientLerp(progress, Color.White, GUI.Style.Red); - } } protected override void Update(float deltaTime) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs index a88ea3e89..c555ce055 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs @@ -22,13 +22,14 @@ namespace Barotrauma { GameMain.Instance.ResolutionChanged += RecalculateSize; } - _instance.ItemComponentHolder = new GUIFrame(new RectTransform(Vector2.One, _instance, Anchor.Center)).RectTransform; + _instance.ChildrenChanged += OnChildrenChanged; } return _instance; } } - public RectTransform ItemComponentHolder; + //GUICanvas stores the children as weak references, to allow elements that we no longer need to get garbage collected + private readonly List> childrenWeakRef = new List>(); private static Vector2 size => new Vector2(GameMain.GraphicsWidth / (float)GUI.UIWidth, 1f); @@ -36,16 +37,41 @@ namespace Barotrauma private enum ResizeAxis { Both = 0, X = 1, Y = 2 } + private static void OnChildrenChanged(RectTransform _) + { + //add weak reference if we don't have one yet + foreach (var child in _instance.Children) + { + if (!_instance.childrenWeakRef.Any(c => c.TryGetTarget(out var existingChild) && existingChild == child)) + { + _instance.childrenWeakRef.Add(new WeakReference(child)); + } + } + //get rid of strong references + _instance.children.Clear(); + //remove dead children + for (int i = _instance.childrenWeakRef.Count - 2; i >= 0; i--) + { + if (!_instance.childrenWeakRef[i].TryGetTarget(out var child) || child.Parent != _instance) + { + _instance.childrenWeakRef.RemoveAt(i); + } + } + } + // Turn public, if there is a need to call this manually. private static void RecalculateSize() { Vector2 recalculatedSize = size; // Scale children that are supposed to encompass the whole screen so that they are properly scaled on ultrawide as well - for (int i = 0; i < Instance.Children.Count(); i++) + for (int i = 0; i < Instance.childrenWeakRef.Count; i++) { - RectTransform target = Instance.GetChild(i); - if (target == null || target.RelativeSize.X < 1 && target.RelativeSize.Y < 1) continue; + if (!_instance.childrenWeakRef[i].TryGetTarget(out RectTransform target) || target == null) { continue; }; + + _instance.children.Add(target); + + if (target.RelativeSize.X < 1 && target.RelativeSize.Y < 1) { continue; } ResizeAxis axis; @@ -80,6 +106,7 @@ namespace Barotrauma Instance.Resize(size, resizeChildren: true); Instance.GetAllChildren().Select(c => c.GUIComponent as GUITextBlock).ForEach(t => t?.SetTextPos()); + _instance.children.Clear(); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index f03ad241a..e9ce18130 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -317,7 +317,10 @@ namespace Barotrauma set { selected = value; - Children.ForEach(c => c.Selected = value); + foreach (var child in Children) + { + child.Selected = value; + } } } public virtual ComponentState State @@ -537,7 +540,10 @@ namespace Barotrauma //would be real nice to un-jank this some day ForceUpdate(); ForceUpdate(); - foreach (var child in Children) { child.ForceLayoutRecalculation(); } + foreach (var child in Children) + { + child.ForceLayoutRecalculation(); + } } public void ForceUpdate() => Update((float)Timing.Step); @@ -547,7 +553,10 @@ namespace Barotrauma /// public void UpdateChildren(float deltaTime, bool recursive) { - RectTransform.Children.ForEach(c => c.GUIComponent.UpdateManually(deltaTime, recursive, recursive)); + foreach (var child in RectTransform.Children) + { + child.GUIComponent.UpdateManually(deltaTime, recursive, recursive); + } } #endregion @@ -583,7 +592,10 @@ namespace Barotrauma /// public virtual void DrawChildren(SpriteBatch spriteBatch, bool recursive) { - RectTransform.Children.ForEach(c => c.GUIComponent.DrawManually(spriteBatch, recursive, recursive)); + foreach (RectTransform child in RectTransform.Children) + { + child.GUIComponent.DrawManually(spriteBatch, recursive, recursive); + } } protected Color _currentColor; @@ -764,8 +776,8 @@ namespace Barotrauma { toolTipBlock = new GUITextBlock(new RectTransform(new Point(width, height), null), richTextData, toolTip, font: GUI.SmallFont, wrap: true, style: "GUIToolTip"); toolTipBlock.RectTransform.NonScaledSize = new Point( - (int)(GUI.SmallFont.MeasureString(toolTipBlock.WrappedText).X + padding.X + toolTipBlock.Padding.X + toolTipBlock.Padding.Z), - (int)(GUI.SmallFont.MeasureString(toolTipBlock.WrappedText).Y + padding.Y + toolTipBlock.Padding.Y + toolTipBlock.Padding.W)); + (int)(toolTipBlock.Font.MeasureString(toolTipBlock.WrappedText).X + padding.X + toolTipBlock.Padding.X + toolTipBlock.Padding.Z), + (int)(toolTipBlock.Font.MeasureString(toolTipBlock.WrappedText).Y + padding.Y + toolTipBlock.Padding.Y + toolTipBlock.Padding.W)); toolTipBlock.userData = toolTip; } @@ -1007,8 +1019,10 @@ namespace Barotrauma case "gridtext": LoadGridText(element, parent); return null; + case "conditional": + break; default: - throw new NotImplementedException("Loading GUI component \""+element.Name+"\" from XML is not implemented."); + throw new NotImplementedException("Loading GUI component \"" + element.Name + "\" from XML is not implemented."); } if (component != null) @@ -1079,6 +1093,29 @@ namespace Barotrauma var maxVersion = new Version(attribute.Value); if (GameMain.Version > maxVersion) { return false; } break; + case "buildconfiguration": + switch (attribute.Value.ToString().ToLowerInvariant()) + { + case "debug": +#if DEBUG + return true; +#else + break; +#endif + case "unstable": +#if UNSTABLE + return true; +#else + break; +#endif + case "release": +#if !DEBUG && !UNSTABLE + return true; +#else + break; +#endif + } + return false; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs index 7d932bcc9..92b4362c2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs @@ -362,6 +362,14 @@ namespace Barotrauma RectTransform.ScaleChanged += () => dimensionsNeedsRecalculation = true; RectTransform.SizeChanged += () => dimensionsNeedsRecalculation = true; UpdateDimensions(); + + rectT.ChildrenChanged += CheckForChildren; + } + + private void CheckForChildren(RectTransform rectT) + { + if (rectT == ScrollBar.RectTransform || rectT == Content.RectTransform || rectT == ContentBackground.RectTransform) { return; } + throw new InvalidOperationException($"Children were added to {nameof(GUIListBox)}, Add them to {nameof(GUIListBox)}.{nameof(Content)} instead."); } public void UpdateDimensions() @@ -431,7 +439,7 @@ namespace Barotrauma for (int i = 0; i < Content.CountChildren; i++) { GUIComponent child = Content.GetChild(i); - if (!child.Visible) { continue; } + if (child == null || !child.Visible) { continue; } if (RectTransform != null) { callback(i, new Point(x, y)); @@ -837,7 +845,6 @@ namespace Barotrauma UpdateScrollBarSize(); } - if (FadeElements) { foreach (var (component, _) in childVisible) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs index 232c13c6a..4ec5929f3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs @@ -319,28 +319,29 @@ namespace Barotrauma AbsoluteSpacing = absoluteSpacing.Y, }; - var bottomContainer = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.3f), verticalLayoutGroup.RectTransform), style: null); - - var tickBoxLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.67f, 1.0f), bottomContainer.RectTransform, anchor: Anchor.CenterLeft), - isHorizontal: true, childAnchor: Anchor.CenterLeft) + var bottomContainer = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.3f), verticalLayoutGroup.RectTransform), style: null) { - Stretch = true, - RelativeSpacing = 0.02f + CanBeFocused = true }; - var dontShowAgainTickBox = new GUITickBox(new RectTransform(new Vector2(0.5f, 1.0f), tickBoxLayoutGroup.RectTransform), + var tickBoxLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.67f, 1.0f), bottomContainer.RectTransform, anchor: Anchor.CenterLeft)) + { + CanBeFocused = true, + Stretch = true + }; + Vector2 tickBoxRelativeSize = new Vector2(1.0f, 0.5f); + var dontShowAgainTickBox = new GUITickBox(new RectTransform(tickBoxRelativeSize, tickBoxLayoutGroup.RectTransform), TextManager.Get("hintmessagebox.dontshowagain")) { ToolTip = TextManager.Get("hintmessagebox.dontshowagaintooltip"), UserData = "dontshowagain" }; - - //var disableHintsTickBox = new GUITickBox(new RectTransform(new Vector2(0.33f, 1.0f), tickBoxLayoutGroup.RectTransform), - // TextManager.Get("hintmessagebox.disablehints")) - //{ - // ToolTip = TextManager.Get("hintmessagebox.disablehintstooltip"), - // UserData = "disablehints" - //}; + var disableHintsTickBox = new GUITickBox(new RectTransform(tickBoxRelativeSize, tickBoxLayoutGroup.RectTransform), + TextManager.Get("hintmessagebox.disablehints")) + { + ToolTip = TextManager.Get("hintmessagebox.disablehintstooltip"), + UserData = "disablehints" + }; Buttons = new List(1) { @@ -379,12 +380,16 @@ namespace Barotrauma upperContainerHeight = Math.Max(upperContainerHeight, Icon.Rect.Height); height += upperContainerHeight; height += absoluteSpacing.Y; - height += (int)((bottomContainer.RectTransform.RelativeSize.Y / topHorizontalLayoutGroup.RectTransform.RelativeSize.Y) * upperContainerHeight); + int bottomContainerHeight = dontShowAgainTickBox.Rect.Height + disableHintsTickBox.Rect.Height; + height += bottomContainerHeight; height += absoluteSpacing.Y; if (minSize.HasValue) { height = Math.Max(height, minSize.Value.Y); } InnerFrame.RectTransform.NonScaledSize = new Point(InnerFrame.Rect.Width, height); verticalLayoutGroup.RectTransform.NonScaledSize = GetVerticalLayoutGroupSize(); + float upperContainerRelativeHeight = (float)upperContainerHeight / (upperContainerHeight + bottomContainerHeight); + topHorizontalLayoutGroup.RectTransform.RelativeSize = new Vector2(topHorizontalLayoutGroup.RectTransform.RelativeSize.X, upperContainerRelativeHeight); + bottomContainer.RectTransform.RelativeSize = new Vector2(bottomContainer.RectTransform.RelativeSize.X, 1.0f - upperContainerRelativeHeight); verticalLayoutGroup.Recalculate(); topHorizontalLayoutGroup.Recalculate(); Content.Recalculate(); @@ -613,6 +618,7 @@ namespace Barotrauma public bool Close(GUIButton button, object obj) { + RectTransform.Parent = null; Close(); return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScissorComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScissorComponent.cs index 07119e38a..3969a3db3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScissorComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScissorComponent.cs @@ -29,6 +29,12 @@ namespace Barotrauma ClampChildMouseRects(Content); } + public override void DrawChildren(SpriteBatch spriteBatch, bool recursive) + { + //do nothing (the children have to be drawn in the Draw method after the ScissorRectangle has been set) + return; + } + protected override void Draw(SpriteBatch spriteBatch) { if (!Visible) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs index 6dc1d5f03..031ec550b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs @@ -204,6 +204,12 @@ namespace Barotrauma set { textColor = value; } } + public Color DisabledTextColor + { + get => disabledTextColor; + set => disabledTextColor = value; + } + private Color? hoverTextColor; public Color HoverTextColor { @@ -303,6 +309,10 @@ namespace Barotrauma if (parseRichText) { RichTextData = Barotrauma.RichTextData.GetRichTextData(text, out text); + if (RichTextData != null && RichTextData.Count == 0) + { + RichTextData = null; + } } //if the text is in chinese/korean/japanese and we're not using a CJK-compatible font, @@ -457,7 +467,7 @@ namespace Barotrauma while (size == Vector2.Zero) { try { size = Font.MeasureString(string.IsNullOrEmpty(text) ? " " : text); } - catch { text = text.Substring(0, text.Length - 1); } + catch { text = text.Length > 0 ? text.Substring(0, text.Length - 1) : ""; } } return size; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index f3e9a7513..22baf0be1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -80,6 +80,8 @@ namespace Barotrauma private Vector2 selectionEndPos; private Vector2 selectionRectSize; + private GUICustomComponent caretAndSelectionRenderer; + private bool mouseHeldInside; private readonly Memento memento = new Memento(); @@ -188,8 +190,7 @@ namespace Barotrauma } set { - base.ToolTip = value; - textBlock.ToolTip = value; + base.ToolTip = textBlock.ToolTip = caretAndSelectionRenderer.ToolTip = value; } } @@ -278,7 +279,7 @@ namespace Barotrauma CaretEnabled = true; caretPosDirty = true; - new GUICustomComponent(new RectTransform(Vector2.One, frame.RectTransform), onDraw: DrawCaretAndSelection); + caretAndSelectionRenderer = new GUICustomComponent(new RectTransform(Vector2.One, frame.RectTransform), onDraw: DrawCaretAndSelection); int clearButtonWidth = 0; if (createClearButton) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs new file mode 100644 index 000000000..4596b3c7e --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs @@ -0,0 +1,1063 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + [SuppressMessage("ReSharper", "UnusedVariable")] + internal class MedicalClinicUI + { + private enum ElementState + { + Enabled, + Disabled + } + + // Represents a pending affliction in the right side pending heal list + private struct PendingAfflictionElement + { + public readonly GUIComponent UIElement; + public readonly MedicalClinic.NetAffliction Target; + public readonly GUITextBlock Price; + + public PendingAfflictionElement(MedicalClinic.NetAffliction target, GUIComponent element, GUITextBlock price) + { + UIElement = element; + Target = target; + Price = price; + } + } + + // Represents a pending heal on the right side list + private struct PendingHealElement + { + public readonly GUIComponent UIElement; + public MedicalClinic.NetCrewMember Target; + public readonly GUIListBox AfflictionList; + public readonly List Afflictions; + + public PendingHealElement(MedicalClinic.NetCrewMember target, GUIComponent element, GUIListBox afflictionList) + { + UIElement = element; + Target = target; + AfflictionList = afflictionList; + Afflictions = new List(); + } + + public PendingAfflictionElement? FindAfflictionElement(MedicalClinic.NetAffliction target) => Afflictions.FirstOrNull(element => element.Target.Identifier.Equals(target.Identifier, StringComparison.OrdinalIgnoreCase)); + } + + // Represents an affliction on the left side crew entry + private readonly struct AfflictionElement + { + public readonly GUIImage? UIImage; + public readonly GUIComponent UIElement; + public readonly MedicalClinic.NetAffliction Target; + + public AfflictionElement(MedicalClinic.NetAffliction target, GUIComponent element, GUIImage? icon) + { + UIElement = element; + UIImage = icon; + Target = target; + } + } + + // Represent an entry on the left side crew list + private readonly struct CrewElement + { + public readonly GUIComponent UIElement; + public readonly CharacterInfo Target; + public readonly GUIListBox AfflictionList; + public readonly List Afflictions; + public readonly GUIComponent OverflowIndicator; + + public CrewElement(CharacterInfo target, GUIComponent overflowIndicator, GUIComponent element, GUIListBox afflictionList) + { + OverflowIndicator = overflowIndicator; + UIElement = element; + Target = target; + AfflictionList = afflictionList; + Afflictions = new List(); + } + } + + // Represents the right side pending list + private readonly struct PendingHealList + { + public readonly GUIListBox HealList; + public readonly GUITextBlock? ErrorBlock; + public readonly GUITextBlock PriceBlock; + public readonly List HealElements; + public readonly GUIButton HealButton; + + public PendingHealList(GUIListBox healList, GUITextBlock priceBlock, GUIButton healButton, GUITextBlock? errorBlock) + { + HealList = healList; + ErrorBlock = errorBlock; + PriceBlock = priceBlock; + HealButton = healButton; + HealElements = new List(); + } + + public void UpdateElement(PendingHealElement newElement) + { + foreach (PendingHealElement element in HealElements.ToList()) + { + if (element.Target.CharacterEquals(newElement.Target)) + { + HealElements.Remove(element); + HealElements.Add(newElement); + return; + } + } + } + + public PendingHealElement? FindCrewElement(MedicalClinic.NetCrewMember crewMember) => HealElements.FirstOrNull(element => element.Target.CharacterInfoID == crewMember.CharacterInfoID); + } + + // Represents the left side crew list + private readonly struct CrewHealList + { + public readonly GUIComponent Panel; + public readonly GUIListBox HealList; + public readonly List HealElements; + + public CrewHealList(GUIListBox healList, GUIComponent panel) + { + Panel = panel; + HealList = healList; + HealElements = new List(); + } + } + + private readonly struct PopupAffliction + { + public readonly MedicalClinic.NetAffliction Target; + public readonly ImmutableArray ElementsToDisable; + + public PopupAffliction(ImmutableArray elementsToDisable, MedicalClinic.NetAffliction target) + { + Target = target; + ElementsToDisable = elementsToDisable; + } + } + + private readonly struct PopupAfflictionList + { + public readonly MedicalClinic.NetCrewMember Target; + public readonly GUIButton TreatAllButton; + public readonly List Afflictions; + + public PopupAfflictionList(MedicalClinic.NetCrewMember crewMember, GUIButton treatAllButton) + { + Target = crewMember; + TreatAllButton = treatAllButton; + Afflictions = new List(); + } + } + + // private enum SortMode + // { + // Severity + // } + + private readonly MedicalClinic medicalClinic; + private readonly GUIComponent container; + private Point prevResolution; + + private PendingHealList? pendingHealList; + private CrewHealList? crewHealList; + + private GUIFrame? selectedCrewElement; + private PopupAfflictionList? selectedCrewAfflictionList; + private bool isWaitingForServer; + private const float refreshTimerMax = 3f; + private float refreshTimer = 0; + + public MedicalClinicUI(MedicalClinic clinic, GUIComponent parent) + { + medicalClinic = clinic; + container = parent; + clinic.OnUpdate = OnMedicalClinicUpdated; + +#if DEBUG + // creates a button that re-creates the UI + CreateRefreshButton(); + void CreateRefreshButton() + { + new GUIButton(new RectTransform(new Vector2(0.2f, 0.1f), parent.RectTransform, Anchor.TopCenter), "Recreate UI - NOT PRESENT IN RELEASE!") + { + OnClicked = (_, __) => + { + parent.ClearChildren(); + CreateUI(); + CreateRefreshButton(); + RequestLatestPending(); + return true; + } + }; + } +#endif + CreateUI(); + } + + private void OnMedicalClinicUpdated() + { + UpdateCrewPanel(); + UpdatePending(); + UpdatePopupAfflictions(); + } + + private void UpdatePopupAfflictions() + { + if (selectedCrewAfflictionList is { } afflictionList) + { + foreach (PopupAffliction popupAffliction in afflictionList.Afflictions) + { + ToggleElements(ElementState.Enabled, popupAffliction.ElementsToDisable); + if (medicalClinic.IsAfflictionPending(afflictionList.Target, popupAffliction.Target)) + { + ToggleElements(ElementState.Disabled, popupAffliction.ElementsToDisable); + } + } + + afflictionList.TreatAllButton.Enabled = true; + if (afflictionList.Afflictions.All(a => medicalClinic.IsAfflictionPending(afflictionList.Target, a.Target))) + { + afflictionList.TreatAllButton.Enabled = false; + } + } + } + + private void UpdatePending() + { + if (!(pendingHealList is { } healList)) { return; } + + ImmutableArray pendingList = medicalClinic.PendingHeals.ToImmutableArray(); + + // check if there are crew members that are not in the UI + foreach (MedicalClinic.NetCrewMember crewMember in pendingList) + { + if (healList.FindCrewElement(crewMember) is { } element) + { + element.Target = crewMember; + healList.UpdateElement(element); + continue; + } + + CreatePendingHealElement(healList.HealList.Content, crewMember, healList, Array.Empty()); + } + + // check if there are elements that the crew doesn't have + foreach (PendingHealElement element in healList.HealElements.ToList()) + { + if (pendingList.Any(member => member.CharacterEquals(element.Target))) + { + UpdatePendingAfflictions(element); + continue; + } + + healList.HealElements.Remove(element); + healList.HealList.Content.RemoveChild(element.UIElement); + } + + int totalCost = medicalClinic.GetTotalCost(); + healList.PriceBlock.Text = UpgradeStore.FormatCurrency(totalCost); + healList.PriceBlock.TextColor = GUI.Style.Red; + healList.HealButton.Enabled = false; + if (medicalClinic.GetMoney() > totalCost) + { + healList.PriceBlock.TextColor = GUI.Style.TextColor; + if (medicalClinic.PendingHeals.Any()) + { + healList.HealButton.Enabled = true; + } + } + } + + private void UpdatePendingAfflictions(PendingHealElement element) + { + MedicalClinic.NetCrewMember crewMember = element.Target; + foreach (MedicalClinic.NetAffliction affliction in crewMember.Afflictions.ToList()) + { + if (element.FindAfflictionElement(affliction) is { } existingAffliction) + { + existingAffliction.Price.Text = UpgradeStore.FormatCurrency(affliction.Strength); + continue; + } + + CreatePendingAffliction(element.AfflictionList, crewMember, affliction, element); + } + + foreach (PendingAfflictionElement afflictionElement in element.Afflictions.ToList()) + { + if (crewMember.Afflictions.Any(affliction => affliction.AfflictionEquals(afflictionElement.Target))) { continue; } + + element.Afflictions.Remove(afflictionElement); + element.AfflictionList.Content.RemoveChild(afflictionElement.UIElement); + } + } + + private void UpdateCrewPanel() + { + if (!(crewHealList is { } healList)) { return; } + + ImmutableArray crew = MedicalClinic.GetCrewCharacters(); + + // check if there are crew members that are not in the UI + foreach (CharacterInfo info in crew) + { + if (healList.HealElements.Any(element => element.Target == info)) { continue; } + + CreateCrewEntry(healList.HealList.Content, healList, info, healList.Panel); + } + + // check if there are elements that the crew doesn't have + foreach (CrewElement element in healList.HealElements.ToList()) + { + if (crew.Any(info => element.Target == info)) + { + UpdateAfflictionList(element); + continue; + } + + healList.HealElements.Remove(element); + healList.HealList.Content.RemoveChild(element.UIElement); + } + + IEnumerable orderedList = healList.HealElements.OrderBy(element => element.Target.Character?.HealthPercentage ?? 100); + + foreach (CrewElement element in orderedList) + { + element.UIElement.SetAsLastChild(); + } + } + + private static void UpdateAfflictionList(CrewElement healElement) + { + CharacterHealth? health = healElement.Target.Character?.CharacterHealth; + if (health is null) { return; } + + // sum up all the afflictions and their strengths + Dictionary afflictionAndStrength = new Dictionary(); + + foreach (Affliction affliction in health.GetAllAfflictions().Where(a => MedicalClinic.IsHealable(a))) + { + if (afflictionAndStrength.TryGetValue(affliction.Prefab, out float strength)) + { + strength += affliction.Strength; + afflictionAndStrength[affliction.Prefab] = strength; + continue; + } + + afflictionAndStrength.Add(affliction.Prefab, affliction.Strength); + } + + // hide all the elements because we only want to show 3 later on + foreach (AfflictionElement element in healElement.Afflictions) + { + element.UIElement.Visible = false; + } + + healElement.OverflowIndicator.Visible = false; + + foreach (var (prefab, strength) in afflictionAndStrength) + { + bool found = false; + foreach (AfflictionElement existingElement in healElement.Afflictions) + { + if (!existingElement.Target.AfflictionEquals(prefab)) { continue; } + + if (existingElement.UIImage is { } icon) + { + icon.Color = CharacterHealth.GetAfflictionIconColor(prefab, strength); + } + + found = true; + } + + if (found) { continue; } + + CreateCrewAfflictionIcon(healElement, healElement.AfflictionList.Content, prefab, strength); + } + + foreach (AfflictionElement element in healElement.Afflictions.ToList()) + { + if (afflictionAndStrength.Any(pair => element.Target.AfflictionEquals(pair.Key))) { continue; } + + healElement.AfflictionList.Content.RemoveChild(element.UIElement); + healElement.Afflictions.Remove(element); + } + + for (int i = 0; i < 3 && i < healElement.Afflictions.Count; i++) + { + healElement.Afflictions[i].UIElement.Visible = true; + } + + healElement.OverflowIndicator.Visible = healElement.Afflictions.Count > 3; + healElement.OverflowIndicator.SetAsLastChild(); + } + + private static void CreateCrewAfflictionIcon(CrewElement healElement, GUIComponent parent, AfflictionPrefab prefab, float strength) + { + GUIFrame backgroundFrame = new GUIFrame(new RectTransform(new Vector2(0.25f, 1f), parent.RectTransform), style: null) + { + CanBeFocused = false, + Visible = false + }; + + GUIImage? uiIcon = null; + if (prefab.Icon is { } icon) + { + uiIcon = new GUIImage(new RectTransform(Vector2.One, backgroundFrame.RectTransform), icon, scaleToFit: true) + { + Color = CharacterHealth.GetAfflictionIconColor(prefab, strength) + }; + } + + healElement.Afflictions.Add(new AfflictionElement(new MedicalClinic.NetAffliction { Prefab = prefab }, backgroundFrame, uiIcon)); + } + + private void CreateUI() + { + container.ClearChildren(); + pendingHealList = null; + int panelMaxWidth = (int)(GUI.xScale * (GUI.HorizontalAspectRatio < 1.4f ? 650 : 560)); + + GUIFrame paddedParent = new GUIFrame(new RectTransform(new Vector2(0.95f), container.RectTransform, Anchor.Center), style: null); + + GUILayoutGroup clinicContent = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 1.0f), paddedParent.RectTransform) + { + MaxSize = new Point(panelMaxWidth, container.Rect.Height) + }) + { + Stretch = true, + RelativeSpacing = 0.01f + }; + + GUILayoutGroup clinicLabelLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), clinicContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + GUIImage clinicIcon = new GUIImage(new RectTransform(Vector2.One, clinicLabelLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "CrewManagementHeaderIcon", scaleToFit: true); + GUITextBlock clinicLabel = new GUITextBlock(new RectTransform(Vector2.One, clinicLabelLayout.RectTransform), TextManager.Get("medicalclinic.medicalclinic"), font: GUI.LargeFont); + + GUIFrame clinicBackground = new GUIFrame(new RectTransform(Vector2.One, clinicContent.RectTransform)); + + CreateLeftSidePanel(clinicBackground); + + GUILayoutGroup crewContent = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 1.0f), paddedParent.RectTransform, anchor: Anchor.TopRight) + { + MaxSize = new Point(panelMaxWidth, container.Rect.Height) + }) + { + Stretch = true, + RelativeSpacing = 0.01f + }; + + GUILayoutGroup balanceLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), crewContent.RectTransform)); + GUITextBlock balanceLabel = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), balanceLayout.RectTransform), TextManager.Get("campaignstore.balance"), textAlignment: Alignment.BottomRight, font: GUI.Font) + { + AutoScaleVertical = true, + ForceUpperCase = true + }; + + GUITextBlock moneyLabel = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), balanceLayout.RectTransform), string.Empty, textAlignment: Alignment.TopRight, font: GUI.Style.SubHeadingFont) + { + TextGetter = () => UpgradeStore.FormatCurrency(medicalClinic.GetMoney()), + AutoScaleVertical = true, + TextScale = 1.1f + }; + + GUIFrame crewBackground = new GUIFrame(new RectTransform(Vector2.One, crewContent.RectTransform)); + + CreateRightSidePanel(crewBackground); + + prevResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); + } + + private void CreateLeftSidePanel(GUIComponent parent) + { + crewHealList = null; + GUILayoutGroup clinicContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), parent.RectTransform, Anchor.Center)) + { + RelativeSpacing = 0.015f, + Stretch = true + }; + + // GUILayoutGroup sortLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.05f), clinicContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + + // new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), sortLayout.RectTransform), TextManager.Get("campaignstore.sortby"), font: GUI.SubHeadingFont); + + // GUIDropDown sortDropdown = new GUIDropDown(new RectTransform(new Vector2(0.3f, 1f), sortLayout.RectTransform)); + // + // foreach (SortMode mode in Enum.GetValues(typeof(SortMode)).Cast()) + // { + // sortDropdown.AddItem(TextManager.Get($"medicalclinic.sortmode.{mode}"), mode); + // } + // + // sortDropdown.SelectItem(SortMode.Severity); + + GUIListBox crewList = new GUIListBox(new RectTransform(Vector2.One, clinicContainer.RectTransform)); + + crewHealList = new CrewHealList(crewList, parent); + } + + private void CreateCrewEntry(GUIComponent parent, CrewHealList healList, CharacterInfo info, GUIComponent panel) + { + GUIButton crewBackground = new GUIButton(new RectTransform(new Vector2(1f, 0.1f), parent.RectTransform), style: "ListBoxElement"); + + GUILayoutGroup crewLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f), crewBackground.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft); + + GUILayoutGroup characterBlockLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 0.9f), crewLayout.RectTransform), isHorizontal: true, Anchor.CenterLeft); + CreateCharacterBlock(characterBlockLayout, info); + + GUIListBox afflictionList = new GUIListBox(new RectTransform(new Vector2(0.45f, 1f), crewLayout.RectTransform), style: null, isHorizontal: true); + + GUILayoutGroup healthLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.1f, 1f), crewLayout.RectTransform), isHorizontal: true, Anchor.Center); + + new GUITextBlock(new RectTransform(Vector2.One, healthLayout.RectTransform), string.Empty, textAlignment: Alignment.Center, font: GUI.SubHeadingFont) + { + TextGetter = () => $"{(int)(info.Character?.HealthPercentage ?? 100f)}%", + TextColor = GUI.Style.Green + }; + + GUITextBlock overflowIndicator = + new GUITextBlock(new RectTransform(new Vector2(0.25f, 1f), afflictionList.Content.RectTransform, scaleBasis: ScaleBasis.BothHeight), text: "+", textAlignment: Alignment.Center, font: GUI.LargeFont) + { + Visible = false, + CanBeFocused = false, + TextColor = GUI.Style.Red + }; + + MedicalClinic.NetCrewMember member = new MedicalClinic.NetCrewMember { CharacterInfo = info, Afflictions = Array.Empty() }; + + crewBackground.OnClicked = (_, __) => + { + SelectCharacter(member, new Vector2(panel.Rect.Right, crewBackground.Rect.Top)); + return true; + }; + + healList.HealElements.Add(new CrewElement(info, overflowIndicator, crewBackground, afflictionList)); + } + + private void CreateRightSidePanel(GUIComponent parent) + { + GUILayoutGroup pendingHealContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), parent.RectTransform, anchor: Anchor.Center)) + { + RelativeSpacing = 0.015f, + Stretch = true + }; + + new GUITextBlock(new RectTransform(new Vector2(1f, 0.05f), pendingHealContainer.RectTransform), TextManager.Get("medicalclinic.pendingheals"), font: GUI.SubHeadingFont); + + GUIFrame healListContainer = new GUIFrame(new RectTransform(new Vector2(1f, 0.9f), pendingHealContainer.RectTransform), style: null); + GUITextBlock? errorBlock = null; + if (!GameMain.IsSingleplayer) + { + errorBlock = new GUITextBlock(new RectTransform(Vector2.One, healListContainer.RectTransform), text: TextManager.Get("pleasewaitupnp"), font: GUI.LargeFont, textAlignment: Alignment.Center); + } + + GUIListBox healList = new GUIListBox(new RectTransform(Vector2.One, healListContainer.RectTransform)) + { + Spacing = GUI.IntScale(8), + Visible = GameMain.IsSingleplayer + }; + + GUILayoutGroup footerLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), pendingHealContainer.RectTransform)); + + GUILayoutGroup priceLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), footerLayout.RectTransform), isHorizontal: true); + GUITextBlock priceLabelBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), priceLayout.RectTransform), TextManager.Get("campaignstore.total")); + GUITextBlock priceBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), priceLayout.RectTransform), UpgradeStore.FormatCurrency(medicalClinic.GetTotalCost()), font: GUI.SubHeadingFont, + textAlignment: Alignment.Right); + + 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(), + OnClicked = (button, _) => + { + button.Enabled = false; + medicalClinic.HealAllButtonAction(request => + { + switch (request.HealResult) + { + case MedicalClinic.HealRequestResult.InsufficientFunds: + GUI.NotifyPrompt(TextManager.Get("medicalclinic.unabletoheal"), TextManager.Get("medicalclinic.insufficientfunds")); + break; + case MedicalClinic.HealRequestResult.Refused: + GUI.NotifyPrompt(TextManager.Get("medicalclinic.unabletoheal"), TextManager.Get("medicalclinic.healrefused")); + break; + } + + button.Enabled = true; + ClosePopup(); + }); + ClosePopup(); + return true; + } + }; + + GUIButton clearButton = new GUIButton(new RectTransform(new Vector2(0.33f, 1f), buttonLayout.RectTransform), TextManager.Get("campaignstore.clearall")) + { + OnClicked = (button, _) => + { + button.Enabled = false; + medicalClinic.ClearAllButtonAction(_ => + { + button.Enabled = true; + }); + return true; + } + }; + + PendingHealList list = new PendingHealList(healList, priceBlock, healButton, errorBlock); + + foreach (MedicalClinic.NetCrewMember heal in GetPendingCharacters()) + { + CreatePendingHealElement(healList.Content, heal, list, heal.Afflictions); + } + + pendingHealList = list; + } + + private void CreatePendingHealElement(GUIComponent parent, MedicalClinic.NetCrewMember crewMember, PendingHealList healList, MedicalClinic.NetAffliction[] afflictions) + { + CharacterInfo? healInfo = crewMember.FindCharacterInfo(MedicalClinic.GetCrewCharacters()); + if (healInfo is null) { return; } + + GUIFrame pendingHealBackground = new GUIFrame(new RectTransform(new Vector2(1f, 0.25f), parent.RectTransform), style: "ListBoxElement") + { + CanBeFocused = false + }; + GUILayoutGroup pendingHealLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f), pendingHealBackground.RectTransform, Anchor.Center)); + + GUILayoutGroup topHeaderLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.3f), pendingHealLayout.RectTransform), isHorizontal: true, Anchor.CenterLeft) { Stretch = true }; + + CreateCharacterBlock(topHeaderLayout, healInfo); + + GUILayoutGroup bottomLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.7f), pendingHealLayout.RectTransform), childAnchor: Anchor.Center); + + GUIListBox pendingAfflictionList = new GUIListBox(new RectTransform(Vector2.One, bottomLayout.RectTransform)) + { + AutoHideScrollBar = false, + ScrollBarVisible = true + }; + + PendingHealElement healElement = new PendingHealElement(crewMember, pendingHealBackground, pendingAfflictionList); + + foreach (MedicalClinic.NetAffliction affliction in afflictions) + { + CreatePendingAffliction(pendingAfflictionList, crewMember, affliction, healElement); + } + + healList.HealElements.Add(healElement); + RecalculateLayouts(pendingHealLayout, topHeaderLayout, bottomLayout); + pendingAfflictionList.ForceUpdate(); + } + + private void CreatePendingAffliction(GUIListBox parent, MedicalClinic.NetCrewMember crewMember, MedicalClinic.NetAffliction affliction, PendingHealElement healElement) + { + GUIFrame backgroundFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.33f), parent.Content.RectTransform), style: "ListBoxElement") + { + CanBeFocused = false + }; + GUILayoutGroup parentLayout = new GUILayoutGroup(new RectTransform(Vector2.One, backgroundFrame.RectTransform), isHorizontal: true) { Stretch = true }; + + if (!(affliction.Prefab is { } prefab)) { return; } + + if (prefab.Icon is { } icon) + { + new GUIImage(new RectTransform(Vector2.One, parentLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), icon, scaleToFit: true) + { + Color = CharacterHealth.GetAfflictionIconColor(prefab, affliction.Strength) + }; + } + + GUILayoutGroup textLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parentLayout.RectTransform), isHorizontal: true); + + string name = prefab.Name; + + GUIFrame textContainer = new GUIFrame(new RectTransform(new Vector2(0.6f, 1f), textLayout.RectTransform), style: null); + GUITextBlock afflictionName = new GUITextBlock(new RectTransform(Vector2.One, textContainer.RectTransform), name, font: GUI.SubHeadingFont); + + GUITextBlock healCost = new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), textLayout.RectTransform), UpgradeStore.FormatCurrency(affliction.Price), textAlignment: Alignment.Center, font: GUI.LargeFont) + { + Padding = Vector4.Zero + }; + + GUIButton healButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), textLayout.RectTransform), style: "CrewManagementRemoveButton") + { + OnClicked = (button, _) => + { + button.Enabled = false; + medicalClinic.RemovePendingButtonAction(crewMember, affliction, _ => + { + button.Enabled = true; + }); + return true; + } + }; + + EnsureTextDoesntOverflow(name, afflictionName, textContainer.Rect, ImmutableArray.Create(textLayout, parentLayout)); + + healElement.Afflictions.Add(new PendingAfflictionElement(affliction, backgroundFrame, healCost)); + + RecalculateLayouts(parentLayout, textLayout); + + parent.ForceUpdate(); + } + + private static void CreateCharacterBlock(GUIComponent parent, CharacterInfo info) + { + new GUICustomComponent(new RectTransform(Vector2.One, parent.RectTransform, scaleBasis: ScaleBasis.BothHeight), (spriteBatch, component) => + { + info.DrawPortrait(spriteBatch, component.Rect.Location.ToVector2(), Vector2.Zero, component.Rect.Width); + }); + + GUILayoutGroup textGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.8f), parent.RectTransform)); + + string? characterName = info.Name, + jobName = null; + + GUITextBlock? nameBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), textGroup.RectTransform), characterName), + jobBlock = null; + + if (info.Job is { Name: { } name, Prefab: { UIColor: var color} } job) + { + jobName = name; + jobBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), textGroup.RectTransform), jobName); + nameBlock.TextColor = color; + } + + if (parent is GUILayoutGroup layoutGroup) + { + ImmutableArray layoutGroups = ImmutableArray.Create(layoutGroup, textGroup); + + EnsureTextDoesntOverflow(characterName, nameBlock, parent.Rect, layoutGroups); + + if (jobBlock is null) { return; } + + EnsureTextDoesntOverflow(jobName, jobBlock, parent.Rect, layoutGroups); + } + } + + private void SelectCharacter(MedicalClinic.NetCrewMember crewMember, Vector2 location) + { + CharacterInfo? info = crewMember.FindCharacterInfo(MedicalClinic.GetCrewCharacters()); + if (info is null) { return; } + + if (isWaitingForServer) { return; } + + ClosePopup(); + + GUIFrame mainFrame = new GUIFrame(new RectTransform(new Vector2(0.28f, 0.45f), container.RectTransform) + { + ScreenSpaceOffset = location.ToPoint() + }); + + GUILayoutGroup mainLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f), mainFrame.RectTransform, Anchor.Center)) { RelativeSpacing = 0.01f, Stretch = true }; + + if (mainFrame.Rect.Bottom > GameMain.GraphicsHeight) + { + mainFrame.RectTransform.ScreenSpaceOffset = new Point((int)location.X, GameMain.GraphicsHeight - mainFrame.Rect.Height); + } + + GUITextBlock feedbackBlock = new GUITextBlock(new RectTransform(Vector2.One, mainFrame.RectTransform), TextManager.Get("pleasewaitupnp"), textAlignment: Alignment.Center, font: GUI.LargeFont, wrap: true) + { + Visible = true + }; + + GUIButton treatAllButton = new GUIButton(new RectTransform(new Vector2(1f, 0.2f), mainLayout.RectTransform), TextManager.Get("medicalclinic.treatall")) + { + Font = GUI.SubHeadingFont, + Visible = false + }; + + GUIListBox afflictionList = new GUIListBox(new RectTransform(new Vector2(1f, 0.8f), mainLayout.RectTransform)) { Visible = false }; + + PopupAfflictionList popupAfflictionList = new PopupAfflictionList(crewMember, treatAllButton); + selectedCrewElement = mainFrame; + selectedCrewAfflictionList = popupAfflictionList; + + isWaitingForServer = true; + medicalClinic.RequestAfflictions(info, OnReceived); + + void OnReceived(MedicalClinic.AfflictionRequest request) + { + isWaitingForServer = false; + + if (request.Result != MedicalClinic.RequestResult.Success) + { + feedbackBlock.Text = GetErrorText(request.Result); + feedbackBlock.TextColor = GUI.Style.Red; + return; + } + + List allComponents = new List(); + foreach (MedicalClinic.NetAffliction affliction in request.Afflictions) + { + ImmutableArray createdComponents = CreatePopupAffliction(afflictionList.Content, crewMember, affliction); + allComponents.AddRange(createdComponents); + popupAfflictionList.Afflictions.Add(new PopupAffliction(createdComponents, affliction)); + } + + allComponents.Add(treatAllButton); + treatAllButton.OnClicked = (_, __) => + { + ImmutableArray afflictions = request.Afflictions.Where(a => !medicalClinic.IsAfflictionPending(crewMember, a)).ToImmutableArray(); + if (!afflictions.Any()) { return true; } + + AddPending(allComponents.ToImmutableArray(), crewMember, afflictions); + return true; + }; + + afflictionList.Visible = true; + feedbackBlock.Visible = false; + treatAllButton.Visible = true; + UpdatePopupAfflictions(); + } + } + + private ImmutableArray CreatePopupAffliction(GUIComponent parent, MedicalClinic.NetCrewMember crewMember, MedicalClinic.NetAffliction affliction) + { + if (!(affliction.Prefab is { } prefab)) { return ImmutableArray.Empty; } + + GUIFrame backgroundFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.33f), parent.RectTransform), style: "ListBoxElement"); + GUILayoutGroup mainLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f), backgroundFrame.RectTransform, Anchor.Center)) + { + RelativeSpacing = 0.05f + }; + + GUILayoutGroup topLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.33f), mainLayout.RectTransform), isHorizontal: true) { Stretch = true }; + + Color iconColor = CharacterHealth.GetAfflictionIconColor(prefab, affliction.Strength); + + GUIImage icon = new GUIImage(new RectTransform(Vector2.One, topLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), prefab.Icon, scaleToFit: true) + { + Color = iconColor, + DisabledColor = iconColor * 0.5f + }; + + GUILayoutGroup topTextLayout = new GUILayoutGroup(new RectTransform(Vector2.One, topLayout.RectTransform), isHorizontal: true); + + GUITextBlock prefabBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), topTextLayout.RectTransform), prefab.Name, font: GUI.SubHeadingFont); + + Color textColor = Color.Lerp(GUI.Style.Orange, GUI.Style.Red, (int)affliction.AfflictionSeverity / 2f); + + string vitalityText = TextManager.GetWithVariable("medicalclinic.vitalitydifference", "[amount]", (-affliction.Strength).ToString()); + GUITextBlock vitalityBlock = new GUITextBlock(new RectTransform(new Vector2(0.25f, 1f), topTextLayout.RectTransform), vitalityText, textAlignment: Alignment.Center) + { + TextColor = textColor, + DisabledTextColor = textColor * 0.5f, + Padding = Vector4.Zero, + AutoScaleHorizontal = true + }; + + string severityText = TextManager.Get($"AfflictionStrength{affliction.AfflictionSeverity}"); + GUITextBlock severityBlock = new GUITextBlock(new RectTransform(new Vector2(0.25f, 1f), topTextLayout.RectTransform), severityText, textAlignment: Alignment.Center, font: GUI.SubHeadingFont) + { + TextColor = textColor, + DisabledTextColor = textColor * 0.5f, + Padding = Vector4.Zero, + AutoScaleHorizontal = true + }; + + EnsureTextDoesntOverflow(prefab.Name, prefabBlock, prefabBlock.Rect, ImmutableArray.Create(mainLayout, topLayout, topTextLayout)); + + GUILayoutGroup bottomLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.66f), mainLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + + GUILayoutGroup bottomTextLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 1f), bottomLayout.RectTransform)); + GUITextBlock descriptionBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), bottomTextLayout.RectTransform), ToolBox.LimitString(prefab.Description, GUI.IntScale(64)), wrap: true) + { + ToolTip = prefab.Description + }; + + GUITextBlock priceBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), bottomTextLayout.RectTransform), UpgradeStore.FormatCurrency(affliction.Price), font: GUI.LargeFont); + + GUIButton buyButton = new GUIButton(new RectTransform(new Vector2(0.2f, 0.75f), bottomLayout.RectTransform), style: "CrewManagementAddButton"); + + ImmutableArray elementsToDisable = ImmutableArray.Create(prefabBlock, backgroundFrame, icon, vitalityBlock, severityBlock, buyButton, descriptionBlock, priceBlock); + + buyButton.OnClicked = (_, __) => + { + if (!buyButton.Enabled) { return false; } + + AddPending(elementsToDisable, crewMember, ImmutableArray.Create(affliction)); + return true; + }; + + return elementsToDisable; + } + + private void AddPending(ImmutableArray elementsToDisable, MedicalClinic.NetCrewMember crewMember, ImmutableArray afflictions) + { + MedicalClinic.NetCrewMember existingMember; + + if (medicalClinic.PendingHeals.FirstOrNull(m => m.CharacterEquals(crewMember)) is { } foundHeal) + { + existingMember = foundHeal; + } + else + { + MedicalClinic.NetCrewMember newMember = new MedicalClinic.NetCrewMember + { + CharacterInfoID = crewMember.CharacterInfoID, + Afflictions = Array.Empty() + }; + + existingMember = newMember; + } + + foreach (MedicalClinic.NetAffliction affliction in afflictions) + { + if (existingMember.Afflictions.FirstOrNull(a => a.AfflictionEquals(affliction)) != null) + { + return; + } + } + + existingMember.Afflictions = existingMember.Afflictions.Concat(afflictions).ToArray(); + ToggleElements(ElementState.Disabled, elementsToDisable); + medicalClinic.AddPendingButtonAction(existingMember, request => + { + if (request.Result == MedicalClinic.RequestResult.Timeout) + { + ToggleElements(ElementState.Enabled, elementsToDisable); + } + }); + } + + private static void EnsureTextDoesntOverflow(string? text, GUITextBlock textBlock, Rectangle bounds, ImmutableArray? layoutGroups = null) + { + if (string.IsNullOrWhiteSpace(text)) { return; } + + string originalText = text; + + UpdateLayoutGroups(); + + while (textBlock.Rect.X + textBlock.TextSize.X + textBlock.Padding.X + textBlock.Padding.W > bounds.Right) + { + if (string.IsNullOrWhiteSpace(text)) { break; } + + text = text[..^1]; + textBlock.Text = text + "..."; + textBlock.ToolTip = originalText; + + UpdateLayoutGroups(); + } + + void UpdateLayoutGroups() + { + if (layoutGroups is null) { return; } + + foreach (GUILayoutGroup layoutGroup in layoutGroups) + { + layoutGroup.Recalculate(); + } + } + } + + public void RequestLatestPending() + { + UpdateCrewPanel(); + + if (GameMain.IsSingleplayer || !(pendingHealList is { ErrorBlock: { } errorBlock, HealList: { } healList })) { return; } + + errorBlock.Visible = true; + errorBlock.TextColor = GUI.Style.TextColor; + errorBlock.Text = TextManager.Get("pleasewaitupnp"); + healList.Visible = false; + + isWaitingForServer = true; + + medicalClinic.RequestLatestPending(OnReceived); + + void OnReceived(MedicalClinic.PendingRequest request) + { + isWaitingForServer = false; + + if (request.Result != MedicalClinic.RequestResult.Success) + { + errorBlock.Text = GetErrorText(request.Result); + errorBlock.TextColor = GUI.Style.Red; + return; + } + + medicalClinic.PendingHeals.Clear(); + foreach (MedicalClinic.NetCrewMember member in request.CrewMembers) + { + medicalClinic.PendingHeals.Add(member); + } + + OnMedicalClinicUpdated(); + + errorBlock.Visible = false; + healList.Visible = true; + } + } + + public void ClosePopup() + { + if (selectedCrewElement is { } popup) + { + popup.Parent?.RemoveChild(selectedCrewElement); + } + + selectedCrewElement = null; + selectedCrewAfflictionList = null; + } + + private static string GetErrorText(MedicalClinic.RequestResult result) + { + return result switch + { + MedicalClinic.RequestResult.Error => TextManager.Get("error"), + MedicalClinic.RequestResult.Timeout => TextManager.Get("medicalclinic.requesttimeout"), + _ => "What the hell did you just do" // this should never happen + }; + } + + private ImmutableArray GetPendingCharacters() => medicalClinic.PendingHeals.ToImmutableArray(); + + private static void ToggleElements(ElementState state, ImmutableArray elements) + { + foreach (GUIComponent component in elements) + { + component.Enabled = state switch + { + ElementState.Enabled => true, + ElementState.Disabled => false, + _ => throw new ArgumentOutOfRangeException(nameof(state), state, null) + }; + } + } + + private static void RecalculateLayouts(params GUILayoutGroup[] layouts) + { + foreach (GUILayoutGroup layout in layouts) + { + layout.Recalculate(); + } + } + + public void Update(float deltaTime) + { + if (prevResolution.X != GameMain.GraphicsWidth || prevResolution.Y != GameMain.GraphicsHeight) + { + CreateUI(); + } + + refreshTimer += deltaTime; + + if (refreshTimer > refreshTimerMax) + { + UpdateCrewPanel(); + refreshTimer = 0; + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs index 04bbaa485..5a750998d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs @@ -58,7 +58,7 @@ namespace Barotrauma } } - private readonly List children = new List(); + protected readonly List children = new List(); public IEnumerable Children => children; public int CountChildren => children.Count; @@ -637,7 +637,15 @@ namespace Barotrauma public bool IsParentOf(RectTransform rectT, bool recursive = true) { - return children.Contains(rectT) || (recursive && children.Any(c => c.IsParentOf(rectT))); + if (children.Contains(rectT)) { return true; } + if (recursive) + { + foreach (var child in children) + { + if (child.IsParentOf(rectT)) { return true; } + } + } + return false; } public bool IsChildOf(RectTransform rectT, bool recursive = true) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index 191a8392d..4d2e47df8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -43,36 +43,42 @@ namespace Barotrauma private bool needsRefresh, needsBuyingRefresh, needsSellingRefresh, needsItemsToSellRefresh, needsSellingFromSubRefresh, needsItemsToSellFromSubRefresh; private Point resolutionWhenCreated; - private bool hadPermissions; private Dictionary OwnedItems { get; } = new Dictionary(); private CargoManager CargoManager => campaignUI.Campaign.CargoManager; private Location CurrentLocation => campaignUI.Campaign.Map?.CurrentLocation; private int PlayerMoney => campaignUI.Campaign.Money; - private bool HasPermissions => campaignUI.Campaign.AllowedToManageCampaign(); + private bool IsBuying => activeTab switch { StoreTab.Buy => true, StoreTab.Sell => false, - StoreTab.SellFromSub => false, + StoreTab.SellSub => false, _ => throw new NotImplementedException() }; private GUIListBox ActiveShoppingCrateList => activeTab switch { StoreTab.Buy => shoppingCrateBuyList, StoreTab.Sell => shoppingCrateSellList, - StoreTab.SellFromSub => shoppingCrateSellFromSubList, + StoreTab.SellSub => shoppingCrateSellFromSubList, _ => throw new NotImplementedException() }; - private bool IsTabUnavailable(StoreTab tab) => !tabLists.ContainsKey(tab); - public enum StoreTab { + /// + /// Buy items from the store + /// Buy, + /// + /// Sell items from the character inventory + /// Sell, - SellFromSub + /// + /// Sell items from the sub + /// + SellSub } private enum SortingMethod @@ -84,11 +90,117 @@ namespace Barotrauma CategoryAsc } + #region Permissions + + private bool hadPermissions, hadBuyPermissions, hadSellInventoryPermissions, hadSellSubPermissions; + + private bool HasPermissions + { + get => GetPermissions(); + set => hadPermissions = value; + } + private bool HasBuyPermissions + { + get => HasPermissions || GetPermissions(StoreTab.Buy); + set => hadBuyPermissions = value; + } + private bool HasSellInventoryPermissions + { + get => HasPermissions || GetPermissions(StoreTab.Sell); + set => hadSellInventoryPermissions = value; + } + private bool HasSellSubPermissions + { + get => HasPermissions || GetPermissions(StoreTab.SellSub); + set => hadSellSubPermissions = value; + } + + private bool GetPermissions(StoreTab? tab = null) + { + if (!tab.HasValue) + { + return campaignUI.Campaign.AllowedToManageCampaign() || campaignUI.Campaign.AllowedToManageCampaign(Networking.ClientPermissions.CampaignStore); + } + else + { + return tab.Value switch + { + StoreTab.Buy => campaignUI.Campaign.AllowedToManageCampaign(Networking.ClientPermissions.BuyItems), + StoreTab.Sell => campaignUI.Campaign.AllowedToManageCampaign(Networking.ClientPermissions.SellInventoryItems), + StoreTab.SellSub => campaignUI.Campaign.AllowedToManageCampaign(Networking.ClientPermissions.SellSubItems), + _ => false, + }; + } + } + + private void UpdatePermissions(StoreTab? tab = null) + { + HasPermissions = GetPermissions(); + if (!tab.HasValue) + { + HasBuyPermissions = GetPermissions(StoreTab.Buy); + HasSellInventoryPermissions = GetPermissions(StoreTab.Sell); + HasSellSubPermissions = GetPermissions(StoreTab.SellSub); + } + else + { + switch (tab.Value) + { + case StoreTab.Buy: + HasBuyPermissions = GetPermissions(tab.Value); + break; + case StoreTab.Sell: + HasSellInventoryPermissions = GetPermissions(tab.Value); + break; + case StoreTab.SellSub: + HasSellSubPermissions = GetPermissions(tab.Value); + break; + } + } + } + + private bool HasTabPermissions(StoreTab tab) + { + return tab switch + { + StoreTab.Buy => HasBuyPermissions, + StoreTab.Sell => HasSellInventoryPermissions, + StoreTab.SellSub => HasSellSubPermissions, + _ => false + }; + } + + private bool HasActiveTabPermissions() + { + return HasTabPermissions(activeTab); + } + + private bool HavePermissionsChanged(StoreTab? tab = null) + { + if (!tab.HasValue) + { + return hadPermissions != HasPermissions; + } + else + { + bool hadTabPermissions = tab.Value switch + { + StoreTab.Buy => hadBuyPermissions, + StoreTab.Sell => hadSellInventoryPermissions, + StoreTab.SellSub => hadSellSubPermissions, + _ => false + }; + return hadTabPermissions != HasTabPermissions(tab.Value); + } + } + + #endregion + public Store(CampaignUI campaignUI, GUIComponent parentComponent) { this.campaignUI = campaignUI; this.parentComponent = parentComponent; - hadPermissions = HasPermissions; + UpdatePermissions(); CreateUI(); campaignUI.Campaign.Map.OnLocationChanged += UpdateLocation; if (CurrentLocation?.Reputation != null) @@ -109,7 +221,7 @@ namespace Barotrauma public void Refresh(bool updateOwned = true) { - hadPermissions = HasPermissions; + UpdatePermissions(); if (updateOwned) { UpdateOwnedItems(); } RefreshBuying(updateOwned: false); RefreshSelling(updateOwned: false); @@ -122,7 +234,7 @@ namespace Barotrauma if (updateOwned) { UpdateOwnedItems(); } RefreshShoppingCrateBuyList(); RefreshStoreBuyList(); - var hasPermissions = HasPermissions; + bool hasPermissions = HasTabPermissions(StoreTab.Buy); storeBuyList.Enabled = hasPermissions; shoppingCrateBuyList.Enabled = hasPermissions; needsBuyingRefresh = false; @@ -133,7 +245,7 @@ namespace Barotrauma if (updateOwned) { UpdateOwnedItems(); } RefreshShoppingCrateSellList(); RefreshStoreSellList(); - var hasPermissions = HasPermissions; + bool hasPermissions = HasTabPermissions(StoreTab.Sell); storeSellList.Enabled = hasPermissions; shoppingCrateSellList.Enabled = hasPermissions; needsSellingRefresh = false; @@ -141,13 +253,11 @@ namespace Barotrauma private void RefreshSellingFromSub(bool updateOwned = true, bool updateItemsToSellFromSub = true) { - if (IsTabUnavailable(StoreTab.SellFromSub)) { return; } if (updateOwned) { UpdateOwnedItems(); } if (updateItemsToSellFromSub) RefreshItemsToSellFromSub(); RefreshShoppingCrateSellFromSubList(); RefreshStoreSellFromSubList(); - // TODO: Separate permissions from regular campaign permissions - var hasPermissions = HasPermissions; + bool hasPermissions = HasTabPermissions(StoreTab.SellSub); storeSellFromSubList.Enabled = hasPermissions; shoppingCrateSellFromSubList.Enabled = hasPermissions; needsSellingFromSubRefresh = false; @@ -261,7 +371,7 @@ namespace Barotrauma { StoreTab.Buy => CurrentLocation.StoreCurrentBalance + buyTotal, StoreTab.Sell => CurrentLocation.StoreCurrentBalance - sellTotal, - StoreTab.SellFromSub => CurrentLocation.StoreCurrentBalance - sellFromSubTotal, + StoreTab.SellSub => CurrentLocation.StoreCurrentBalance - sellFromSubTotal, _ => throw new NotImplementedException(), }; if (balanceAfterTransaction != CurrentLocation.StoreCurrentBalance) @@ -325,10 +435,9 @@ namespace Barotrauma tabSortingMethods.Clear(); foreach (StoreTab tab in tabs) { - if (tab == StoreTab.SellFromSub && GameMain.IsMultiplayer) { continue; } string text = tab switch { - StoreTab.SellFromSub => TextManager.Get("submarine"), + StoreTab.SellSub => TextManager.Get("submarine"), _ => TextManager.Get("campaignstoretab." + tab) }; var tabButton = new GUIButton(new RectTransform(new Vector2(1.0f / (tabs.Length + 1), 1.0f), modeButtonContainer.RectTransform), @@ -456,16 +565,13 @@ namespace Barotrauma storeRequestedGoodGroup = CreateDealsGroup(storeSellList); tabLists.Add(StoreTab.Sell, storeSellList); - if (GameMain.IsSingleplayer) + storeSellFromSubList = new GUIListBox(new RectTransform(Vector2.One, storeItemListContainer.RectTransform)) { - storeSellFromSubList = new GUIListBox(new RectTransform(Vector2.One, storeItemListContainer.RectTransform)) - { - AutoHideScrollBar = false, - Visible = false - }; - storeRequestedSubGoodGroup = CreateDealsGroup(storeSellFromSubList); - tabLists.Add(StoreTab.SellFromSub, storeSellFromSubList); - } + AutoHideScrollBar = false, + Visible = false + }; + storeRequestedSubGoodGroup = CreateDealsGroup(storeSellFromSubList); + tabLists.Add(StoreTab.SellSub, storeSellFromSubList); // Shopping Crate ------------------------------------------------------------------------------------------------------------------------------------------ @@ -526,10 +632,7 @@ namespace Barotrauma var shoppingCrateListContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.8f), shoppingCrateInventoryContainer.RectTransform), style: null); shoppingCrateBuyList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false }; shoppingCrateSellList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false }; - if (GameMain.IsSingleplayer) - { - shoppingCrateSellFromSubList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false }; - } + shoppingCrateSellFromSubList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false }; var relevantBalanceContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), shoppingCrateInventoryContainer.RectTransform), isHorizontal: true) { @@ -569,16 +672,16 @@ namespace Barotrauma clearAllButton = new GUIButton(new RectTransform(new Vector2(0.35f, 1.0f), buttonContainer.RectTransform), TextManager.Get("campaignstore.clearall")) { ClickSound = GUISoundType.DecreaseQuantity, - Enabled = HasPermissions, + Enabled = HasActiveTabPermissions(), ForceUpperCase = true, OnClicked = (button, userData) => { - if (!HasPermissions) { return false; } + if (!HasActiveTabPermissions()) { return false; } var itemsToRemove = activeTab switch { StoreTab.Buy => new List(CargoManager.ItemsInBuyCrate), StoreTab.Sell => new List(CargoManager.ItemsInSellCrate), - StoreTab.SellFromSub => new List(CargoManager.ItemsInSellFromSubCrate), + StoreTab.SellSub => new List(CargoManager.ItemsInSellFromSubCrate), _ => throw new NotImplementedException(), }; itemsToRemove.ForEach(i => ClearFromShoppingCrate(i)); @@ -637,7 +740,6 @@ namespace Barotrauma private void ChangeStoreTab(StoreTab tab) { - if (IsTabUnavailable(tab)) { return; } activeTab = tab; foreach (GUIButton tabButton in storeTabButtons) { @@ -680,7 +782,7 @@ namespace Barotrauma } shoppingCrateSellList.Visible = true; break; - case StoreTab.SellFromSub: + case StoreTab.SellSub: storeBuyList.Visible = false; storeSellList.Visible = false; if (storeSellFromSubList != null) @@ -699,7 +801,6 @@ namespace Barotrauma private void FilterStoreItems(MapEntityCategory? category, string filter) { - if (IsTabUnavailable(activeTab)) { return; } selectedItemCategory = category; var list = tabLists[activeTab]; filter = filter?.ToLower(); @@ -733,7 +834,7 @@ namespace Barotrauma float prevBuyListScroll = storeBuyList.BarScroll; float prevShoppingCrateScroll = shoppingCrateBuyList.BarScroll; - bool hasPermissions = HasPermissions; + bool hasPermissions = HasBuyPermissions; HashSet existingItemFrames = new HashSet(); int dailySpecialCount = CurrentLocation?.DailySpecials.Count() ?? 3; @@ -816,7 +917,7 @@ namespace Barotrauma { float prevSellListScroll = storeSellList.BarScroll; float prevShoppingCrateScroll = shoppingCrateSellList.BarScroll; - bool hasPermissions = HasPermissions; + bool hasPermissions = HasTabPermissions(StoreTab.Sell); HashSet existingItemFrames = new HashSet(); if ((storeRequestedGoodGroup != null) != CurrentLocation.RequestedGoods.Any()) @@ -894,7 +995,7 @@ namespace Barotrauma { float prevSellListScroll = storeSellFromSubList.BarScroll; float prevShoppingCrateScroll = shoppingCrateSellFromSubList.BarScroll; - bool hasPermissions = HasPermissions; + bool hasPermissions = HasSellSubPermissions; HashSet existingItemFrames = new HashSet(); if ((storeRequestedSubGoodGroup != null) != CurrentLocation.RequestedGoods.Any()) @@ -938,12 +1039,12 @@ namespace Barotrauma if (itemFrame == null) { var parentComponent = isRequestedGood ? storeRequestedSubGoodGroup : storeSellFromSubList as GUIComponent; - itemFrame = CreateItemFrame(new PurchasedItem(itemPrefab, itemQuantity), parentComponent, StoreTab.SellFromSub, forceDisable: !hasPermissions); + itemFrame = CreateItemFrame(new PurchasedItem(itemPrefab, itemQuantity), parentComponent, StoreTab.SellSub, forceDisable: !hasPermissions); } else { (itemFrame.UserData as PurchasedItem).Quantity = itemQuantity; - SetQuantityLabelText(StoreTab.SellFromSub, itemFrame); + SetQuantityLabelText(StoreTab.SellSub, itemFrame); SetOwnedLabelText(itemFrame); SetPriceGetters(itemFrame, false); } @@ -961,8 +1062,8 @@ namespace Barotrauma removedItemFrames.AddRange(storeRequestedSubGoodGroup.Children.Where(c => c.UserData is PurchasedItem).Except(existingItemFrames).ToList()); } removedItemFrames.ForEach(f => f.RectTransform.Parent = null); - if (activeTab == StoreTab.SellFromSub) { FilterStoreItems(); } - SortItems(StoreTab.SellFromSub); + if (activeTab == StoreTab.SellSub) { FilterStoreItems(); } + SortItems(StoreTab.SellSub); storeSellFromSubList.BarScroll = prevSellListScroll; shoppingCrateSellFromSubList.BarScroll = prevShoppingCrateScroll; @@ -1062,17 +1163,14 @@ namespace Barotrauma private void RefreshShoppingCrateList(List items, GUIListBox listBox, StoreTab tab) { - bool hasPermissions = HasPermissions; + bool hasPermissions = HasTabPermissions(tab); HashSet existingItemFrames = new HashSet(); int totalPrice = 0; foreach (PurchasedItem item in items) { - PriceInfo priceInfo = item.ItemPrefab.GetPriceInfo(CurrentLocation); - if (priceInfo == null) { continue; } - - var itemFrame = listBox.Content.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab.Identifier == item.ItemPrefab.Identifier); + if (!(item.ItemPrefab.GetPriceInfo(CurrentLocation) is { } priceInfo)) { continue; } GUINumberInput numInput = null; - if (itemFrame == null) + if (!(listBox.Content.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab.Identifier == item.ItemPrefab.Identifier) is { } itemFrame)) { itemFrame = CreateItemFrame(item, listBox, tab, forceDisable: !hasPermissions); numInput = itemFrame.FindChild(c => c is GUINumberInput, recursive: true) as GUINumberInput; @@ -1100,10 +1198,21 @@ namespace Barotrauma } suppressBuySell = false; - var price = tab == StoreTab.Buy ? - CurrentLocation.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo) : - CurrentLocation.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo); - totalPrice += item.Quantity * price; + try + { + int price = tab switch + { + StoreTab.Buy => CurrentLocation.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo), + StoreTab.Sell => CurrentLocation.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo), + StoreTab.SellSub => CurrentLocation.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo), + _ => throw new NotImplementedException() + }; + totalPrice += item.Quantity * price; + } + catch (NotImplementedException e) + { + DebugConsole.ShowError($"Error getting item price: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); + } } var removedItemFrames = listBox.Content.Children.Except(existingItemFrames).ToList(); @@ -1119,7 +1228,7 @@ namespace Barotrauma case StoreTab.Sell: sellTotal = totalPrice; break; - case StoreTab.SellFromSub: + case StoreTab.SellSub: sellFromSubTotal = totalPrice; break; } @@ -1135,7 +1244,7 @@ namespace Barotrauma private void RefreshShoppingCrateSellList() => RefreshShoppingCrateList(CargoManager.ItemsInSellCrate, shoppingCrateSellList, StoreTab.Sell); - private void RefreshShoppingCrateSellFromSubList() => RefreshShoppingCrateList(CargoManager.ItemsInSellFromSubCrate, shoppingCrateSellFromSubList, StoreTab.SellFromSub); + private void RefreshShoppingCrateSellFromSubList() => RefreshShoppingCrateList(CargoManager.ItemsInSellFromSubCrate, shoppingCrateSellFromSubList, StoreTab.SellSub); private void SortItems(GUIListBox list, SortingMethod sortingMethod) { @@ -1286,14 +1395,12 @@ namespace Barotrauma private void SortItems(StoreTab tab, SortingMethod sortingMethod) { - if (IsTabUnavailable(tab)) { return; } tabSortingMethods[tab] = sortingMethod; SortItems(tabLists[tab], sortingMethod); } private void SortItems(StoreTab tab) { - if (IsTabUnavailable(tab)) { return; } SortItems(tab, tabSortingMethods[tab]); } @@ -1301,12 +1408,6 @@ namespace Barotrauma private GUIComponent CreateItemFrame(PurchasedItem pi, GUIComponent parentComponent, StoreTab containingTab, bool forceDisable = false) { - var tooltip = pi.ItemPrefab.Name; - if (!string.IsNullOrWhiteSpace(pi.ItemPrefab.Description)) - { - tooltip += "\n" + pi.ItemPrefab.Description; - } - GUIListBox parentListBox = parentComponent as GUIListBox; int width = 0; RectTransform parent = null; @@ -1320,7 +1421,11 @@ namespace Barotrauma width = parentComponent.Rect.Width; parent = parentComponent.RectTransform; } - + string tooltip = pi.ItemPrefab.Name; + if (!string.IsNullOrWhiteSpace(pi.ItemPrefab.Description)) + { + tooltip += $"\n{pi.ItemPrefab.Description}"; + } GUIFrame frame = new GUIFrame(new RectTransform(new Point(width, (int)(GUI.yScale * 80)), parent: parent), style: "ListBoxElement") { ToolTip = tooltip, @@ -1338,8 +1443,7 @@ namespace Barotrauma var iconRelativeWidth = 0.0f; var priceAndButtonRelativeWidth = 1.0f - nameAndIconRelativeWidth; - Sprite itemIcon = pi.ItemPrefab.InventoryIcon ?? pi.ItemPrefab.sprite; - if (itemIcon != null) + if ((pi.ItemPrefab.InventoryIcon ?? pi.ItemPrefab.sprite) is { } itemIcon) { iconRelativeWidth = (0.9f * mainGroup.Rect.Height) / mainGroup.Rect.Width; GUIImage img = new GUIImage(new RectTransform(new Vector2(iconRelativeWidth, 0.9f), mainGroup.RectTransform), itemIcon, scaleToFit: true) @@ -1425,7 +1529,7 @@ namespace Barotrauma { if (suppressBuySell) { return; } PurchasedItem purchasedItem = numberInput.UserData as PurchasedItem; - if (!HasPermissions) + if (!HasActiveTabPermissions()) { numberInput.IntValue = purchasedItem.Quantity; return; @@ -1528,11 +1632,16 @@ namespace Barotrauma OwnedItems.Clear(); // Add items on the sub(s) - Submarine.MainSub?.GetItems(true) - .Where(i => i.Components.All(c => !(c is Holdable h) || !h.Attachable || !h.Attached) && - i.Components.All(c => !(c is Wire w) || w.Connections.All(c => c == null)) && - ItemAndAllContainersInteractable(i)) - .ForEach(i => AddToOwnedItems(i.Prefab)); + if (Submarine.MainSub?.GetItems(true) is List subItems) + { + foreach (var subItem in subItems) + { + if (!subItem.Components.All(c => !(c is Holdable h) || !h.Attachable || !h.Attached)) { continue; } + if (!subItem.Components.All(c => !(c is Wire w) || w.Connections.All(c => c == null))) { continue; } + if (!ItemAndAllContainersInteractable(subItem)) { continue; } + AddToOwnedItems(subItem.Prefab); + } + } // Add items in character inventories foreach (var item in Item.ItemList) @@ -1574,8 +1683,9 @@ namespace Barotrauma private void SetItemFrameStatus(GUIComponent itemFrame, bool enabled) { - if (itemFrame == null || !(itemFrame.UserData is PurchasedItem pi)) { return; } - + if (!(itemFrame?.UserData is PurchasedItem pi)) { return; } + bool refreshFrameStatus = !pi.IsStoreComponentEnabled.HasValue || pi.IsStoreComponentEnabled.Value != enabled; + if (!refreshFrameStatus) { return; } if (itemFrame.FindChild("icon", recursive: true) is GUIImage icon) { if (pi.ItemPrefab?.InventoryIcon != null) @@ -1587,14 +1697,11 @@ namespace Barotrauma icon.Color = pi.ItemPrefab.SpriteColor * (enabled ? 1.0f : 0.5f); } }; - var color = Color.White * (enabled ? 1.0f : 0.5f); - if (itemFrame.FindChild("name", recursive: true) is GUITextBlock name) { name.TextColor = color; } - if (itemFrame.FindChild("quantitylabel", recursive: true) is GUITextBlock qty) { qty.TextColor = color; @@ -1603,25 +1710,21 @@ namespace Barotrauma { numberInput.Enabled = enabled; } - if (itemFrame.FindChild("owned", recursive: true) is GUITextBlock ownedBlock) { ownedBlock.TextColor = color; } - - var isDiscounted = false; + bool isDiscounted = false; if (itemFrame.FindChild("undiscountedprice", recursive: true) is GUITextBlock undiscountedPriceBlock) { undiscountedPriceBlock.TextColor = color; undiscountedPriceBlock.Strikethrough.Color = color; isDiscounted = true; } - if (itemFrame.FindChild("price", recursive: true) is GUITextBlock priceBlock) { priceBlock.TextColor = isDiscounted ? storeSpecialColor * (enabled ? 1.0f : 0.5f) : color; } - if (itemFrame.FindChild("addbutton", recursive: true) is GUIButton addButton) { addButton.Enabled = enabled; @@ -1630,6 +1733,8 @@ namespace Barotrauma { removeButton.Enabled = enabled; } + pi.IsStoreComponentEnabled = enabled; + itemFrame.UserData = pi; } private void SetQuantityLabelText(StoreTab mode, GUIComponent itemFrame) @@ -1664,14 +1769,22 @@ namespace Barotrauma private int GetMaxAvailable(ItemPrefab itemPrefab, StoreTab mode) { - var list = mode switch + List list = null; + try { - StoreTab.Buy => CurrentLocation.StoreStock, - StoreTab.Sell => itemsToSell, - StoreTab.SellFromSub => itemsToSellFromSub, - _ => throw new NotImplementedException() - }; - if (list.Find(i => i.ItemPrefab == itemPrefab) is PurchasedItem item) + list = mode switch + { + StoreTab.Buy => CurrentLocation?.StoreStock, + StoreTab.Sell => itemsToSell, + StoreTab.SellSub => itemsToSellFromSub, + _ => throw new NotImplementedException() + }; + } + catch (NotImplementedException e) + { + DebugConsole.ShowError($"Error getting item availability: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); + } + if (list != null && list.Find(i => i.ItemPrefab == itemPrefab) is PurchasedItem item) { if (mode == StoreTab.Buy) { @@ -1691,8 +1804,8 @@ namespace Barotrauma private bool ModifyBuyQuantity(PurchasedItem item, int quantity) { - if (item == null || item.ItemPrefab == null) { return false; } - if (!HasPermissions) { return false; } + if (item?.ItemPrefab == null) { return false; } + if (!HasBuyPermissions) { return false; } if (quantity > 0) { var itemInCrate = CargoManager.ItemsInBuyCrate.Find(i => i.ItemPrefab == item.ItemPrefab); @@ -1703,13 +1816,13 @@ namespace Barotrauma } CargoManager.ModifyItemQuantityInBuyCrate(item.ItemPrefab, quantity); GameMain.Client?.SendCampaignState(); - return false; + return true; } private bool ModifySellQuantity(PurchasedItem item, int quantity) { - if (item == null || item.ItemPrefab == null) { return false; } - if (!HasPermissions) { return false; } + if (item?.ItemPrefab == null) { return false; } + if (!HasSellInventoryPermissions) { return false; } if (quantity > 0) { // Make sure there's enough available to sell @@ -1718,45 +1831,68 @@ namespace Barotrauma if (totalQuantityToSell > GetMaxAvailable(item.ItemPrefab, StoreTab.Sell)) { return false; } } CargoManager.ModifyItemQuantityInSellCrate(item.ItemPrefab, quantity); - //GameMain.Client?.SendCampaignState(); - return false; + return true; } private bool ModifySellFromSubQuantity(PurchasedItem item, int quantity) { - if (item == null || item.ItemPrefab == null) { return false; } - if (!HasPermissions) { return false; } + if (item?.ItemPrefab == null) { return false; } + if (!HasSellSubPermissions) { return false; } if (quantity > 0) { // Make sure there's enough available to sell var itemToSell = CargoManager.ItemsInSellFromSubCrate.Find(i => i.ItemPrefab == item.ItemPrefab); var totalQuantityToSell = itemToSell != null ? itemToSell.Quantity + quantity : quantity; - if (totalQuantityToSell > GetMaxAvailable(item.ItemPrefab, StoreTab.SellFromSub)) { return false; } + if (totalQuantityToSell > GetMaxAvailable(item.ItemPrefab, StoreTab.SellSub)) { return false; } } CargoManager.ModifyItemQuantityInSellFromSubCrate(item.ItemPrefab, quantity); - // TODO: GameMain.Client?.SendCampaignState(); - return false; + GameMain.Client?.SendCampaignState(); + return true; } - private bool AddToShoppingCrate(PurchasedItem item, int quantity = 1) => activeTab switch + private bool AddToShoppingCrate(PurchasedItem item, int quantity = 1) { - StoreTab.Buy => ModifyBuyQuantity(item, quantity), - StoreTab.Sell => ModifySellQuantity(item, quantity), - StoreTab.SellFromSub => ModifySellFromSubQuantity(item, quantity), - _ => throw new NotImplementedException(), - }; + if (item == null) { return false; } + try + { + return activeTab switch + { + StoreTab.Buy => ModifyBuyQuantity(item, quantity), + StoreTab.Sell => ModifySellQuantity(item, quantity), + StoreTab.SellSub => ModifySellFromSubQuantity(item, quantity), + _ => throw new NotImplementedException() + }; + } + catch (NotImplementedException e) + { + DebugConsole.ShowError($"Error adding an item to the shopping crate: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); + return false; + } + } - private bool ClearFromShoppingCrate(PurchasedItem item) => activeTab switch + private bool ClearFromShoppingCrate(PurchasedItem item) { - StoreTab.Buy => ModifyBuyQuantity(item, -item.Quantity), - StoreTab.Sell => ModifySellQuantity(item, -item.Quantity), - StoreTab.SellFromSub => ModifySellFromSubQuantity(item, -item.Quantity), - _ => throw new NotImplementedException(), - }; + if (item == null) { return false; } + try + { + return activeTab switch + { + StoreTab.Buy => ModifyBuyQuantity(item, -item.Quantity), + StoreTab.Sell => ModifySellQuantity(item, -item.Quantity), + StoreTab.SellSub => ModifySellFromSubQuantity(item, -item.Quantity), + _ => throw new NotImplementedException(), + }; + } + catch (NotImplementedException e) + { + DebugConsole.ShowError($"Error clearing the shopping crate: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); + return false; + } + } private bool BuyItems() { - if (!HasPermissions) { return false; } + if (!HasBuyPermissions) { return false; } var itemsToPurchase = new List(CargoManager.ItemsInBuyCrate); var itemsToRemove = new List(); @@ -1788,15 +1924,24 @@ namespace Barotrauma private bool SellItems() { - if (!HasPermissions) { return false; } - var itemsToSell = activeTab switch + if (!HasActiveTabPermissions()) { return false; } + List itemsToSell; + try { - StoreTab.Sell => new List(CargoManager.ItemsInSellCrate), - StoreTab.SellFromSub => new List(CargoManager.ItemsInSellFromSubCrate), - _ => throw new NotImplementedException() - }; + itemsToSell = activeTab switch + { + StoreTab.Sell => new List(CargoManager.ItemsInSellCrate), + StoreTab.SellSub => new List(CargoManager.ItemsInSellFromSubCrate), + _ => throw new NotImplementedException() + }; + } + catch (NotImplementedException e) + { + DebugConsole.ShowError($"Error confirming the store transaction: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); + return false; + } var itemsToRemove = new List(); - var totalValue = 0; + int totalValue = 0; foreach (PurchasedItem item in itemsToSell) { if (item?.ItemPrefab?.GetPriceInfo(CurrentLocation) is PriceInfo priceInfo) @@ -1811,11 +1956,7 @@ namespace Barotrauma itemsToRemove.ForEach(i => itemsToSell.Remove(i)); if (itemsToSell.None() || totalValue > CurrentLocation.StoreCurrentBalance) { return false; } CargoManager.SellItems(itemsToSell, activeTab); - if (activeTab == StoreTab.Sell) - { - // TODO: Implement selling sub items in multiplayer - GameMain.Client?.SendCampaignState(); - } + GameMain.Client?.SendCampaignState(); return false; } @@ -1831,7 +1972,7 @@ namespace Barotrauma int total = activeTab switch { StoreTab.Sell => sellTotal, - StoreTab.SellFromSub => sellFromSubTotal, + StoreTab.SellSub => sellFromSubTotal, _ => throw new NotImplementedException(), }; shoppingCrateTotal.Text = GetCurrencyFormatted(total); @@ -1863,21 +2004,29 @@ namespace Barotrauma } } - private void SetConfirmButtonStatus() => confirmButton.Enabled = - HasPermissions && ActiveShoppingCrateList.Content.RectTransform.Children.Any() && - activeTab switch - { - StoreTab.Buy => buyTotal <= PlayerMoney, - StoreTab.Sell => CurrentLocation != null && sellTotal <= CurrentLocation.StoreCurrentBalance, - StoreTab.SellFromSub => CurrentLocation != null && sellFromSubTotal <= CurrentLocation.StoreCurrentBalance, - _ => throw new NotImplementedException(), - }; + private void SetConfirmButtonStatus() + { + confirmButton.Enabled = + HasActiveTabPermissions() && + ActiveShoppingCrateList.Content.RectTransform.Children.Any() && + activeTab switch + { + StoreTab.Buy => buyTotal <= PlayerMoney, + StoreTab.Sell => CurrentLocation != null && sellTotal <= CurrentLocation.StoreCurrentBalance, + StoreTab.SellSub => CurrentLocation != null && sellFromSubTotal <= CurrentLocation.StoreCurrentBalance, + _ => false + }; + } - private void SetClearAllButtonStatus() => clearAllButton.Enabled = - HasPermissions && ActiveShoppingCrateList.Content.RectTransform.Children.Any(); + private void SetClearAllButtonStatus() + { + clearAllButton.Enabled = + HasActiveTabPermissions() && + ActiveShoppingCrateList.Content.RectTransform.Children.Any(); + } private float ownedItemsUpdateTimer = 0.0f, sellableItemsFromSubUpdateTimer = 0.0f; - private readonly float timerUpdateInterval = 1.5f; + private const float timerUpdateInterval = 1.5f; public void Update(float deltaTime) { @@ -1914,10 +2063,10 @@ namespace Barotrauma if (needsItemsToSellRefresh) { RefreshItemsToSell(); } if (needsItemsToSellFromSubRefresh) { RefreshItemsToSellFromSub(); } - if (needsRefresh || hadPermissions != HasPermissions) { Refresh(updateOwned: ownedItemsUpdateTimer > 0.0f); } - if (needsBuyingRefresh) { RefreshBuying(); } - if (needsSellingRefresh) { RefreshSelling(); } - if (needsSellingFromSubRefresh) { RefreshSellingFromSub(updateItemsToSellFromSub: sellableItemsFromSubUpdateTimer > 0.0f); } + 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); } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index 848bfc4b2..1c687913c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -414,15 +414,8 @@ namespace Barotrauma } else { - if (GameMain.Client == null) - { - subsToShow.AddRange(SubmarineInfo.SavedSubmarines.Where(s => s.IsCampaignCompatible && !GameMain.GameSession.OwnedSubmarines.Any(os => os.Name == s.Name))); - } - else - { - subsToShow.AddRange(GameMain.NetLobbyScreen.CampaignSubmarines.Where(s => !GameMain.GameSession.OwnedSubmarines.Any(os => os.Name == s.Name))); - } - + subsToShow.AddRange((GameMain.Client is null ? SubmarineInfo.SavedSubmarines : MultiPlayerCampaign.GetCampaignSubs()) + .Where(s => s.IsCampaignCompatible && !GameMain.GameSession.OwnedSubmarines.Any(os => os.Name == s.Name))); subsToShow.Sort((x, y) => x.SubmarineClass.CompareTo(y.SubmarineClass)); } @@ -446,20 +439,11 @@ namespace Barotrauma if (preview == null) { - SubmarineInfo potentialMatch; - - if (GameMain.Client == null) - { - potentialMatch = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.EqualityCheckVal == info.EqualityCheckVal); - } - else - { - potentialMatch = GameMain.NetLobbyScreen.CampaignSubmarines.FirstOrDefault(s => s.EqualityCheckVal == info.EqualityCheckVal); - } + SubmarineInfo potentialMatch = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.EqualityCheckVal == info.EqualityCheckVal); preview = potentialMatch?.PreviewImage; - // Try from savedsubmarines with name comparison as a backup + // Try name comparison as a backup if (preview == null) { potentialMatch = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == info.Name); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index a5554ebf2..7be59a84f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -20,7 +20,7 @@ namespace Barotrauma private static Sprite ownerIcon, moderatorIcon; public enum InfoFrameTab { Crew, Mission, Reputation, Traitor, Submarine, Talents }; - public static InfoFrameTab selectedTab; + public static InfoFrameTab SelectedTab { get; private set; } private GUIFrame infoFrame, contentFrame; private readonly List tabButtons = new List(); @@ -129,9 +129,8 @@ namespace Barotrauma public TabMenu() { if (!initialized) { Initialize(); } - - CreateInfoFrame(selectedTab); - SelectInfoFrameTab(null, selectedTab); + CreateInfoFrame(SelectedTab); + SelectInfoFrameTab(SelectedTab); } public void Update() @@ -147,8 +146,8 @@ namespace Barotrauma } } - if (selectedTab != InfoFrameTab.Crew) return; - if (linkedGUIList == null) return; + if (SelectedTab != InfoFrameTab.Crew) { return; } + if (linkedGUIList == null) { return; } if (GameMain.IsMultiplayer) { @@ -226,7 +225,7 @@ namespace Barotrauma { UserData = tab, ToolTip = TextManager.Get(textTag), - OnClicked = SelectInfoFrameTab + OnClicked = (btn, userData) => { SelectInfoFrameTab((InfoFrameTab)userData); return true; } }; tabButtons.Add(newButton); return newButton; @@ -277,16 +276,16 @@ namespace Barotrauma talentsButton.Enabled = Character.Controlled?.Info != null; if (!talentsButton.Enabled && selectedTab == InfoFrameTab.Talents) { - SelectInfoFrameTab(null, InfoFrameTab.Crew); + SelectInfoFrameTab(InfoFrameTab.Crew); } }; talentPointNotification = GameSession.CreateTalentIconNotification(talentsButton); } - private bool SelectInfoFrameTab(GUIButton button, object userData) + public void SelectInfoFrameTab(InfoFrameTab selectedTab) { - selectedTab = (InfoFrameTab)userData; + SelectedTab = selectedTab; CreateInfoFrame(selectedTab); tabButtons.ForEach(tb => tb.Selected = (InfoFrameTab)tb.UserData == selectedTab); @@ -300,7 +299,7 @@ namespace Barotrauma CreateMissionInfo(infoFrameHolder); break; case InfoFrameTab.Reputation: - if (GameMain.GameSession.RoundSummary != null && GameMain.GameSession.GameMode is CampaignMode campaignMode) + if (GameMain.GameSession?.RoundSummary != null && GameMain.GameSession?.GameMode is CampaignMode campaignMode) { infoFrameHolder.ClearChildren(); GUIFrame reputationFrame = new GUIFrame(new RectTransform(Vector2.One, infoFrameHolder.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox"); @@ -308,9 +307,9 @@ namespace Barotrauma } break; case InfoFrameTab.Traitor: - TraitorMissionPrefab traitorMission = GameMain.Client.TraitorMission; - Character traitor = GameMain.Client.Character; - if (traitor == null || traitorMission == null) return false; + TraitorMissionPrefab traitorMission = GameMain.Client?.TraitorMission; + Character traitor = GameMain.Client?.Character; + if (traitor == null || traitorMission == null) { return; } CreateTraitorInfo(infoFrameHolder, traitorMission, traitor); break; case InfoFrameTab.Submarine: @@ -320,8 +319,6 @@ namespace Barotrauma CreateTalentInfo(infoFrameHolder); break; } - - return true; } private const float jobColumnWidthPercentage = 0.138f; @@ -755,7 +752,7 @@ namespace Barotrauma if (character != null) { - if (GameMain.NetworkMember == null) + if (GameMain.Client == null) { GUIComponent preview = character.Info.CreateInfoFrame(background, false, null); } @@ -859,7 +856,7 @@ namespace Barotrauma string msg = ChatMessage.GetTimeStamp() + message.TextWithSender; storedMessages.Add(new Pair(msg, message.ChangeType)); - if (GameSession.IsTabMenuOpen && selectedTab == InfoFrameTab.Crew) + if (GameSession.IsTabMenuOpen && SelectedTab == InfoFrameTab.Crew) { TabMenu instance = GameSession.TabMenuInstance; instance.AddLineToLog(msg, message.ChangeType); @@ -1023,13 +1020,15 @@ namespace Barotrauma int iconHeight = Math.Max(missionTextGroup.RectTransform.NonScaledSize.Y, (int)(iconWidth * iconAspectRatio)); Point iconSize = new Point(iconWidth, iconHeight);*/ - new GUIImage(new RectTransform(new Point(iconSize), missionDescriptionHolder.RectTransform), mission.Prefab.Icon, null, true) + var icon = new GUIImage(new RectTransform(new Point(iconSize), missionDescriptionHolder.RectTransform), mission.Prefab.Icon, null, true) { Color = mission.Prefab.IconColor, HoverColor = mission.Prefab.IconColor, SelectedColor = mission.Prefab.IconColor, CanBeFocused = false }; + UpdateMissionStateIcon(mission, icon); + mission.OnMissionStateChanged += (mission) => UpdateMissionStateIcon(mission, icon); } new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionNameRichTextData, missionNameString, font: GUI.LargeFont); GUILayoutGroup difficultyIndicatorGroup = null; @@ -1066,6 +1065,33 @@ namespace Barotrauma } } + private void UpdateMissionStateIcon(Mission mission, GUIImage missionIcon) + { + if (mission == null || missionIcon == null) { return; } + string style = string.Empty; + if (mission.DisplayAsFailed) + { + style = "MissionFailedIcon"; + } + else if (mission.DisplayAsCompleted) + { + style = "MissionCompletedIcon"; + } + GUIImage stateIcon = missionIcon.GetChild(); + if (string.IsNullOrEmpty(style)) + { + if (stateIcon != null) + { + stateIcon.Visible = false; + } + } + else + { + stateIcon ??= new GUIImage(new RectTransform(Vector2.One, missionIcon.RectTransform), style, scaleToFit: true); + stateIcon.Visible = true; + } + } + private void CreateTraitorInfo(GUIFrame infoFrame, TraitorMissionPrefab traitorMission, Character traitor) { GUIFrame missionFrame = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox"); @@ -1443,7 +1469,7 @@ namespace Barotrauma selectedTalents.Remove(talentIdentifier); } - UpdateTalentButtons(); + UpdateTalentInfo(); return true; }, }; @@ -1508,7 +1534,7 @@ namespace Barotrauma }; GUITextBlock.AutoScaleAndNormalize(talentResetButton.TextBlock, talentApplyButton.TextBlock); - UpdateTalentButtons(); + UpdateTalentInfo(); } private void CreateTalentSkillList(Character character, GUIListBox parent) @@ -1543,30 +1569,14 @@ namespace Barotrauma GUITextBlock.AutoScaleAndNormalize(skillNames); } - private bool HasUnlockedAllTalents(Character controlledCharacter) - { - if (TalentTree.JobTalentTrees.TryGetValue(controlledCharacter.Info.Job.Prefab.Identifier, out TalentTree talentTree)) - { - foreach (TalentSubTree talentSubTree in talentTree.TalentSubTrees) - { - foreach (TalentOption talentOption in talentSubTree.TalentOptionStages) - { - if (talentOption.Talents.None(t => controlledCharacter.HasTalent(t.Identifier))) - { - return false; - } - } - } - } - return true; - } - - private void UpdateTalentButtons() + private void UpdateTalentInfo() { Character controlledCharacter = Character.Controlled; if (controlledCharacter?.Info == null) { return; } - bool unlockedAllTalents = HasUnlockedAllTalents(controlledCharacter); + if (SelectedTab != InfoFrameTab.Talents) { return; } + + bool unlockedAllTalents = controlledCharacter.HasUnlockedAllTalents(); if (unlockedAllTalents) { @@ -1646,7 +1656,7 @@ namespace Barotrauma } } selectedTalents = controlledCharacter.Info.GetUnlockedTalentsInTree().ToList(); - UpdateTalentButtons(); + UpdateTalentInfo(); } private bool ApplyTalentSelection(GUIButton guiButton, object userData) @@ -1660,63 +1670,14 @@ namespace Barotrauma { Character controlledCharacter = Character.Controlled; selectedTalents = controlledCharacter.Info.GetUnlockedTalentsInTree().ToList(); - UpdateTalentButtons(); + UpdateTalentInfo(); return true; } public void OnExperienceChanged(Character character) { if (character != Character.Controlled) { return; } - UpdateTalentButtons(); - } - - private readonly StatTypes[] basicStats = new StatTypes[] - { - StatTypes.MaximumHealthMultiplier, - StatTypes.MovementSpeed, - StatTypes.SwimmingSpeed, - StatTypes.RepairSpeed, - }; - - private readonly StatTypes[] combatStats = new StatTypes[] - { - StatTypes.MeleeAttackMultiplier, - StatTypes.MeleeAttackSpeed, - StatTypes.RangedAttackSpeed, - StatTypes.TurretAttackSpeed, - }; - - private readonly StatTypes[] miscStats = new StatTypes[] - { - StatTypes.ReputationGainMultiplier, - StatTypes.MissionMoneyGainMultiplier, - StatTypes.ExperienceGainMultiplier, - StatTypes.MissionExperienceGainMultiplier, - }; - - private void CreateCharacterSheet(GUILayoutGroup characterInfoColumn) - { - Character controlledCharacter = Character.Controlled; - - CreateRow(basicStats); - CreateRow(combatStats); - CreateRow(miscStats); - - void CreateRow(StatTypes[] statTypes) - { - GUILayoutGroup characterInfoRow = new GUILayoutGroup(new RectTransform(new Vector2(0.33f, 1.0f), characterInfoColumn.RectTransform, anchor: Anchor.TopLeft), childAnchor: Anchor.TopCenter); - foreach (StatTypes statType in statTypes) - { - ShowStat(statType, characterInfoRow); - } - } - - void ShowStat(StatTypes statType, GUILayoutGroup characterInfoRow) - { - GUIFrame textInfoFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.33f), characterInfoRow.RectTransform, Anchor.TopCenter), style: null); - new GUITextBlock(new RectTransform(new Vector2(1f, 1f), textInfoFrame.RectTransform, Anchor.TopLeft), statType.ToString(), font: GUI.SmallFont, textAlignment: Alignment.TopLeft); - new GUITextBlock(new RectTransform(new Vector2(1f, 1f), textInfoFrame.RectTransform, Anchor.TopLeft), (int)(100f * (1 + controlledCharacter.GetStatValue(statType))) + "%", font: GUI.Font, textAlignment: Alignment.TopRight); - } + UpdateTalentInfo(); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index 1994a8d59..258c55b37 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -16,7 +16,6 @@ using Microsoft.Xna.Framework.Input; namespace Barotrauma { - internal class UpgradeStore { public readonly struct CategoryData @@ -168,6 +167,7 @@ namespace Barotrauma //TODO: move this somewhere else public static void UpdateCategoryList(GUIListBox categoryList, CampaignMode campaign, Submarine? drawnSubmarine, IEnumerable applicableCategories) { + var subItems = GetSubItems(); foreach (GUIComponent component in categoryList.Content.Children) { if (!(component.UserData is CategoryData data)) { continue; } @@ -179,7 +179,7 @@ namespace Barotrauma var customizeButton = component.FindChild("customizebutton", true); if (customizeButton != null) { - customizeButton.Visible = HasSwappableItems(data.Category); + customizeButton.Visible = HasSwappableItems(data.Category, subItems); } } @@ -434,6 +434,7 @@ namespace Barotrauma if (AvailableMoney >= hullRepairCost) { Campaign.Money -= hullRepairCost; + GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "hullrepairs"); Campaign.PurchasedHullRepairs = true; button.Enabled = false; SelectTab(UpgradeTab.Repairs); @@ -468,6 +469,7 @@ namespace Barotrauma if (AvailableMoney >= itemRepairCost && !Campaign.PurchasedItemRepairs) { Campaign.Money -= itemRepairCost; + GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "devicerepairs"); Campaign.PurchasedItemRepairs = true; button.Enabled = false; SelectTab(UpgradeTab.Repairs); @@ -513,6 +515,7 @@ namespace Barotrauma if (AvailableMoney >= shuttleRetrieveCost && !Campaign.PurchasedLostShuttles) { Campaign.Money -= shuttleRetrieveCost; + GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "retrieveshuttle"); Campaign.PurchasedLostShuttles = true; button.Enabled = false; SelectTab(UpgradeTab.Repairs); @@ -717,16 +720,19 @@ namespace Barotrauma private bool customizeTabOpen; - private static bool HasSwappableItems(UpgradeCategory category) + private static bool HasSwappableItems(UpgradeCategory category, List? subItems = null) { if (Submarine.MainSub == null) { return false; } - return Submarine.MainSub.GetItems(true).Any(i => + subItems ??= GetSubItems(); + return subItems.Any(i => i.Prefab.SwappableItem != null && !i.HiddenInGame && i.AllowSwapping && (i.Prefab.SwappableItem.CanBeBought || ItemPrefab.Prefabs.Any(ip => ip.SwappableItem?.ReplacementOnUninstall == i.Prefab.Identifier)) && Submarine.MainSub.IsEntityFoundOnThisSub(i, true) && category.ItemTags.Any(t => i.HasTag(t))); } + private static List GetSubItems() => Submarine.MainSub?.GetItems(true) ?? new List(); + private void SelectUpgradeCategory(List prefabs, UpgradeCategory category, Submarine submarine) { if (selectedUpgradeCategoryLayout == null) { return; } @@ -1688,7 +1694,7 @@ namespace Barotrauma private bool HasPermission => campaignUI.Campaign.AllowedToManageCampaign(); - private static string FormatCurrency(int money, bool format = true) + public static string FormatCurrency(int money, bool format = true) { return TextManager.GetWithVariable("CurrencyFormat", "[credits]", format ? string.Format(CultureInfo.InvariantCulture, "{0:N0}", money) : money.ToString()); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs index 03683f3f8..8f0c177f2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs @@ -5,7 +5,7 @@ using System.Linq; namespace Barotrauma { - public static partial class GameAnalyticsManager + static partial class GameAnalyticsManager { static partial void CreateConsentPrompt() { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index afe9f013f..e6205bfda 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -32,6 +32,13 @@ namespace Barotrauma public static PerformanceCounter PerformanceCounter; + private static Stopwatch performanceCounterTimer; + private static int updateCount = 0; + public static int CurrentUpdateRate + { + get; private set; + } + public static readonly Version Version = Assembly.GetEntryAssembly().GetName().Version; public static string[] ConsoleArguments; @@ -124,6 +131,12 @@ namespace Barotrauma private bool exiting; + public static bool IsFirstLaunch + { + get; + private set; + } + public static GameMain Instance { get; @@ -353,6 +366,8 @@ namespace Barotrauma Hyper.ComponentModel.HyperTypeDescriptionProvider.Add(typeof(Item)); Hyper.ComponentModel.HyperTypeDescriptionProvider.Add(typeof(Items.Components.ItemComponent)); Hyper.ComponentModel.HyperTypeDescriptionProvider.Add(typeof(Hull)); + + performanceCounterTimer = Stopwatch.StartNew(); } /// @@ -441,8 +456,8 @@ namespace Barotrauma TaskPool.Add("AutoUpdateWorkshopItemsAsync", SteamManager.AutoUpdateWorkshopItemsAsync(), (task) => { - bool result = ((Task)task).Result; - + if (!task.TryGetResult(out bool result)) { return; } + Config.WaitingForAutoUpdate = false; }); @@ -583,6 +598,18 @@ namespace Barotrauma { Steamworks.SteamFriends.OnGameRichPresenceJoinRequested += OnInvitedToGame; Steamworks.SteamFriends.OnGameLobbyJoinRequested += OnLobbyJoinRequested; + + if (SteamManager.TryGetUnlockedAchievements(out List achievements)) + { + //check the achievements too, so we don't consider people who've played the game before this "gamelaunchcount" stat was added as being 1st-time-players + //(people who have played previous versions, but not unlocked any achievements, will be incorrectly considered 1st-time-players, but that should be a small enough group to not skew the statistics) + if (!achievements.Any() && SteamManager.GetStatInt("gamelaunchcount") <= 0) + { + IsFirstLaunch = true; + GameAnalyticsManager.AddDesignEvent("FirstLaunch"); + } + } + SteamManager.IncrementStat("gamelaunchcount", 1); } #endif @@ -700,11 +727,10 @@ namespace Barotrauma protected override void Update(GameTime gameTime) { Timing.Accumulator += gameTime.ElapsedGameTime.TotalSeconds; - int updateIterations = (int)Math.Floor(Timing.Accumulator / Timing.Step); - if (Timing.Accumulator > Timing.Step * 6.0) + if (Timing.Accumulator > Timing.AccumulatorMax) { - //if the game's running too slowly then we have no choice - //but to skip a bunch of steps + //prevent spiral of death: + //if the game's running too slowly then we have no choice but to skip a bunch of steps //otherwise it snowballs and becomes unplayable Timing.Accumulator = Timing.Step; } @@ -740,7 +766,6 @@ namespace Barotrauma PlayerInput.Update(Timing.Step); - if (loadingScreenOpen) { //reset accumulator if loading @@ -760,7 +785,10 @@ namespace Barotrauma } #if DEBUG - CancelQuickStart |= PlayerInput.KeyDown(Keys.LeftShift); + if (PlayerInput.KeyHit(Keys.LeftShift)) + { + CancelQuickStart = !CancelQuickStart; + } if (TitleScreen.LoadState >= 100.0f && !TitleScreen.PlayingSplashScreen && (Config.AutomaticQuickStartEnabled || Config.AutomaticCampaignLoadEnabled || Config.TestScreenEnabled) && FirstLoad && !CancelQuickStart) { @@ -979,13 +1007,21 @@ namespace Barotrauma Timing.Accumulator -= Timing.Step; + updateCount++; + sw.Stop(); PerformanceCounter.AddElapsedTicks("Update total", sw.ElapsedTicks); PerformanceCounter.UpdateTimeGraph.Update(sw.ElapsedTicks * 1000.0f / (float)Stopwatch.Frequency); - PerformanceCounter.UpdateIterationsGraph.Update(updateIterations); } - if (!Paused) Timing.Alpha = Timing.Accumulator / Timing.Step; + if (!Paused) { Timing.Alpha = Timing.Accumulator / Timing.Step; } + + if (performanceCounterTimer.ElapsedMilliseconds > 1000) + { + CurrentUpdateRate = (int)Math.Round(updateCount / (double)(performanceCounterTimer.ElapsedMilliseconds / 1000.0)); + performanceCounterTimer.Restart(); + updateCount = 0; + } } public static void ResetFrameTime() @@ -1090,8 +1126,15 @@ namespace Barotrauma { double roundDuration = Timing.TotalTime - GameSession.RoundStartTime; GameAnalyticsManager.AddProgressionEvent(GameAnalyticsManager.ProgressionStatus.Fail, - GameSession.GameMode?.Name ?? "none", + GameSession.GameMode?.Preset.Identifier ?? "none", roundDuration); + string eventId = "QuitRound:" + (GameSession.GameMode?.Preset.Identifier ?? "none") + ":"; + GameAnalyticsManager.AddDesignEvent(eventId + "EventManager:CurrentIntensity", GameSession.EventManager.CurrentIntensity); + foreach (var activeEvent in GameSession.EventManager.ActiveEvents) + { + GameAnalyticsManager.AddDesignEvent(eventId + "EventManager:ActiveEvents:" + activeEvent.ToString()); + } + GameSession.LogEndRoundStats(eventId); if (Tutorial.Initialized) { ((TutorialMode)GameSession.GameMode).Tutorial?.Stop(); @@ -1183,11 +1226,7 @@ namespace Barotrauma new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), linkHolder.RectTransform), TextManager.Get("bugreportgithubform"), style: "MainMenuGUIButton", textAlignment: Alignment.Left) { -#if UNSTABLE - UserData = "https://barotraumagame.com/unstable-3rf3w5t4ter/", -#else UserData = "https://github.com/Regalis11/Barotrauma/issues/new?template=bug_report.md", -#endif OnClicked = (btn, userdata) => { ShowOpenUrlInWebBrowserPrompt(userdata as string); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs index a92ac9d93..2d8f290ab 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs @@ -1,5 +1,6 @@ using Barotrauma.Extensions; using Barotrauma.Items.Components; +using System; using System.Collections.Generic; using System.Linq; @@ -7,37 +8,6 @@ namespace Barotrauma { partial class CargoManager { - private class SoldEntity - { - public enum SellStatus - { - /// - /// Entity sold in SP. Or, entity sold by client and confirmed by server in MP. - /// - Confirmed, - /// - /// Entity sold by client in MP. Client has received at least one update from server after selling, but this entity wasn't yet confirmed. - /// - Unconfirmed, - /// - /// Entity sold by client in MP. Client hasn't yet received an update from server after selling. - /// - Local - } - - public Item Item { get; } - public SellStatus Status { get; set; } - - private SoldEntity(Item item, SellStatus status) - { - Item = item; - Status = status; - } - - public static SoldEntity CreateInSinglePlayer(Item item) => new SoldEntity(item, SellStatus.Confirmed); - public static SoldEntity CreateInMultiPlayer(Item item) => new SoldEntity(item, SellStatus.Local); - } - private List SoldEntities { get; } = new List(); // The bag slot is intentionally left out since we want to be able to sell items from there @@ -67,31 +37,6 @@ namespace Barotrauma } } - public IEnumerable GetSellableItemsFromSub() - { - if (Submarine.MainSub == null) { return new List(); } - var confirmedSoldEntities = GetConfirmedSoldEntities(); - return Submarine.MainSub.GetItems(true).FindAll(item => - { - if (!IsItemSellable(item, confirmedSoldEntities)) { return false; } - if (item.GetRootInventoryOwner() is Character) { return false; } - if (!item.Components.All(c => !(c is Holdable h) || !h.Attachable || !h.Attached)) { return false; } - if (!item.Components.All(c => !(c is Wire w) || w.Connections.All(c => c == null))) { return false; } - if (!ItemAndAllContainersInteractable(item)) { return false; } - return true; - }).Distinct(); - - static bool ItemAndAllContainersInteractable(Item item) - { - do - { - if (!item.IsPlayerTeamInteractable) { return false; } - item = item.Container; - } while (item != null); - return true; - } - } - private IEnumerable GetConfirmedSoldEntities() { // Only consider items which have been: @@ -100,24 +45,6 @@ namespace Barotrauma return SoldEntities.Where(se => se.Status != SoldEntity.SellStatus.Unconfirmed); } - private bool IsItemSellable(Item item, IEnumerable confirmedSoldEntities) - { - if (!item.Prefab.CanBeSold) { return false; } - if (item.SpawnedInCurrentOutpost) { return false; } - if (!item.Prefab.AllowSellingWhenBroken && item.ConditionPercentage < 90.0f) { return false; } - if (confirmedSoldEntities.Any(it => it.Item == item)) { return false; } - if (item.OwnInventory?.Container is ItemContainer itemContainer) - { - var containedItems = item.ContainedItems; - if (containedItems.None()) { return true; } - // Allow selling the item if contained items are unsellable and set to be removed on deconstruct - if (itemContainer.RemoveContainedItemsOnDeconstruct && containedItems.All(it => !it.Prefab.CanBeSold)) { return true; } - // Otherwise there must be no contained items or the contained items must be confirmed as sold - if (!containedItems.All(it => confirmedSoldEntities.Any(se => se.Item == it))) { return false; } - } - return true; - } - public void SetItemsInBuyCrate(List items) { ItemsInBuyCrate.Clear(); @@ -125,15 +52,21 @@ namespace Barotrauma OnItemsInBuyCrateChanged?.Invoke(); } + public void SetItemsInSubSellCrate(List items) + { + ItemsInSellFromSubCrate.Clear(); + ItemsInSellFromSubCrate.AddRange(items); + OnItemsInSellFromSubCrateChanged?.Invoke(); + } + public void SetSoldItems(List items) { SoldItems.Clear(); SoldItems.AddRange(items); - - foreach (SoldEntity se in SoldEntities) + foreach (var se in SoldEntities) { if (se.Status == SoldEntity.SellStatus.Confirmed) { continue; } - if (SoldItems.Any(si => si.ID == se.Item.ID && si.ItemPrefab == se.Item.Prefab && (GameMain.Client == null || GameMain.Client.ID == si.SellerID))) + if (SoldItems.Any(si => Match(si, se, true))) { se.Status = SoldEntity.SellStatus.Confirmed; } @@ -142,13 +75,28 @@ namespace Barotrauma se.Status = SoldEntity.SellStatus.Unconfirmed; } } - + foreach (var si in SoldItems) + { + if (si.Origin != SoldItem.SellOrigin.Submarine) { continue; } + if (!(SoldEntities.FirstOrDefault(se => se.Item == null && Match(si, se, false)) is SoldEntity soldEntityMatch)) { continue; } + if (!(Entity.FindEntityByID(si.ID) is Item item)) { continue; } + soldEntityMatch.SetItem(item); + soldEntityMatch.Status = SoldEntity.SellStatus.Confirmed; + } OnSoldItemsChanged?.Invoke(); + + static bool Match(SoldItem soldItem, SoldEntity soldEntity, bool matchId) + { + if (soldItem.ItemPrefab != soldEntity.ItemPrefab) { return false; } + if (matchId && (soldEntity.Item == null || soldItem.ID != soldEntity.Item.ID)) { return false; } + if (soldItem.Origin == SoldItem.SellOrigin.Character && GameMain.Client != null && soldItem.SellerID != GameMain.Client.ID) { return false; } + return true; + } } public void ModifyItemQuantityInSellCrate(ItemPrefab itemPrefab, int changeInQuantity) { - PurchasedItem itemToSell = ItemsInSellCrate.Find(i => i.ItemPrefab == itemPrefab); + var itemToSell = ItemsInSellCrate.Find(i => i.ItemPrefab == itemPrefab); if (itemToSell != null) { itemToSell.Quantity += changeInQuantity; @@ -186,72 +134,69 @@ namespace Barotrauma public void SellItems(List itemsToSell, Store.StoreTab sellingMode) { - var sellableItems = sellingMode switch + IEnumerable sellableItems; + try { - Store.StoreTab.Sell => GetSellableItems(Character.Controlled), - Store.StoreTab.SellFromSub => GetSellableItemsFromSub(), - _ => throw new System.NotImplementedException(), - }; + sellableItems = sellingMode switch + { + Store.StoreTab.Sell => GetSellableItems(Character.Controlled), + Store.StoreTab.SellSub => GetSellableItemsFromSub(), + _ => throw new NotImplementedException() + }; + } + catch (NotImplementedException e) + { + DebugConsole.ShowError($"Error selling items: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); + return; + } bool canAddToRemoveQueue = campaign.IsSinglePlayer && Entity.Spawner != null; - var sellerId = GameMain.Client?.ID ?? 0; - + byte sellerId = GameMain.Client?.ID ?? 0; // Check all the prices before starting the transaction // to make sure the modifiers stay the same for the whole transaction Dictionary sellValues = GetSellValuesAtCurrentLocation(itemsToSell.Select(i => i.ItemPrefab)); - foreach (PurchasedItem item in itemsToSell) { - var itemValue = item.Quantity * sellValues[item.ItemPrefab]; - + int itemValue = item.Quantity * sellValues[item.ItemPrefab]; // check if the store can afford the item if (Location.StoreCurrentBalance < itemValue) { continue; } - // TODO: Write logic for prioritizing certain items over others (e.g. lone Battery Cell should be preferred over one inside a Stun Baton) var matchingItems = sellableItems.Where(i => i.Prefab == item.ItemPrefab); - if (matchingItems.Count() <= item.Quantity) + int count = Math.Min(item.Quantity, matchingItems.Count()); + SoldItem.SellOrigin origin = sellingMode == Store.StoreTab.Sell ? SoldItem.SellOrigin.Character : SoldItem.SellOrigin.Submarine; + if (origin == SoldItem.SellOrigin.Character || GameMain.IsSingleplayer) { - foreach (Item i in matchingItems) + for (int i = 0; i < count; i++) { - SoldItems.Add(new SoldItem(i.Prefab, i.ID, canAddToRemoveQueue, sellerId)); - SoldEntities.Add(campaign.IsSinglePlayer ? SoldEntity.CreateInSinglePlayer(i) : SoldEntity.CreateInMultiPlayer(i)); - if (canAddToRemoveQueue) { Entity.Spawner.AddToRemoveQueue(i); } + var matchingItem = matchingItems.ElementAt(i); + SoldItems.Add(new SoldItem(matchingItem.Prefab, matchingItem.ID, canAddToRemoveQueue, sellerId, origin)); + SoldEntities.Add(new SoldEntity(matchingItem, campaign.IsSinglePlayer ? SoldEntity.SellStatus.Confirmed : SoldEntity.SellStatus.Local)); + if (canAddToRemoveQueue) { Entity.Spawner.AddToRemoveQueue(matchingItem); } } } else { - for (int i = 0; i < item.Quantity; i++) + // When selling from the sub in multiplayer, the server will determine the items that are sold + for (int i = 0; i < count; i++) { - var matchingItem = matchingItems.ElementAt(i); - SoldItems.Add(new SoldItem(matchingItem.Prefab, matchingItem.ID, canAddToRemoveQueue, sellerId)); - SoldEntities.Add(campaign.IsSinglePlayer ? SoldEntity.CreateInSinglePlayer(matchingItem) : SoldEntity.CreateInMultiPlayer(matchingItem)); - if (canAddToRemoveQueue) { Entity.Spawner.AddToRemoveQueue(matchingItem); } + SoldItems.Add(new SoldItem(item.ItemPrefab, Entity.NullEntityID, canAddToRemoveQueue, sellerId, origin)); + SoldEntities.Add(new SoldEntity(item.ItemPrefab, SoldEntity.SellStatus.Local)); } } - // Exchange money Location.StoreCurrentBalance -= itemValue; campaign.Money += itemValue; + GameAnalyticsManager.AddMoneyGainedEvent(itemValue, GameAnalyticsManager.MoneySource.Store, item.ItemPrefab.Identifier); // Remove from the sell crate - // TODO: Simplify duplicate logic? - if (sellingMode == Store.StoreTab.Sell && ItemsInSellCrate.Find(pi => pi.ItemPrefab == item.ItemPrefab) is { } inventoryItem) + if ((sellingMode == Store.StoreTab.Sell ? ItemsInSellCrate : ItemsInSellFromSubCrate)?.Find(pi => pi.ItemPrefab == item.ItemPrefab) is { } itemToSell) { - inventoryItem.Quantity -= item.Quantity; - if (inventoryItem.Quantity < 1) + itemToSell.Quantity -= item.Quantity; + if (itemToSell.Quantity < 1) { - ItemsInSellCrate.Remove(inventoryItem); - } - } - else if(sellingMode == Store.StoreTab.SellFromSub && ItemsInSellFromSubCrate.Find(pi => pi.ItemPrefab == item.ItemPrefab) is { } subItem) - { - subItem.Quantity -= item.Quantity; - if (subItem.Quantity < 1) - { - ItemsInSellFromSubCrate.Remove(subItem); + (sellingMode == Store.StoreTab.Sell ? ItemsInSellCrate : ItemsInSellFromSubCrate)?.Remove(itemToSell); } } } - OnSoldItemsChanged?.Invoke(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 43f30d3b4..845af7489 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -83,7 +83,7 @@ namespace Barotrauma partial void InitProjectSpecific() { - guiFrame = new GUIFrame(new RectTransform(Vector2.One, GUICanvas.Instance), null, Color.Transparent) + guiFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), null, Color.Transparent) { CanBeFocused = false }; @@ -145,12 +145,14 @@ namespace Barotrauma string msgCommand = ChatMessage.GetChatMessageCommand(text, out string msg); // add to local history ChatBox.ChatManager.Store(text); + WifiComponent headset = null; + ChatMessageType messageType = + ((msgCommand == "r" || msgCommand == "radio") && ChatMessage.CanUseRadio(Character.Controlled, out headset)) ? ChatMessageType.Radio : ChatMessageType.Default; AddSinglePlayerChatMessage( Character.Controlled.Info.Name, - msg, - ((msgCommand == "r" || msgCommand == "radio") && ChatMessage.CanUseRadio(Character.Controlled)) ? ChatMessageType.Radio : ChatMessageType.Default, + msg, messageType, Character.Controlled); - if (ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent headset)) + if (messageType == ChatMessageType.Radio && headset != null) { Signal s = new Signal(msg, sender: Character.Controlled, source: headset.Item); headset.TransmitSignal(s, sentFromChat: true); @@ -302,7 +304,7 @@ namespace Barotrauma /// /// The character to remove /// If the character info is also removed, the character will not be visible in the round summary. - public void RemoveCharacter(Character character, bool removeInfo = false) + public void RemoveCharacter(Character character, bool removeInfo = false, bool resetCrewListIndex = true) { if (character == null) { @@ -311,14 +313,15 @@ namespace Barotrauma } characters.Remove(character); if (removeInfo) { characterInfos.Remove(character.Info); } + if (resetCrewListIndex) { ResetCrewListIndex(character); } } /// /// Add character to the list without actually adding it to the crew /// - public void AddCharacterToCrewList(Character character) + public GUIComponent AddCharacterToCrewList(Character character) { - if (character == null) { return; } + if (character == null) { return null; } var background = new GUIFrame( new RectTransform(crewListEntrySize, parent: crewList.Content.RectTransform, anchor: Anchor.TopRight), @@ -510,6 +513,8 @@ namespace Barotrauma return true; } }; + + return background; } private void SetCharacterComponentTooltip(GUIComponent characterComponent) @@ -549,13 +554,13 @@ namespace Barotrauma if (characterInfos.Contains(revivedCharacter.Info)) { AddCharacter(revivedCharacter); } } - public void KillCharacter(Character killedCharacter) + public void KillCharacter(Character killedCharacter, bool resetCrewListIndex = true) { if (crewList.Content.GetChildByUserData(killedCharacter) is GUIComponent characterComponent) { CoroutineManager.StartCoroutine(KillCharacterAnim(characterComponent)); } - RemoveCharacter(killedCharacter); + RemoveCharacter(killedCharacter, resetCrewListIndex: resetCrewListIndex); } private IEnumerable KillCharacterAnim(GUIComponent component) @@ -601,9 +606,53 @@ namespace Barotrauma { if (crewList != this.crewList) { return; } if (!(draggedElementData is Character)) { return; } - if (crewList.HasDraggedElementIndexChanged) { return; } if (!IsSinglePlayer) { return; } - CharacterClicked(crewList.DraggedElement, draggedElementData); + if (crewList.HasDraggedElementIndexChanged) + { + UpdateCrewListIndices(); + } + else + { + CharacterClicked(crewList.DraggedElement, draggedElementData); + } + } + + private void ResetCrewListIndex(Character c) + { + if (c?.Info == null) { return; } + c.Info.CrewListIndex = -1; + UpdateCrewListIndices(); + } + + private void UpdateCrewListIndices() + { + if (crewList == null) { return; } + for (int i = 0; i < crewList.Content.CountChildren; i++) + { + var characterComponent = crewList.Content.GetChild(i); + if (!(characterComponent?.UserData is Character c)) { continue; } + if (c.Info == null) { continue; } + c.Info.CrewListIndex = i; + } + } + + private void SortCrewList() + { + if (crewList == null) { return; } + crewList.Content.RectTransform.SortChildren((x, y) => + { + var infoX = (x.GUIComponent.UserData as Character)?.Info?.CrewListIndex; + var infoY = (y.GUIComponent.UserData as Character)?.Info?.CrewListIndex; + if (infoX.HasValue) + { + return infoY.HasValue ? infoX.Value.CompareTo(infoY.Value) : -1; + } + else + { + return infoY.HasValue ? 1 : 0; + } + }); + UpdateCrewListIndices(); } #endregion @@ -718,7 +767,7 @@ namespace Barotrauma /// Sets the character's current order (if it's close enough to receive messages from orderGiver) and /// displays the order in the crew UI /// - public void SetCharacterOrder(Character character, Order order, string option, int priority, Character orderGiver, Hull targetHull = null) + public void SetCharacterOrder(Character character, Order order, string option, int priority, Character orderGiver, Hull targetHull = null, bool isNewOrder = true) { if (order != null && order.TargetAllCharacters) { @@ -768,11 +817,11 @@ namespace Barotrauma if (IsSinglePlayer) { - orderGiver.Speak(order.GetChatMessage("", hull?.DisplayName, givingOrderToSelf: character == orderGiver), ChatMessageType.Order); + orderGiver.Speak(order.GetChatMessage("", hull?.DisplayName, givingOrderToSelf: character == orderGiver, isNewOrder: isNewOrder), ChatMessageType.Order); } else { - OrderChatMessage msg = new OrderChatMessage(order, "", priority, order.IsReport ? hull : order.TargetEntity, null, orderGiver); + OrderChatMessage msg = new OrderChatMessage(order, "", priority, order.IsReport ? hull : order.TargetEntity, null, orderGiver, isNewOrder: isNewOrder); GameMain.Client?.SendChatMessage(msg); } } @@ -784,12 +833,12 @@ namespace Barotrauma if (IsSinglePlayer) { character.SetOrder(order, option, priority, orderGiver, speak: orderGiver != character); - string message = order?.GetChatMessage(character.Name, orderGiver?.CurrentHull?.DisplayName, givingOrderToSelf: character == orderGiver, orderOption: option, priority: priority); + string message = order?.GetChatMessage(character.Name, orderGiver?.CurrentHull?.DisplayName, givingOrderToSelf: character == orderGiver, orderOption: option, isNewOrder: isNewOrder); orderGiver?.Speak(message); } else if (orderGiver != null) { - OrderChatMessage msg = new OrderChatMessage(order, option, priority, order?.TargetSpatialEntity ?? order?.TargetItemComponent?.Item, character, orderGiver); + OrderChatMessage msg = new OrderChatMessage(order, option, priority, order?.TargetSpatialEntity ?? order?.TargetItemComponent?.Item, character, orderGiver, isNewOrder: isNewOrder); GameMain.Client?.SendChatMessage(msg); } } @@ -1071,13 +1120,19 @@ namespace Barotrauma var priority = Math.Max(CharacterInfo.HighestManualOrderPriority - orderList.Content.GetChildIndex(orderComponent), 1); if (orderInfo.ManualPriority == priority) { return; } var character = (Character)orderList.UserData; - SetCharacterOrder(character, orderInfo.Order, orderInfo.OrderOption, priority, Character.Controlled); + SetCharacterOrder(character, orderInfo.Order, orderInfo.OrderOption, priority, Character.Controlled, isNewOrder: false); } private string CreateOrderTooltip(Order orderPrefab, string option, Entity targetEntity) { if (orderPrefab == null) { return ""; } - if (!string.IsNullOrEmpty(option)) + if (orderPrefab.DisplayGiverInTooltip && orderPrefab.OrderGiver != null) + { + return TextManager.GetWithVariables("crewlistordericontooltip", + new string[2] { "[ordername]", "[orderoption]" }, + new string[2] { orderPrefab.Name, orderPrefab.OrderGiver.DisplayName }); + } + else if (!string.IsNullOrEmpty(option)) { return TextManager.GetWithVariables("crewlistordericontooltip", new string[2] { "[ordername]", "[orderoption]" }, @@ -1210,6 +1265,10 @@ namespace Barotrauma DisableCommandUI(); Character.Controlled = character; HintManager.OnChangeCharacter(); + if (GameSession.TabMenuInstance != null && TabMenu.SelectedTab == TabMenu.InfoFrameTab.Talents) + { + GameSession.TabMenuInstance.SelectInfoFrameTab(TabMenu.SelectedTab); + } } private int TryAdjustIndex(int amount) @@ -1566,10 +1625,28 @@ namespace Barotrauma { crewList.Select(character, force: true); } + // Icon colors might change based on the target so we check if they need to be updated + if (GetCurrentOrderIconList(characterComponent) is GUIListBox currentOrderIconList) + { + foreach (var orderIcon in currentOrderIconList.Content.Children) + { + if (!(orderIcon.UserData is OrderInfo orderInfo)) { continue; } + if (!(orderInfo.Order is Order order)) { continue; } + if (order.ColoredWhenControllingGiver && order.OrderGiver != Character.Controlled) + { + orderIcon.Color = AIObjective.ObjectiveIconColor; + } + else + { + orderIcon.Color = order.Color; + } + } + } + // Only update the order highlights and objective icons here in singleplayer + // The server will let the clients know when they need to update in multiplayer if (GameMain.IsSingleplayer && character.IsBot && character.AIController is HumanAIController controller && controller.ObjectiveManager is AIObjectiveManager objectiveManager) { - // In multiplayer, these are set through character networking (the server lets the clients now when these are updated) if (objectiveManager.CurrentObjective is AIObjective currentObjective) { if (objectiveManager.IsOrder(currentObjective)) @@ -1638,8 +1715,8 @@ namespace Barotrauma bool foundMatch = false; foreach (var orderIcon in currentOrderIconList.Content.Children) { - var glowComponent = orderIcon.GetChildByUserData("glow"); - if (glowComponent == null) { continue; } + if (!(orderIcon.GetChildByUserData("glow") is GUIComponent glowComponent)) { continue; } + glowComponent.Color = orderIcon.Color; if (foundMatch) { glowComponent.Visible = false; @@ -1684,11 +1761,11 @@ namespace Barotrauma objectiveIconFrame.ClearChildren(); if (sprite != null) { - var objectiveIcon = CreateNodeIcon(Vector2.One, objectiveIconFrame.RectTransform, sprite, Color.LightGray, tooltip: tooltip); + var objectiveIcon = CreateNodeIcon(Vector2.One, objectiveIconFrame.RectTransform, sprite, AIObjective.ObjectiveIconColor, tooltip: tooltip); new GUIFrame(new RectTransform(new Vector2(1.5f), objectiveIcon.RectTransform, anchor: Anchor.Center), style: "OuterGlowCircular") { CanBeFocused = false, - Color = Color.LightGray + Color = AIObjective.ObjectiveIconColor }; objectiveIconFrame.Visible = true; } @@ -1909,7 +1986,7 @@ namespace Barotrauma ScaleCommandUI(); commandFrame = new GUIFrame( - new RectTransform(Vector2.One, GUICanvas.Instance, anchor: Anchor.Center), + new RectTransform(Vector2.One, GUI.Canvas, anchor: Anchor.Center), style: null, color: Color.Transparent); background = new GUIImage( @@ -2458,6 +2535,7 @@ namespace Barotrauma { shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, dismissedOrderPrefab, -1)); } + shortcutNodes.RemoveAll(n => n.UserData is Order o && !IsOrderAvailable(o)); if (shortcutNodes.Count < 1) { return; } shortcutCenterNode = new GUIFrame(new RectTransform(shortcutCenterNodeSize, parent: commandFrame.RectTransform, anchor: Anchor.Center), style: null) { @@ -2498,7 +2576,7 @@ namespace Barotrauma private void CreateOrderNodes(OrderCategory orderCategory) { - var orders = Order.PrefabList.FindAll(o => o.Category == orderCategory && !o.IsReport); + var orders = Order.PrefabList.FindAll(o => o.Category == orderCategory && !o.IsReport && IsOrderAvailable(o)); Order order; bool disableNode; var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, @@ -2582,15 +2660,12 @@ namespace Barotrauma { contextualOrders.Remove(pumpOrderInfo); } - if (contextualOrders.None()) + orderIdentifier = "cleanupitems"; + if (contextualOrders.None(info => info.Order.Identifier.Equals(orderIdentifier))) { - orderIdentifier = "cleanupitems"; - if (contextualOrders.None(info => info.Order.Identifier.Equals(orderIdentifier))) + if (AIObjectiveCleanupItems.IsValidTarget(itemContext, Character.Controlled, checkInventory: false) || AIObjectiveCleanupItems.IsValidContainer(itemContext, Character.Controlled)) { - if (AIObjectiveCleanupItems.IsValidTarget(itemContext, Character.Controlled, checkInventory: false) || AIObjectiveCleanupItems.IsValidContainer(itemContext, Character.Controlled)) - { - contextualOrders.Add(new OrderInfo(new Order(Order.GetPrefab(orderIdentifier), itemContext, targetItem: null, Character.Controlled), null)); - } + contextualOrders.Add(new OrderInfo(new Order(Order.GetPrefab(orderIdentifier), itemContext, targetItem: null, Character.Controlled), null)); } } AddIgnoreOrder(itemContext); @@ -2653,6 +2728,7 @@ namespace Barotrauma contextualOrders.Add(new OrderInfo(Order.GetPrefab(orderIdentifier), null)); } } + contextualOrders.RemoveAll(o => !IsOrderAvailable(o.Order)); var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, contextualOrders.Count, MathHelper.ToRadians(90f + 180f / contextualOrders.Count)); bool disableNode = !CanCharacterBeHeard(); for (int i = 0; i < contextualOrders.Count; i++) @@ -3411,6 +3487,20 @@ namespace Barotrauma return character?.Info?.GetManualOrderPriority(order) ?? CharacterInfo.HighestManualOrderPriority; } + private bool IsOrderAvailable(Order order) + { + if (order == null) { return false; } + switch (order.Identifier.ToLowerInvariant()) + { + case "assaultenemy": + Character character = characterContext ?? Character.Controlled; + if (character?.Submarine == null) { return false; } + return character.Submarine.GetConnectedSubs().Any(s => s.TeamID != character.TeamID); + default: + return true; + } + } + #region Crew Member Assignment Logic private bool CanOpenManualAssignment(GUIComponent node) { @@ -3487,7 +3577,7 @@ namespace Barotrauma bool hasLeaks = Character.Controlled.CurrentHull.ConnectedGaps.Any(g => !g.IsRoomToRoom && g.Open > 0.0f); ToggleReportButton("reportbreach", hasLeaks); - bool hasIntruders = Character.CharacterList.Any(c => c.CurrentHull == Character.Controlled.CurrentHull && AIObjectiveFightIntruders.IsValidTarget(c, Character.Controlled)); + bool hasIntruders = Character.CharacterList.Any(c => c.CurrentHull == Character.Controlled.CurrentHull && AIObjectiveFightIntruders.IsValidTarget(c, Character.Controlled, false)); ToggleReportButton("reportintruders", hasIntruders); foreach (GUIComponent reportButton in ReportButtonFrame.Children) @@ -3523,16 +3613,6 @@ namespace Barotrauma InitRound(); } - public void EndRound() - { - //remove characterinfos whose characters have been removed or killed - characterInfos.RemoveAll(c => c.Character == null || c.Character.Removed || c.CauseOfDeath != null); - - characters.Clear(); - crewList.ClearChildren(); - GUIContextMenu.CurrentContextMenu = null; - } - public void Reset() { characters.Clear(); @@ -3543,12 +3623,14 @@ namespace Barotrauma public void Save(XElement parentElement) { XElement element = new XElement("crew"); - foreach (CharacterInfo ci in characterInfos) + for (int i = 0; i < characterInfos.Count; i++) { + var ci = characterInfos[i]; var infoElement = ci.Save(element); if (ci.InventoryData != null) { infoElement.Add(ci.InventoryData); } if (ci.HealthData != null) { infoElement.Add(ci.HealthData); } if (ci.OrderData != null) { infoElement.Add(ci.OrderData); } + infoElement.Add(new XAttribute("crewlistindex", ci.CrewListIndex)); if (ci.LastControlled) { infoElement.Add(new XAttribute("lastcontrolled", true)); } } SaveActiveOrders(element); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index 95d8d567e..6fa04747c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -97,17 +97,16 @@ namespace Barotrauma /// /// There is a server-side implementation of the method in /// - public bool AllowedToManageCampaign() + public bool AllowedToManageCampaign(ClientPermissions permissions = ClientPermissions.ManageCampaign) { - //allow ending the round if the client has permissions, is the owner, the only client in the server, + //allow managing the round if the client has permissions, is the owner, the only client in the server, //or if no-one has management permissions if (GameMain.Client == null) { return true; } return - GameMain.Client.HasPermission(ClientPermissions.ManageCampaign) || + GameMain.Client.HasPermission(permissions) || GameMain.Client.ConnectedClients.Count == 1 || GameMain.Client.IsServerOwner || - GameMain.Client.ConnectedClients.None(c => - c.InGame && (c.IsOwner || c.HasPermission(ClientPermissions.ManageCampaign))); + GameMain.Client.ConnectedClients.None(c => c.InGame && (c.IsOwner || c.HasPermission(permissions))); } public override void Draw(SpriteBatch spriteBatch) @@ -242,6 +241,11 @@ namespace Barotrauma { ReadyCheckButton.RectTransform.ScreenSpaceOffset = endRoundButton.RectTransform.ScreenSpaceOffset; ReadyCheckButton.DrawManually(spriteBatch); + if (ReadyCheck.ReadyCheckCooldown > DateTime.Now) + { + float progress = (ReadyCheck.ReadyCheckCooldown - DateTime.Now).Seconds / 60.0f; + ReadyCheckButton.Color = ToolBox.GradientLerp(progress, Color.White, GUI.Style.Red); + } } } @@ -290,6 +294,9 @@ namespace Barotrauma case InteractionType.Crew when GameMain.NetworkMember != null: CampaignUI.CrewManagement.SendCrewState(false); goto default; + case InteractionType.MedicalClinic: + CampaignUI.MedicalClinic.RequestLatestPending(); + goto default; default: ShowCampaignUI = true; CampaignUI.SelectTab(npc.CampaignInteractionType); @@ -319,6 +326,8 @@ namespace Barotrauma { base.Update(deltaTime); + MedicalClinic?.Update(deltaTime); + if (PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Escape)) { GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData is RoundSummary); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index a0876e3da..167fab3e8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -97,7 +97,7 @@ namespace Barotrauma partial void InitProjSpecific() { - var buttonContainer = new GUILayoutGroup(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.ButtonAreaTop, GUICanvas.Instance), + var buttonContainer = new GUILayoutGroup(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.ButtonAreaTop, GUI.Canvas), isHorizontal: true, childAnchor: Anchor.CenterRight) { CanBeFocused = false @@ -108,7 +108,7 @@ namespace Barotrauma buttonCenter = buttonHeight / 2, screenMiddle = GameMain.GraphicsWidth / 2; - endRoundButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle(screenMiddle - buttonWidth / 2, HUDLayoutSettings.ButtonAreaTop.Center.Y - buttonCenter, buttonWidth, buttonHeight), GUICanvas.Instance), + endRoundButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle(screenMiddle - buttonWidth / 2, HUDLayoutSettings.ButtonAreaTop.Center.Y - buttonCenter, buttonWidth, buttonHeight), GUI.Canvas), TextManager.Get("EndRound"), textAlignment: Alignment.Center, style: "EndRoundButton") { Pulse = true, @@ -145,7 +145,7 @@ namespace Barotrauma int readyButtonHeight = buttonHeight; int readyButtonWidth = (int) (GUI.Scale * 50); - ReadyCheckButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle(screenMiddle + (buttonWidth / 2) + GUI.IntScale(16), HUDLayoutSettings.ButtonAreaTop.Center.Y - buttonCenter, readyButtonWidth, readyButtonHeight), GUICanvas.Instance), + ReadyCheckButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle(screenMiddle + (buttonWidth / 2) + GUI.IntScale(16), HUDLayoutSettings.ButtonAreaTop.Center.Y - buttonCenter, readyButtonWidth, readyButtonHeight), GUI.Canvas), style: "RepairBuyButton") { ToolTip = TextManager.Get("ReadyCheck.Tooltip"), @@ -545,14 +545,21 @@ namespace Barotrauma foreach (PurchasedItem pi in CargoManager.ItemsInBuyCrate) { msg.Write(pi.ItemPrefab.Identifier); - msg.WriteRangedInteger(pi.Quantity, 0, 100); + msg.WriteRangedInteger(pi.Quantity, 0, CargoManager.MaxQuantity); + } + + msg.Write((UInt16)CargoManager.ItemsInSellFromSubCrate.Count); + foreach (PurchasedItem pi in CargoManager.ItemsInSellFromSubCrate) + { + msg.Write(pi.ItemPrefab.Identifier); + msg.WriteRangedInteger(pi.Quantity, 0, CargoManager.MaxQuantity); } msg.Write((UInt16)CargoManager.PurchasedItems.Count); foreach (PurchasedItem pi in CargoManager.PurchasedItems) { msg.Write(pi.ItemPrefab.Identifier); - msg.WriteRangedInteger(pi.Quantity, 0, 100); + msg.WriteRangedInteger(pi.Quantity, 0, CargoManager.MaxQuantity); } msg.Write((UInt16)CargoManager.SoldItems.Count); @@ -562,6 +569,7 @@ namespace Barotrauma msg.Write((UInt16)si.ID); msg.Write(si.Removed); msg.Write(si.SellerID); + msg.Write((byte)si.Origin); } msg.Write((ushort)UpgradeManager.PurchasedUpgrades.Count); @@ -640,6 +648,15 @@ namespace Barotrauma buyCrateItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity)); } + UInt16 subSellCrateItemCount = msg.ReadUInt16(); + List subSellCrateItems = new List(); + for (int i = 0; i < subSellCrateItemCount; i++) + { + string itemPrefabIdentifier = msg.ReadString(); + int itemQuantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity); + subSellCrateItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity)); + } + UInt16 purchasedItemCount = msg.ReadUInt16(); List purchasedItems = new List(); for (int i = 0; i < purchasedItemCount; i++) @@ -657,7 +674,8 @@ namespace Barotrauma UInt16 id = msg.ReadUInt16(); bool removed = msg.ReadBoolean(); byte sellerId = msg.ReadByte(); - soldItems.Add(new SoldItem(ItemPrefab.Prefabs[itemPrefabIdentifier], id, removed, sellerId)); + byte origin = msg.ReadByte(); + soldItems.Add(new SoldItem(ItemPrefab.Prefabs[itemPrefabIdentifier], id, removed, sellerId, (SoldItem.SellOrigin)origin)); } ushort pendingUpgradeCount = msg.ReadUInt16(); @@ -678,13 +696,9 @@ namespace Barotrauma for (int i = 0; i < purchasedItemSwapCount; i++) { UInt16 itemToRemoveID = msg.ReadUInt16(); - Item itemToRemove = Entity.FindEntityByID(itemToRemoveID) as Item; - string itemToInstallIdentifier = msg.ReadString(); ItemPrefab itemToInstall = string.IsNullOrEmpty(itemToInstallIdentifier) ? null : ItemPrefab.Find(string.Empty, itemToInstallIdentifier); - - if (itemToRemove == null) { continue; } - + if (!(Entity.FindEntityByID(itemToRemoveID) is Item itemToRemove)) { continue; } purchasedItemSwaps.Add(new PurchasedItemSwap(itemToRemove, itemToInstall)); } @@ -728,12 +742,11 @@ namespace Barotrauma campaign.Map.SelectMission(selectedMissionIndices); campaign.Map.AllowDebugTeleport = allowDebugTeleport; campaign.CargoManager.SetItemsInBuyCrate(buyCrateItems); + campaign.CargoManager.SetItemsInSubSellCrate(subSellCrateItems); campaign.CargoManager.SetPurchasedItems(purchasedItems); campaign.CargoManager.SetSoldItems(soldItems); if (storeBalance.HasValue) { campaign.Map.CurrentLocation.StoreCurrentBalance = storeBalance.Value; } campaign.UpgradeManager.SetPendingUpgrades(pendingUpgrades); - campaign.UpgradeManager.PurchasedUpgrades.Clear(); - campaign.UpgradeManager.PurchasedUpgrades.Clear(); foreach (var purchasedItemSwap in purchasedItemSwaps) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index 3c376f453..1cc228e61 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -108,6 +108,9 @@ namespace Barotrauma case "pets": petsElement = subElement; break; + case "stats": + LoadStats(subElement); + break; } } @@ -167,7 +170,7 @@ namespace Barotrauma int buttonHeight = (int)(GUI.Scale * 40); int buttonWidth = GUI.IntScale(450); - endRoundButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle((GameMain.GraphicsWidth / 2) - (buttonWidth / 2), HUDLayoutSettings.ButtonAreaTop.Center.Y - (buttonHeight / 2), buttonWidth, buttonHeight), GUICanvas.Instance), + endRoundButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle((GameMain.GraphicsWidth / 2) - (buttonWidth / 2), HUDLayoutSettings.ButtonAreaTop.Center.Y - (buttonHeight / 2), buttonWidth, buttonHeight), GUI.Canvas), TextManager.Get("EndRound"), textAlignment: Alignment.Center, style: "EndRoundButton") { Pulse = true, @@ -412,6 +415,10 @@ namespace Barotrauma break; case TransitionType.ProgressToNextLocation: Map.MoveToNextLocation(); + TotalPassedLevels++; + break; + case TransitionType.ProgressToNextEmptyLocation: + TotalPassedLevels++; break; } @@ -728,6 +735,7 @@ namespace Barotrauma new XAttribute("purchaseditemrepairs", PurchasedItemRepairs), new XAttribute("cheatsenabled", CheatsEnabled)); modeElement.Add(Settings.Save()); + modeElement.Add(SaveStats()); //save and remove all items that are in someone's inventory so they don't get included in the sub file as well foreach (Character c in Character.CharacterList) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs index 8f22864c3..5ad157c47 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs @@ -220,12 +220,12 @@ namespace Barotrauma.Tutorials SetHighlight(captain_navConsole.Item, true); SetHighlight(captain_sonar.Item, true); SetHighlight(captain_statusMonitor, true); - captain_navConsole.UseAutoDocking = false; do { //captain_navConsoleCustomInterface.HighlightElement(0, uiHighlightColor, duration: 1.0f, pulsateAmount: 0.0f); yield return new WaitForSeconds(1.0f, false); } while (Submarine.MainSub.DockedTo.Any()); + captain_navConsole.UseAutoDocking = false; RemoveCompletedObjective(segments[4]); yield return new WaitForSeconds(2f, false); TriggerTutorialSegment(5); // Navigate to destination diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs index 007bef266..113f7e5c3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs @@ -172,7 +172,7 @@ namespace Barotrauma } else { - indicator.Visible = Character.Controlled.Info.GetAvailableTalentPoints() > 0; + indicator.Visible = Character.Controlled.Info.GetAvailableTalentPoints() > 0 && !Character.Controlled.HasUnlockedAllTalents(); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs index 3a22e29d8..40cff56c3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs @@ -12,6 +12,8 @@ namespace Barotrauma static class HintManager { private const string HintManagerFile = "hintmanager.xml"; + + public static bool Enabled => GameMain.Config != null && !GameMain.Config.DisableInGameHints; private static HashSet HintIdentifiers { get; set; } private static Dictionary> HintTags { get; } = new Dictionary>(); private static Dictionary HintOrders { get; } = new Dictionary(); @@ -666,6 +668,8 @@ namespace Barotrauma ActiveHintMessageBox.InnerFrame.Flash(color: iconColor ?? Color.Orange, flashDuration: 0.75f); onDisplay?.Invoke(); + GameAnalyticsManager.AddDesignEvent($"HintManager:{GameMain.GameSession?.GameMode?.Preset?.Identifier ?? "none"}:HintDisplayed:{hintIdentifier}"); + return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs new file mode 100644 index 000000000..26f8f67b8 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs @@ -0,0 +1,391 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Barotrauma.Extensions; +using Barotrauma.Networking; + +namespace Barotrauma +{ + internal partial class MedicalClinic + { + public enum RequestResult + { + Undecided, + Success, + Error, + Timeout + } + + public readonly struct RequestAction + { + public readonly Action Callback; + public readonly DateTimeOffset Timeout; + + public RequestAction(Action callback, DateTimeOffset timeout) + { + Callback = callback; + Timeout = timeout; + } + } + + public readonly struct AfflictionRequest + { + public readonly RequestResult Result; + public readonly ImmutableArray Afflictions; + + public AfflictionRequest(RequestResult result, ImmutableArray afflictions) + { + Result = result; + Afflictions = afflictions; + } + } + + public readonly struct PendingRequest + { + public readonly RequestResult Result; + public readonly ImmutableArray CrewMembers; + + public PendingRequest(RequestResult result, ImmutableArray crewMembers) + { + Result = result; + CrewMembers = crewMembers; + } + } + + public readonly struct CallbackOnlyRequest + { + public readonly RequestResult Result; + + public CallbackOnlyRequest(RequestResult result) + { + Result = result; + } + } + + public readonly struct HealRequest + { + public readonly RequestResult Result; + public readonly HealRequestResult HealResult; + + public HealRequest(RequestResult result, HealRequestResult healResult) + { + Result = result; + HealResult = healResult; + } + } + + private readonly List> afflictionRequests = new List>(); + private readonly List> pendingHealRequests = new List>(); + private readonly List> clearAllRequests = new List>(); + private readonly List> healAllRequests = new List>(); + private readonly List> addRequests = new List>(); + private readonly List> removeRequests = new List>(); + + public void RequestAfflictions(CharacterInfo info, Action onReceived) + { + if (GameMain.IsSingleplayer) + { +#if DEBUG && LINUX + if (Screen.Selected is TestScreen) + { + onReceived.Invoke(new AfflictionRequest(RequestResult.Success, TestAfflictions.ToImmutableArray())); + return; + } +#endif + + if (!(info is { Character: { CharacterHealth: { } health } })) + { + onReceived.Invoke(new AfflictionRequest(RequestResult.Error, ImmutableArray.Empty)); + return; + } + + ImmutableArray pendingAfflictions = GetAllAfflictions(health).ToImmutableArray(); + onReceived.Invoke(new AfflictionRequest(RequestResult.Success, pendingAfflictions)); + return; + } + + afflictionRequests.Add(new RequestAction(onReceived, GetTimeout())); + SendAfflictionRequest(info); + } + + public void RequestLatestPending(Action onReceived) + { + // no need to worry about syncing when there's only one pair of eyes capable of looking at the UI + if (GameMain.IsSingleplayer) { return; } + + pendingHealRequests.Add(new RequestAction(onReceived, GetTimeout())); + SendPendingRequest(); + } + + public void Update(float deltaTime) + { + DateTimeOffset now = DateTimeOffset.Now; + UpdateQueue(afflictionRequests, now, onTimeout: callback => { callback(new AfflictionRequest(RequestResult.Timeout, ImmutableArray.Empty)); }); + UpdateQueue(pendingHealRequests, now, onTimeout: callback => { callback(new PendingRequest(RequestResult.Timeout, ImmutableArray.Empty)); }); + UpdateQueue(healAllRequests, now, onTimeout: callback => { callback(new HealRequest(RequestResult.Timeout, HealRequestResult.Unknown)); }); + UpdateQueue(clearAllRequests, now, onTimeout: CallbackOnlyTimeout); + UpdateQueue(addRequests, now, onTimeout: CallbackOnlyTimeout); + UpdateQueue(removeRequests, now, onTimeout: CallbackOnlyTimeout); + + void CallbackOnlyTimeout(Action callback) { callback(new CallbackOnlyRequest(RequestResult.Timeout)); } + } + + public bool IsAfflictionPending(NetCrewMember character, NetAffliction affliction) + { + foreach (NetCrewMember crewMember in PendingHeals) + { + if (!crewMember.CharacterEquals(character)) { continue; } + + return crewMember.Afflictions.Any(a => a.AfflictionEquals(affliction)); + } + + return false; + } + + private static bool TryDequeue(List> requestQueue, out Action result) + { + RequestAction? first = requestQueue.FirstOrNull(); + if (!(first is { } action)) + { + result = _ => { }; + return false; + } + + requestQueue.Remove(action); + result = action.Callback; + return true; + } + + private static void UpdateQueue(List> requestQueue, DateTimeOffset now, Action> onTimeout) + { + HashSet>? removals = null; + foreach (RequestAction action in requestQueue) + { + if (action.Timeout < now) + { + onTimeout.Invoke(action.Callback); + + removals ??= new HashSet>(); + removals.Add(action); + } + } + + if (removals is null) { return; } + + foreach (RequestAction action in removals) + { + requestQueue.Remove(action); + } + } + + // 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()); + + private static int GetPing() + { + if (GameMain.IsSingleplayer || !(GameMain.Client?.Name is { } ownName) || !(GameMain.NetworkMember?.ConnectedClients is { } clients)) { return 0; } + + return (from client in clients where client.Name == ownName select client.Ping).FirstOrDefault(); + } + + public void HealAllButtonAction(Action onReceived) + { + if (GameMain.IsSingleplayer) + { + HealRequestResult result = HealAllPending(); + onReceived(new HealRequest(RequestResult.Success, HealAllPending())); + if (result == HealRequestResult.Success) + { + OnUpdate?.Invoke(); + } + + return; + } + + if (campaign?.CampaignUI?.MedicalClinic is { } ui) + { + ui.ClosePopup(); + } + + healAllRequests.Add(new RequestAction(onReceived, GetTimeout())); + ClientSend(null, NetworkHeader.HEAL_PENDING, DeliveryMethod.Reliable); + } + + public void ClearAllButtonAction(Action onReceived) + { + if (GameMain.IsSingleplayer) + { + ClearPendingHeals(); + onReceived(new CallbackOnlyRequest(RequestResult.Success)); + OnUpdate?.Invoke(); + return; + } + + clearAllRequests.Add(new RequestAction(onReceived, GetTimeout())); + ClientSend(null, NetworkHeader.CLEAR_PENDING, DeliveryMethod.Reliable); + } + + private void ClearRequstReceived() + { + ClearPendingHeals(); + if (TryDequeue(clearAllRequests, out var callback)) + { + callback(new CallbackOnlyRequest(RequestResult.Success)); + } + OnUpdate?.Invoke(); + } + + private void HealRequestReceived(IReadMessage inc) + { + NetHealRequest request = INetSerializableStruct.Read(inc); + if (request.Result == HealRequestResult.Success) + { + HealAllPending(force: true); + } + + if (TryDequeue(healAllRequests, out var callback)) + { + callback(new HealRequest(RequestResult.Success, request.Result)); + } + + OnUpdate?.Invoke(); + } + + public void AddPendingButtonAction(NetCrewMember crewMember, Action onReceived) + { + if (GameMain.IsSingleplayer) + { + InsertPendingCrewMember(crewMember); + onReceived(new CallbackOnlyRequest(RequestResult.Success)); + OnUpdate?.Invoke(); + return; + } + + addRequests.Add(new RequestAction(onReceived, GetTimeout())); + ClientSend(crewMember, NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable); + } + + public void RemovePendingButtonAction(NetCrewMember crewMember, NetAffliction affliction, Action onReceived) + { + if (GameMain.IsSingleplayer) + { + RemovePendingAffliction(crewMember, affliction); + onReceived(new CallbackOnlyRequest(RequestResult.Success)); + OnUpdate?.Invoke(); + return; + } + + INetSerializableStruct removedAffliction = new NetRemovedAffliction + { + CrewMember = crewMember, + Affliction = affliction + }; + + removeRequests.Add(new RequestAction(onReceived, GetTimeout())); + ClientSend(removedAffliction, NetworkHeader.REMOVE_PENDING, DeliveryMethod.Reliable); + } + + private void NewAdditonReceived(IReadMessage inc, MessageFlag flag) + { + NetCrewMember crewMember = INetSerializableStruct.Read(inc); + InsertPendingCrewMember(crewMember); + if (flag == MessageFlag.Response && TryDequeue(addRequests, out var callback)) + { + callback(new CallbackOnlyRequest(RequestResult.Success)); + } + OnUpdate?.Invoke(); + } + + private void NewRemovalReceived(IReadMessage inc, MessageFlag flag) + { + NetRemovedAffliction removed = INetSerializableStruct.Read(inc); + RemovePendingAffliction(removed.CrewMember, removed.Affliction); + if (flag == MessageFlag.Response && TryDequeue(removeRequests, out var callback)) + { + callback(new CallbackOnlyRequest(RequestResult.Success)); + } + OnUpdate?.Invoke(); + } + + private static void SendAfflictionRequest(CharacterInfo info) + { + INetSerializableStruct crewMember = new NetCrewMember + { + CharacterInfo = info, + Afflictions = Array.Empty() + }; + + ClientSend(crewMember, NetworkHeader.REQUEST_AFFLICTIONS, DeliveryMethod.Unreliable); + } + + private static void SendPendingRequest() + { + ClientSend(null, NetworkHeader.REQUEST_PENDING, DeliveryMethod.Reliable); + } + + private void AfflictionRequestReceived(IReadMessage inc) + { + NetCrewMember crewMember = INetSerializableStruct.Read(inc); + if (TryDequeue(afflictionRequests, out var callback)) + { + RequestResult result = crewMember.CharacterInfoID == 0 ? RequestResult.Error : RequestResult.Success; + callback(new AfflictionRequest(result, crewMember.Afflictions.ToImmutableArray())); + } + } + + private void PendingRequestReceived(IReadMessage inc) + { + NetPendingCrew pendingCrew = INetSerializableStruct.Read(inc); + if (TryDequeue(pendingHealRequests, out var callback)) + { + callback(new PendingRequest(RequestResult.Success, pendingCrew.CrewMembers.ToImmutableArray())); + } + } + + private static IWriteMessage StartSending() + { + IWriteMessage writeMessage = new WriteOnlyMessage(); + writeMessage.Write((byte)ClientPacketHeader.MEDICAL); + return writeMessage; + } + + private static void ClientSend(INetSerializableStruct? netStruct, NetworkHeader header, DeliveryMethod deliveryMethod) + { + IWriteMessage msg = StartSending(); + msg.Write((byte)header); + netStruct?.Write(msg); + GameMain.Client.ClientPeer?.Send(msg, deliveryMethod); + } + + public void ClientRead(IReadMessage inc) + { + NetworkHeader header = (NetworkHeader)inc.ReadByte(); + MessageFlag flag = (MessageFlag)inc.ReadByte(); + + switch (header) + { + case NetworkHeader.REQUEST_AFFLICTIONS: + AfflictionRequestReceived(inc); + break; + case NetworkHeader.REQUEST_PENDING: + PendingRequestReceived(inc); + break; + case NetworkHeader.ADD_PENDING: + NewAdditonReceived(inc, flag); + break; + case NetworkHeader.REMOVE_PENDING: + NewRemovalReceived(inc, flag); + break; + case NetworkHeader.HEAL_PENDING: + HealRequestReceived(inc); + break; + case NetworkHeader.CLEAR_PENDING: + ClearRequstReceived(); + break; + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs index bb01da050..e6d7114bc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs @@ -31,7 +31,7 @@ namespace Barotrauma private GUIMessageBox? msgBox; private GUIMessageBox? resultsBox; - public static DateTime lastReadyCheck = DateTime.MinValue; + public static DateTime ReadyCheckCooldown = DateTime.MinValue; public static bool IsReadyCheck(GUIComponent? msgBox) => msgBox?.UserData as string == PromptData || msgBox?.UserData as string == ResultData; @@ -273,10 +273,10 @@ namespace Barotrauma public static void CreateReadyCheck() { - if (lastReadyCheck < DateTime.Now) + if (ReadyCheckCooldown < DateTime.Now) { #if !DEBUG - lastReadyCheck = DateTime.Now.AddMinutes(1); + ReadyCheckCooldown = DateTime.Now.AddMinutes(1); #endif IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte) ClientPacketHeader.READY_CHECK); @@ -285,7 +285,7 @@ namespace Barotrauma return; } - GUIMessageBox msgBox = new GUIMessageBox(readyCheckHeader, readyCheckPleaseWait((lastReadyCheck - DateTime.Now).Seconds), new[] { closeButton }); + GUIMessageBox msgBox = new GUIMessageBox(readyCheckHeader, readyCheckPleaseWait((ReadyCheckCooldown - DateTime.Now).Seconds), new[] { closeButton }); msgBox.Buttons[0].OnClicked = delegate { msgBox.Close(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs index 251487afb..fd2e7a7cd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs @@ -524,6 +524,7 @@ namespace Barotrauma return true; }; +#if !OSX var statisticsTickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.045f), leftPanel.RectTransform), TextManager.Get("statisticsconsenttickbox")) { OnSelected = (GUITickBox tickBox) => @@ -562,6 +563,15 @@ namespace Barotrauma statisticsTickBox.OnSelected = prevHandler; statisticsTickBox.Enabled = GameAnalyticsManager.UserConsented != GameAnalyticsManager.Consent.Error; }); +#endif + + foreach (var child in leftPanel.Children) + { + if (child is GUITextBlock textBlock) + { + textBlock.RectTransform.MinSize = new Point(textBlock.RectTransform.MinSize.X, (int)Math.Max(textBlock.RectTransform.MinSize.Y, textBlock.TextSize.Y)); + } + } // right panel -------------------------------------- @@ -597,6 +607,10 @@ namespace Barotrauma OnClicked = (bt, userdata) => { SelectTab((Tab)userdata); return true; } }; tabButtons[(int)tab].Text = ToolBox.LimitString(buttonText, tabButtons[(int)tab].Font, (int)(0.75f * tabWidth * tabButtonHolder.Rect.Width)); + if (tabButtons[(int)tab].Text != buttonText) + { + tabButtons[(int)tab].ToolTip = buttonText; + } } /// Graphics tab -------------------------------------------------------------- diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 0fe573f07..88d4d7135 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -153,12 +153,6 @@ namespace Barotrauma return container.Inventory; } - protected override void PutItem(Item item, int i, Character user, bool removeItem = true, bool createNetworkEvent = true) - { - base.PutItem(item, i, user, removeItem, createNetworkEvent); - CreateSlots(); - } - public override void CreateSlots() { visualSlots ??= new VisualSlot[capacity]; @@ -504,6 +498,13 @@ namespace Barotrauma { HUDLayoutSettings.InventoryTopY = visualSlots[0].EquipButtonRect.Y - (int)(15 * GUI.Scale); } + else + { + for (int i = 0; i < capacity; i++) + { + visualSlots[i].DrawOffset = Vector2.Zero; + } + } } protected override void ControlInput(Camera cam) @@ -789,7 +790,7 @@ namespace Barotrauma if (quickUseAction != QuickUseAction.Drop) { slot.QuickUseButtonToolTip = quickUseAction == QuickUseAction.None ? - "" : TextManager.GetWithVariable("QuickUseAction." + quickUseAction.ToString(), "[equippeditem]", item?.Name); + "" : TextManager.GetWithVariable("QuickUseAction." + quickUseAction.ToString(), "[equippeditem]", character.HeldItems.FirstOrDefault()?.Name ?? item?.Name); if (PlayerInput.PrimaryMouseButtonDown()) { slot.EquipButtonState = GUIComponent.ComponentState.Pressed; } if (PlayerInput.PrimaryMouseButtonClicked()) { @@ -963,7 +964,9 @@ namespace Barotrauma { return QuickUseAction.TakeFromCharacter; } - else if (character.HeldItems.Any(i => i.OwnInventory != null && i.OwnInventory.CanBePut(item)) && allowInventorySwap) + else if (character.HeldItems.Any(i => + i.OwnInventory != null && + (i.OwnInventory.CanBePut(item) || (i.OwnInventory.Capacity == 1 && i.OwnInventory.AllowSwappingContainedItems && i.OwnInventory.Container.CanBeContained(item))))) { return QuickUseAction.PutToEquippedItem; } @@ -1128,8 +1131,9 @@ namespace Barotrauma case QuickUseAction.PutToEquippedItem: foreach (Item heldItem in character.HeldItems) { - if (heldItem.OwnInventory != null && - heldItem.OwnInventory.TryPutItem(item, Character.Controlled)) + if (heldItem.OwnInventory == null) { continue; } + if (heldItem.OwnInventory.TryPutItem(item, Character.Controlled) || + (heldItem.OwnInventory.Capacity == 1 && heldItem.OwnInventory.TryPutItem(item, 0, allowSwapping: true, allowCombine: false, user: Character.Controlled))) { success = true; for (int j = 0; j < capacity; j++) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/EntitySpawnerComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/EntitySpawnerComponent.cs index cd8d21dcf..97c661961 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/EntitySpawnerComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/EntitySpawnerComponent.cs @@ -28,8 +28,8 @@ namespace Barotrauma.Items.Components } case AreaShape.Circle: Vector2 center = item.WorldPosition; - center.Y = -center.Y; center += SpawnAreaOffset; + center.Y = -center.Y; spriteBatch.DrawCircle(center, SpawnAreaRadius, 32, GUI.Style.Red, thickness: 4f); if (MaximumAmountRangePadding > 0f) @@ -51,8 +51,8 @@ namespace Barotrauma.Items.Components } case AreaShape.Circle: Vector2 center = item.WorldPosition; - center.Y = -center.Y; center += CrewAreaOffset; + center.Y = -center.Y; spriteBatch.DrawCircle(center, CrewAreaRadius, 32, GUI.Style.Green); break; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs index d57f1ee61..b3057fb3e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs @@ -256,6 +256,10 @@ namespace Barotrauma.Items.Components { targetHull.IncreaseSectionColorOrStrength(targetSections[i], color, sizeAdjustedSprayStrength * deltaTime, true, false); } + if (GameMain.GameSession != null) + { + GameMain.GameSession.TimeSpentCleaning += deltaTime; + } } else { @@ -263,6 +267,10 @@ namespace Barotrauma.Items.Components { targetHull.CleanSection(targetSections[i], -sizeAdjustedSprayStrength * deltaTime, true); } + if (GameMain.GameSession != null) + { + GameMain.GameSession.TimeSpentPainting += deltaTime; + } } Vector2 particleStartPos = item.WorldPosition + ConvertUnits.ToDisplayUnits(TransformedBarrelPos); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index f42e06aa0..49d0725d2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -143,7 +143,7 @@ namespace Barotrauma.Items.Components } } - public GUIFrame GuiFrame { get; protected set; } + public GUIFrame GuiFrame { get; set; } [Serialize(false, false)] public bool AllowUIOverlap @@ -390,10 +390,10 @@ namespace Barotrauma.Items.Components if (SerializableProperties.TryGetValue(sound.VolumeProperty, out SerializableProperty property)) { - float newVolume = 0.0f; + float newVolume; try { - newVolume = (float)property.GetValue(this); + newVolume = property.GetFloatValue(this); } catch { @@ -554,7 +554,7 @@ namespace Barotrauma.Items.Components color = GuiFrameSource.GetAttributeColor("color", Color.White); } string style = GuiFrameSource.Attribute("style") == null ? null : GuiFrameSource.GetAttributeString("style", ""); - GuiFrame = new GUIFrame(RectTransform.Load(GuiFrameSource, GUI.Canvas.ItemComponentHolder, Anchor.Center), style, color); + GuiFrame = new GUIFrame(RectTransform.Load(GuiFrameSource, GUI.Canvas, Anchor.Center), style, color); DefaultLayout = GUILayoutSettings.Load(GuiFrameSource); if (GuiFrame != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index e087b99fa..2154f25ea 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -76,6 +76,9 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, false, description: "If true, the contained state indicator calculates how full the item is based on the total amount of items that can be stacked inside it, as opposed to how many of the inventory slots are occupied.")] + public bool ShowTotalStackCapacityInContainedStateIndicator { get; set; } + [Serialize(false, false, description: "Should the inventory of this item be kept open when the item is equipped by a character.")] public bool KeepOpenWhenEquipped { get; set; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index ec2074a32..c2a3d7957 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -40,7 +40,7 @@ namespace Barotrauma.Items.Components } } - partial void SetLightSourceTransform() + partial void SetLightSourceTransformProjSpecific() { if (ParentBody != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs index 994deb666..0eb2cbf58 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs @@ -6,21 +6,23 @@ namespace Barotrauma.Items.Components { partial class Controller : ItemComponent { + private bool chatBoxOriginalState; + private bool isHUDsHidden; + public override void DrawHUD(SpriteBatch spriteBatch, Character character) { if (focusTarget != null && character.ViewTarget == focusTarget) { foreach (ItemComponent ic in focusTarget.Components) { - ic.DrawHUD(spriteBatch, character); + if (ic.ShouldDrawHUD(character)) + { + ic.DrawHUD(spriteBatch, character); + } } } } - private bool crewAreaOriginalState; - private bool chatBoxOriginalState; - private bool isHUDsHidden; - partial void HideHUDs(bool value) { if (isHUDsHidden == value) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs index a5901c440..ae6905a0b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs @@ -63,7 +63,7 @@ namespace Barotrauma.Items.Components Stretch = true, RelativeSpacing = 0.05f }; - var inputLabel = new GUITextBlock(new RectTransform(Vector2.One, inputLabelArea.RectTransform), TextManager.Get("uilabel.input"), font: GUI.SubHeadingFont) { Padding = Vector4.Zero }; + var inputLabel = new GUITextBlock(new RectTransform(Vector2.One, inputLabelArea.RectTransform), TextManager.Get("deconstructor.input", fallBackTag: "uilabel.input"), font: GUI.SubHeadingFont) { Padding = Vector4.Zero }; inputLabel.RectTransform.Resize(new Point((int) inputLabel.Font.MeasureString(inputLabel.Text).X, inputLabel.RectTransform.Rect.Height)); new GUIFrame(new RectTransform(Vector2.One, inputLabelArea.RectTransform), style: "HorizontalLine"); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index e839046dc..495315dff 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -132,7 +132,7 @@ namespace Barotrauma.Items.Components Stretch = true, RelativeSpacing = 0.03f }; - var inputLabel = new GUITextBlock(new RectTransform(Vector2.One, separatorArea.RectTransform), TextManager.Get("uilabel.input"), font: GUI.SubHeadingFont) { Padding = Vector4.Zero }; + var inputLabel = new GUITextBlock(new RectTransform(Vector2.One, separatorArea.RectTransform), TextManager.Get("fabricator.input", fallBackTag: "uilabel.input"), font: GUI.SubHeadingFont) { Padding = Vector4.Zero }; inputLabel.RectTransform.Resize(new Point((int) inputLabel.Font.MeasureString(inputLabel.Text).X, inputLabel.RectTransform.Rect.Height)); new GUIFrame(new RectTransform(Vector2.One, separatorArea.RectTransform), style: "HorizontalLine"); @@ -302,6 +302,12 @@ namespace Barotrauma.Items.Components } HideEmptyItemListCategories(); + + if (selectedItem != null) + { + //reselect to recreate the info based on the new user's skills + SelectItem(character, selectedItem); + } } private void DrawInputOverLay(SpriteBatch spriteBatch, GUICustomComponent overlayComponent) @@ -343,13 +349,14 @@ namespace Barotrauma.Items.Components foreach (Item it in availableItems) { if (it.ParentInventory == inputContainer.Inventory) { continue; } - var rootContainer = it.GetRootContainer(); - if (rootContainer?.OwnInventory?.visualSlots == null) { continue; } - int availableSlotIndex = rootContainer.OwnInventory.FindIndex(it.Container == rootContainer ? it : it.Container); + var rootInventoryOwner = it.GetRootInventoryOwner(); + Inventory rootInventory = (rootInventoryOwner as Item)?.OwnInventory as Inventory ?? (rootInventoryOwner as Character)?.Inventory; + if (rootInventory?.visualSlots == null) { continue; } + int availableSlotIndex = rootInventory.FindIndex((it.Container != rootInventoryOwner ? it.Container : it) ?? it); if (availableSlotIndex < 0) { continue; } - if (rootContainer.OwnInventory.visualSlots[availableSlotIndex].HighlightTimer <= 0.0f) + if (rootInventory.visualSlots[availableSlotIndex].HighlightTimer <= 0.0f) { - rootContainer.OwnInventory.visualSlots[availableSlotIndex].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f, 0.2f); + rootInventory.visualSlots[availableSlotIndex].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f, 0.2f); if (slotIndex < inputContainer.Capacity) { inputContainer.Inventory.visualSlots[slotIndex].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f, 0.2f); @@ -406,9 +413,16 @@ namespace Barotrauma.Items.Components { toolTipText += " " + (int)Math.Round(requiredItem.MinCondition * 100) + "%"; } - else if(requiredItem.MaxCondition < 1.0f) + else if (requiredItem.MaxCondition < 1.0f) { - toolTipText += " 0-" + (int)Math.Round(requiredItem.MaxCondition * 100) + "%"; + if (requiredItem.MaxCondition <= 0.0f) + { + toolTipText += " " + (int)Math.Round(requiredItem.MaxCondition * 100) + "%"; + } + else + { + toolTipText += " 0-" + (int)Math.Round(requiredItem.MaxCondition * 100) + "%"; + } } else if (requiredItem.MaxCondition <= 0.0f) { @@ -524,16 +538,6 @@ namespace Barotrauma.Items.Components var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.9f), selectedItemFrame.RectTransform, Anchor.Center)) { RelativeSpacing = 0.03f }; var paddedReqFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.9f), selectedItemReqsFrame.RectTransform, Anchor.Center)) { RelativeSpacing = 0.03f }; - /*var itemIcon = selectedItem.TargetItem.InventoryIcon ?? selectedItem.TargetItem.sprite; - if (itemIcon != null) - { - GUIImage img = new GUIImage(new RectTransform(new Point(40, 40), paddedFrame.RectTransform), - itemIcon, scaleToFit: true) - { - Color = selectedItem.TargetItem.InventoryIconColor - }; - }*/ - string itemName = GetRecipeNameAndAmount(selectedItem); string name = itemName; @@ -732,8 +736,6 @@ namespace Barotrauma.Items.Components Character user = Entity.FindEntityByID(userID) as Character; State = newState; - timeUntilReady = newTimeUntilReady; - if (newState == FabricatorState.Stopped || itemIndex == -1) { CancelFabricating(); @@ -747,6 +749,7 @@ namespace Barotrauma.Items.Components SelectItem(user, fabricationRecipes[itemIndex]); StartFabricating(fabricationRecipes[itemIndex], user); } + timeUntilReady = newTimeUntilReady; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index 8bda33a7a..b8464aec8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -507,7 +507,7 @@ namespace Barotrauma.Items.Components { Vector2 origin = weaponSprite.Origin; float scale = parentWidth / Math.Max(weaponSprite.size.X, weaponSprite.size.Y); - Color color = !hasPower ? NoPowerColor : turret.ActiveUser is null ? GUI.Style.Red : GUI.Style.Green; + Color color = !hasPower ? NoPowerColor : turret.ActiveUser is null ? Color.DimGray : GUI.Style.Green; weaponSprite.Draw(batch, center, color, origin, rotation, scale, it.SpriteEffects); } }); @@ -537,9 +537,9 @@ namespace Barotrauma.Items.Components if (item.Submarine == null && displayedSubs.Count > 0 || // item not inside a sub anymore, but display is still showing subs item.Submarine is { } itemSub && ( - !displayedSubs.Contains(itemSub) || // current sub not displayed - itemSub.DockedTo.Any(s => !displayedSubs.Contains(s)) || // some of the docked subs not displayed - displayedSubs.Any(s => s != itemSub && !itemSub.DockedTo.Contains(s)) // displaying a sub that shouldn't be displayed + !displayedSubs.Contains(itemSub) || // current sub not displayed + itemSub.DockedTo.Any(s => !displayedSubs.Contains(s) && itemSub.ConnectedDockingPorts[s].IsLocked) || // some of the docked subs not displayed + displayedSubs.Any(s => s != itemSub && !itemSub.DockedTo.Contains(s)) // displaying a sub that shouldn't be displayed ) || prevResolution.X != GameMain.GraphicsWidth || prevResolution.Y != GameMain.GraphicsHeight || // resolution changed !submarineContainer.Children.Any()) // We lack a GUI @@ -1092,6 +1092,12 @@ namespace Barotrauma.Items.Components if (!(entity is Item it)) { continue; } if (!electricalChildren.TryGetValue(miniMapGuiComponent, out GUIComponent component)) { continue; } + if (entity.Removed) + { + component.Visible = false; + continue; + } + if (item.Submarine == null || !hasPower) { component.Color = component.OutlineColor = NoPowerElectricalColor; @@ -1117,7 +1123,7 @@ namespace Barotrauma.Items.Components int current = (int)-powerTransfer.CurrPowerConsumption, load = (int)powerTransfer.PowerLoad; line1 = TextManager.GetWithVariable("statusmonitor.junctionpower.tooltip", "[amount]", current.ToString(), fallBackTag: "statusmonitor.junctioncurrent.tooltip"); - line2 = TextManager.GetWithVariable("statusmonitor.junctionload.tooltip", "[amount]", load.ToString()); + line2 = TextManager.GetWithVariables("statusmonitor.junctionload.tooltip", new string[] { "[amount]", "[load]" }, new string[] { load.ToString(), load.ToString() }); } string line3 = TextManager.GetWithVariable("statusmonitor.durability.tooltip", "[amount]", durability.ToString()); @@ -1329,7 +1335,7 @@ namespace Barotrauma.Items.Components RectangleF entityRect = ScaleRectToUI(structure, parent, border); Vector2 spriteScale = new Vector2(entityRect.Size.X / sprite.size.X, entityRect.Size.Y / sprite.size.Y); - sprite.Draw(spriteBatch, new Vector2(entityRect.Location.X + inflate, entityRect.Location.Y + inflate), structure.SpriteColor, Vector2.Zero, 0f, spriteScale, structure.SpriteEffects); + sprite.Draw(spriteBatch, new Vector2(entityRect.Location.X + inflate, entityRect.Location.Y + inflate), structure.SpriteColor, Vector2.Zero, 0f, spriteScale, sprite.effects ^ structure.SpriteEffects); } private static RectangleF ScaleRectToUI(MapEntity entity, RectangleF parentRect, RectangleF worldBorders) @@ -1414,10 +1420,11 @@ namespace Barotrauma.Items.Components Vector2 drawPos = new Vector2(frame.Rect.Right - sizeX, frame.Rect.Y - sizeY / 2f); UISprite icon = GUI.Style.IconOverflowIndicator; - - const int iconPadding = 4; - icon.Draw(spriteBatch, new Rectangle((int) drawPos.X - iconPadding, (int) drawPos.Y - iconPadding, (int) maxWidth + iconPadding * 2, (int) maxWidth + iconPadding * 2), Color.White, SpriteEffects.None); - + if (icon != null) + { + const int iconPadding = 4; + icon.Draw(spriteBatch, new Rectangle((int) drawPos.X - iconPadding, (int) drawPos.Y - iconPadding, (int) maxWidth + iconPadding * 2, (int) maxWidth + iconPadding * 2), Color.White, SpriteEffects.None); + } GUI.DrawString(spriteBatch, drawPos, text, GUI.Style.TextColor, font: GUI.SubHeadingFont); } break; @@ -1712,5 +1719,20 @@ namespace Barotrauma.Items.Components return new MiniMapHullData(scaledPolygon, worldRect, parentRect.Size, snappedRectangles, hullRefs.ToImmutableArray()); } + + protected override void RemoveComponentSpecific() + { + base.RemoveComponentSpecific(); + if (searchAutoComplete != null) + { + searchAutoComplete.RectTransform.Parent = null; + searchAutoComplete = null; + } + if (hullInfoFrame != null) + { + hullInfoFrame.RectTransform.Parent = null; + hullInfoFrame = null; + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs index 80e5671a5..a3724b8a3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs @@ -14,7 +14,7 @@ namespace Barotrauma.Items.Components if (selectionUI == null) { - selectionUI = new SubmarineSelection(true, null, GUICanvas.Instance.ItemComponentHolder); + selectionUI = new SubmarineSelection(true, null, GUI.Canvas); } GuiFrame = selectionUI.GuiFrame; @@ -35,5 +35,15 @@ namespace Barotrauma.Items.Components selectionUI?.Update(); } + + protected override void RemoveComponentSpecific() + { + base.RemoveComponentSpecific(); + if (selectionUI != null) + { + selectionUI.GuiFrame.RectTransform.Parent = null; + selectionUI = null; + } + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs index a6b0b74b8..d0bf73438 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs @@ -19,15 +19,6 @@ namespace Barotrauma.Items.Components private readonly List<(Vector2 position, ParticleEmitter emitter)> pumpOutEmitters = new List<(Vector2 position, ParticleEmitter emitter)>(); private readonly List<(Vector2 position, ParticleEmitter emitter)> pumpInEmitters = new List<(Vector2 position, ParticleEmitter emitter)>(); - public float CurrentBrokenVolume - { - get - { - if (item.ConditionPercentage > 10.0f || !IsActive) { return 0.0f; } - return (1.0f - item.ConditionPercentage / 10.0f) * 100.0f; - } - } - partial void InitProjSpecific(XElement element) { foreach (XElement subElement in element.Elements()) @@ -193,8 +184,6 @@ namespace Barotrauma.Items.Components private readonly float flickerFrequency = 1; public override void UpdateHUD(Character character, float deltaTime, Camera cam) { - pumpSpeedLockTimer -= deltaTime; - isActiveLockTimer -= deltaTime; autoControlIndicator.Selected = IsAutoControlled; PowerButton.Enabled = isActiveLockTimer <= 0.0f; if (HasPower) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index d8831b9db..efda23637 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -83,9 +83,7 @@ namespace Barotrauma.Items.Components private const float ConnectedSubUpdateInterval = 1.0f; float connectedSubUpdateTimer; - //Vector2 = vector from the ping source to the position of the disruption - //float = strength of the disruption, between 0-1 - private readonly List> disruptedDirections = new List>(); + private readonly List<(Vector2 pos, float strength)> disruptedDirections = new List<(Vector2 pos, float strength)>(); private readonly Dictionary markerDistances = new Dictionary(); @@ -350,6 +348,27 @@ namespace Barotrauma.Items.Components } } + private Vector2 GetTransducerPos() + { + if (!UseTransducers || connectedTransducers.Count == 0) + { + //use the position of the sub if the item is static (no body) and inside a sub + return item.Submarine != null && item.body == null ? item.Submarine.WorldPosition : item.WorldPosition; + } + + Vector2 transducerPosSum = Vector2.Zero; + foreach (ConnectedTransducer transducer in connectedTransducers) + { + if (transducer.Transducer.Item.Submarine != null && !CenterOnTransducers) + { + return transducer.Transducer.Item.Submarine.WorldPosition; + } + transducerPosSum += transducer.Transducer.Item.WorldPosition; + } + return transducerPosSum / connectedTransducers.Count; + } + + public override void OnItemLoaded() { base.OnItemLoaded(); @@ -455,7 +474,20 @@ namespace Barotrauma.Items.Components zoomSlider.OnMoved(zoomSlider, zoomSlider.BarScroll); } } - + + Vector2 transducerCenter = GetTransducerPos(); + + if (steering != null && steering.DockingModeEnabled && steering.ActiveDockingSource != null) + { + Vector2 worldFocusPos = (steering.ActiveDockingSource.Item.WorldPosition + steering.DockingTarget.Item.WorldPosition) / 2.0f; + DisplayOffset = Vector2.Lerp(DisplayOffset, worldFocusPos - transducerCenter, 0.1f); + } + else + { + DisplayOffset = Vector2.Lerp(DisplayOffset, Vector2.Zero, 0.1f); + } + transducerCenter += DisplayOffset; + float distort = MathHelper.Clamp(1.0f - item.Condition / item.MaxCondition, 0.0f, 1.0f); for (int i = sonarBlips.Count - 1; i >= 0; i--) { @@ -502,8 +534,6 @@ namespace Barotrauma.Items.Components return; } - Vector2 transducerCenter = GetTransducerPos() + DisplayOffset; - if (Level.Loaded != null) { nearbyObjectUpdateTimer -= deltaTime; @@ -829,8 +859,7 @@ namespace Barotrauma.Items.Components } } - Vector2 transducerCenter = GetTransducerPos(); - + Vector2 transducerCenter = GetTransducerPos();// + DisplayOffset; if (sonarBlips.Count > 0) { @@ -840,7 +869,7 @@ namespace Barotrauma.Items.Components foreach (SonarBlip sonarBlip in sonarBlips) { - DrawBlip(spriteBatch, sonarBlip, transducerCenter, center, sonarBlip.FadeTimer / 2.0f * signalStrength, blipScale); + DrawBlip(spriteBatch, sonarBlip, transducerCenter + DisplayOffset, center, sonarBlip.FadeTimer / 2.0f * signalStrength, blipScale); } spriteBatch.End(); @@ -849,8 +878,8 @@ namespace Barotrauma.Items.Components if (item.Submarine != null && !DetectSubmarineWalls) { - DrawDockingPorts(spriteBatch, transducerCenter, signalStrength); transducerCenter += DisplayOffset; + DrawDockingPorts(spriteBatch, transducerCenter, signalStrength); DrawOwnSubmarineBorders(spriteBatch, transducerCenter, signalStrength); } else @@ -1083,14 +1112,11 @@ namespace Barotrauma.Items.Components { DrawDockingIndicator(spriteBatch, steering, ref transducerCenter); } - else - { - DisplayOffset = Vector2.Lerp(DisplayOffset, Vector2.Zero, 0.1f); - } foreach (DockingPort dockingPort in DockingPort.List) { if (Level.Loaded != null && dockingPort.Item.Submarine.WorldPosition.Y > Level.Loaded.Size.Y) { continue; } + if (dockingPort.Item.HiddenInGame) { continue; } if (dockingPort.Item.Submarine == null) { continue; } if (dockingPort.Item.Submarine.Info.IsWreck) { continue; } // docking ports should be shown even if defined as not, if the submarine is the same as the sonar's @@ -1131,9 +1157,6 @@ namespace Barotrauma.Items.Components Vector2 worldFocusPos = (steering.ActiveDockingSource.Item.WorldPosition + steering.DockingTarget.Item.WorldPosition) / 2.0f; worldFocusPos.X = steering.DockingTarget.Item.WorldPosition.X; - DisplayOffset = Vector2.Lerp(DisplayOffset, worldFocusPos - transducerCenter, 0.1f); - transducerCenter += DisplayOffset; - Vector2 sourcePortDiff = (steering.ActiveDockingSource.Item.WorldPosition - transducerCenter) * scale; Vector2 sourcePortPos = new Vector2(sourcePortDiff.X, -sourcePortDiff.Y); Vector2 targetPortDiff = (steering.DockingTarget.Item.WorldPosition - transducerCenter) * scale; @@ -1234,7 +1257,7 @@ namespace Barotrauma.Items.Components Vector2 disruptionPos = new Vector2(levelObject.Position.X, levelObject.Position.Y); float disruptionDist = Vector2.Distance(pingSource, disruptionPos); - disruptedDirections.Add(new Pair((disruptionPos - pingSource) / disruptionDist, disruptionStrength)); + disruptedDirections.Add(((disruptionPos - pingSource) / disruptionDist, disruptionStrength)); CreateBlipsForDisruption(disruptionPos, disruptionStrength); @@ -1246,7 +1269,7 @@ namespace Barotrauma.Items.Components float distSqr = Vector2.DistanceSquared(aiTarget.WorldPosition, pingSource); if (distSqr > worldPingRadiusSqr) { continue; } float disruptionDist = (float)Math.Sqrt(distSqr); - disruptedDirections.Add(new Pair((aiTarget.WorldPosition - pingSource) / disruptionDist, aiTarget.SonarDisruption)); + disruptedDirections.Add(((aiTarget.WorldPosition - pingSource) / disruptionDist, aiTarget.SonarDisruption)); CreateBlipsForDisruption(aiTarget.WorldPosition, disruption); } } @@ -1461,10 +1484,10 @@ namespace Barotrauma.Items.Components float transducerDist = transducerDiff.Length(); Vector2 pingDirection = transducerDiff / transducerDist; bool disrupted = false; - foreach (Pair disruptDir in disruptedDirections) + foreach ((Vector2 disruptPos, float disruptStrength) in disruptedDirections) { - float dot = Vector2.Dot(pingDirection, disruptDir.First); - if (dot > 1.0f - disruptDir.Second) + float dot = Vector2.Dot(pingDirection, disruptPos); + if (dot > 1.0f - disruptStrength) { disrupted = true; break; @@ -1670,13 +1693,12 @@ namespace Barotrauma.Items.Components } } - if (iconIdentifier == null || !targetIcons.ContainsKey(iconIdentifier)) + if (iconIdentifier == null || !targetIcons.TryGetValue(iconIdentifier, out var iconInfo) || iconInfo.Item1 == null) { GUI.DrawRectangle(spriteBatch, new Rectangle((int)markerPos.X - 3, (int)markerPos.Y - 3, 6, 6), markerColor, thickness: 2); } else { - var iconInfo = targetIcons[iconIdentifier]; iconInfo.Item1.Draw(spriteBatch, markerPos, iconInfo.Item2); } @@ -1689,7 +1711,10 @@ namespace Barotrauma.Items.Components Vector2 textSize = GUI.SmallFont.MeasureString(wrappedLabel); //flip the text to left side when the marker is on the left side or goes outside the right edge of the interface - if ((dir.X < 0.0f || labelPos.X + textSize.X + 10 > GuiFrame.Rect.X) && labelPos.X - textSize.X > 0) labelPos.X -= textSize.X + 10; + if (GuiFrame != null && (dir.X < 0.0f || labelPos.X + textSize.X + 10 > GuiFrame.Rect.X) && labelPos.X - textSize.X > 0) + { + labelPos.X -= textSize.X + 10; + } GUI.DrawString(spriteBatch, new Vector2(labelPos.X + 10, labelPos.Y), diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index b66bcaa5a..ee079c02a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -403,7 +403,7 @@ namespace Barotrauma.Items.Components { if (GameMain.Client == null) { - item.SendSignal("1", "toggle_docking"); + item.SendSignal(new Signal("1", sender: Character.Controlled), "toggle_docking"); } else { @@ -682,7 +682,7 @@ namespace Barotrauma.Items.Components enterOutpostPrompt?.Close(); } } - else if (DockingSources.Any(d => d.Docked)) + else if (connectedPorts.Any(d => d.Docked)) { dockingButton.Text = undockText; dockingContainer.Visible = true; @@ -819,7 +819,7 @@ namespace Barotrauma.Items.Components Connection dockingConnection = item.Connections?.FirstOrDefault(c => c.Name == "toggle_docking"); if (dockingConnection != null) { - connectedPorts = item.GetConnectedComponentsRecursive(dockingConnection); + connectedPorts = item.GetConnectedComponentsRecursive(dockingConnection, ignoreInactiveRelays: true); } checkConnectedPortsTimer = CheckConnectedPortsInterval; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs index 643b2042e..f8962c80e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs @@ -10,6 +10,12 @@ namespace Barotrauma.Items.Components private GUIProgressBar chargeIndicator; private GUIScrollBar rechargeSpeedSlider; + [Serialize(0.0f, true)] + public float RechargeWarningIndicatorLow { get; set; } + + [Serialize(0.0f, true)] + public float RechargeWarningIndicatorHigh { get; set; } + public Vector2 DrawSize { //use the extents of the item as the draw size @@ -28,19 +34,38 @@ namespace Barotrauma.Items.Components var upperArea = new GUIFrame(new RectTransform(new Vector2(1, 0.4f), paddedFrame.RectTransform, Anchor.TopCenter), style: null); var lowerArea = new GUIFrame(new RectTransform(new Vector2(1, 0.6f), paddedFrame.RectTransform, Anchor.BottomCenter), style: null); - - string rechargeStr = TextManager.Get("PowerContainerRechargeRate"); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), upperArea.RectTransform, Anchor.TopCenter), - "RechargeRate", textColor: GUI.Style.TextColor, font: GUI.SubHeadingFont, textAlignment: Alignment.Center) + var rechargeRateContainer = new GUIFrame(new RectTransform(new Vector2(1, 0.4f), upperArea.RectTransform), style: null); + var rechargeLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 0.0f), rechargeRateContainer.RectTransform, Anchor.CenterLeft), + TextManager.Get("rechargerate"), textColor: GUI.Style.TextColor, font: GUI.SubHeadingFont, textAlignment: Alignment.CenterLeft); + string kW = TextManager.Get("kilowatt"); + var rechargeText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1), rechargeRateContainer.RectTransform, Anchor.CenterRight), + "", textColor: GUI.Style.TextColor, font: GUI.Font, textAlignment: Alignment.CenterRight) { - TextGetter = () => - { - return rechargeStr.Replace("[rate]", ((int)((rechargeSpeed / maxRechargeSpeed) * 100.0f)).ToString()); - } + TextGetter = () => $"{(int)MathF.Round(currPowerConsumption)} {kW} ({(int)MathF.Round(RechargeRatio * 100)} %)" }; + if (rechargeText.TextSize.X > rechargeText.Rect.Width) { rechargeText.Font = GUI.SmallFont; } - rechargeSpeedSlider = new GUIScrollBar(new RectTransform(new Vector2(0.9f, 0.4f), upperArea.RectTransform, Anchor.BottomCenter), - barSize: 0.15f, style: "DeviceSlider") + var rechargeSliderContainer = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.4f), upperArea.RectTransform, Anchor.BottomCenter)); + + if (RechargeWarningIndicatorLow > 0.0f || RechargeWarningIndicatorHigh > 0.0f) + { + var rechargeSliderFill = new GUICustomComponent(new RectTransform(new Vector2(0.95f, 0.9f), rechargeSliderContainer.RectTransform, Anchor.Center), (SpriteBatch sb, GUICustomComponent c) => + { + if (RechargeWarningIndicatorLow > 0.0f) + { + float warningLow = c.Rect.Width * RechargeWarningIndicatorLow; + GUI.DrawRectangle(sb, new Vector2(c.Rect.X + warningLow, c.Rect.Y), new Vector2(c.Rect.Width - warningLow, c.Rect.Height), GUI.Style.Orange, isFilled: true); + } + if (RechargeWarningIndicatorHigh > 0.0f) + { + float warningHigh = c.Rect.Width * RechargeWarningIndicatorHigh; + GUI.DrawRectangle(sb, new Vector2(c.Rect.X + warningHigh, c.Rect.Y), new Vector2(c.Rect.Width - warningHigh, c.Rect.Height), GUI.Style.Red, isFilled: true); + } + }); + } + + rechargeSpeedSlider = new GUIScrollBar(new RectTransform(Vector2.One, rechargeSliderContainer.RectTransform, Anchor.Center), + barSize: 0.15f, style: "DeviceSliderSeeThrough") { Step = 0.1f, OnMoved = (GUIScrollBar scrollBar, float barScroll) => @@ -61,17 +86,17 @@ namespace Barotrauma.Items.Components // lower area -------------------------- - var textArea = new GUIFrame(new RectTransform(new Vector2(1, 0.4f), lowerArea.RectTransform), style: null); - var chargeLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 0.0f), textArea.RectTransform, Anchor.CenterLeft), + var chargeTextContainer = new GUIFrame(new RectTransform(new Vector2(1, 0.4f), lowerArea.RectTransform), style: null); + var chargeLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 0.0f), chargeTextContainer.RectTransform, Anchor.CenterLeft), TextManager.Get("charge"), textColor: GUI.Style.TextColor, font: GUI.SubHeadingFont, textAlignment: Alignment.CenterLeft) { ToolTip = TextManager.Get("PowerTransferTipPower") }; string kWmin = TextManager.Get("kilowattminute"); - var chargeText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1), textArea.RectTransform, Anchor.CenterRight), + var chargeText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1), chargeTextContainer.RectTransform, Anchor.CenterRight), "", textColor: GUI.Style.TextColor, font: GUI.Font, textAlignment: Alignment.CenterRight) { - TextGetter = () => $"{(int)Math.Round(charge)}/{(int)capacity} {kWmin} ({(int)Math.Round(MathUtils.Percentage(charge, capacity))} %)" + TextGetter = () => $"{(int)MathF.Round(charge)}/{(int)capacity} {kWmin} ({(int)MathF.Round(MathUtils.Percentage(charge, capacity))} %)" }; if (chargeText.TextSize.X > chargeText.Rect.Width) { chargeText.Font = GUI.SmallFont; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index 2c2e02a2b..0900f4fed 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -381,17 +381,17 @@ namespace Barotrauma.Items.Components if (deteriorationTimer > 0.0f) { GUI.DrawString(spriteBatch, - new Vector2(item.WorldPosition.X, -item.WorldPosition.Y), "Deterioration delay " + ((int)deteriorationTimer) + (paused ? " [PAUSED]" : ""), + new Vector2(item.DrawPosition.X, -item.DrawPosition.Y), "Deterioration delay " + ((int)deteriorationTimer) + (paused ? " [PAUSED]" : ""), paused ? Color.Cyan : Color.Lime, Color.Black * 0.5f); } else { GUI.DrawString(spriteBatch, - new Vector2(item.WorldPosition.X, -item.WorldPosition.Y), "Deteriorating at " + (int)(DeteriorationSpeed * 60.0f) + " units/min" + (paused ? " [PAUSED]" : ""), + new Vector2(item.DrawPosition.X, -item.DrawPosition.Y), "Deteriorating at " + (int)(DeteriorationSpeed * 60.0f) + " units/min" + (paused ? " [PAUSED]" : ""), paused ? Color.Cyan : GUI.Style.Red, Color.Black * 0.5f); } GUI.DrawString(spriteBatch, - new Vector2(item.WorldPosition.X, -item.WorldPosition.Y + 20), "Condition: " + (int)item.Condition + "/" + (int)item.MaxCondition, + new Vector2(item.DrawPosition.X, -item.DrawPosition.Y + 20), "Condition: " + (int)item.Condition + "/" + (int)item.MaxCondition, GUI.Style.Orange); } } @@ -434,6 +434,7 @@ namespace Barotrauma.Items.Components DeteriorateAlways = msg.ReadBoolean(); tinkeringDuration = msg.ReadSingle(); tinkeringStrength = msg.ReadSingle(); + tinkeringPowersDevices = msg.ReadBoolean(); ushort currentFixerID = msg.ReadUInt16(); currentFixerAction = (FixActions)msg.ReadRangedInteger(0, 2); CurrentFixer = currentFixerID != 0 ? Entity.FindEntityByID(currentFixerID) as Character : null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs index 436e99507..e98bf3f58 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs @@ -199,6 +199,26 @@ namespace Barotrauma.Items.Components public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { snapped = msg.ReadBoolean(); + + if (!snapped) + { + UInt16 targetId = msg.ReadUInt16(); + UInt16 sourceId = msg.ReadUInt16(); + byte limbIndex = msg.ReadByte(); + + Item target = Entity.FindEntityByID(targetId) as Item; + if (target == null) { return; } + var source = Entity.FindEntityByID(sourceId); + if (source is Character sourceCharacter && limbIndex >= 0 && limbIndex < sourceCharacter.AnimController.Limbs.Length) + { + Limb sourceLimb = sourceCharacter.AnimController.Limbs[limbIndex]; + Attach(sourceLimb, target); + } + else if (source is ISpatialEntity spatialEntity) + { + Attach(spatialEntity, target); + } + } } protected override void RemoveComponentSpecific() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs index ff50d0fef..d0a181824 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs @@ -46,6 +46,7 @@ namespace Barotrauma.Items.Components child.Enabled = buttonsEnabled; child.Children.ForEach(c => c.Enabled = buttonsEnabled); } + if (Container == null) { return; } bool itemsContained = Container.Inventory.AllItems.Any(); if (itemsContained) { @@ -77,7 +78,7 @@ namespace Barotrauma.Items.Components { if (GameMain.IsSingleplayer) { - SendSignal((int)userData); + SendSignal((int)userData, Character.Controlled); } else { @@ -98,6 +99,7 @@ namespace Barotrauma.Items.Components partial void OnItemLoadedProjSpecific() { + if (Container == null) { return; } Container.AllowUIOverlap = true; Container.Inventory.RectTransform = containerHolder.RectTransform; } @@ -109,7 +111,7 @@ namespace Barotrauma.Items.Components public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { - SendSignal(msg.ReadRangedInteger(0, Signals.Length - 1), isServerMessage: true); + SendSignal(msg.ReadRangedInteger(0, Signals.Length - 1), sender: null, isServerMessage: true); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs index 336769480..2e8e07a2b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs @@ -23,7 +23,7 @@ namespace Barotrauma.Items.Components public static void DrawConnections(SpriteBatch spriteBatch, ConnectionPanel panel, Character character) { - if (DraggingConnected?.Item.Removed ?? false) + if (DraggingConnected?.Item?.Removed ?? true) { DraggingConnected = null; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs index 365680a44..5a24346cc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs @@ -197,7 +197,7 @@ namespace Barotrauma.Items.Components } } - partial void UpdateProjSpecific() + public override void UpdateHUD(Character character, float deltaTime, Camera cam) { bool elementVisibilityChanged = false; int visibleElementCount = 0; @@ -209,6 +209,7 @@ namespace Barotrauma.Items.Components if (uiElement.Visible != visible) { uiElement.Visible = visible; + uiElement.IgnoreLayoutGroups = !uiElement.Visible; elementVisibilityChanged = true; } } @@ -223,6 +224,7 @@ namespace Barotrauma.Items.Components uiElement.RectTransform.RelativeSize = new Vector2(1.0f, elementSize); } GuiFrame.Visible = visibleElementCount > 0; + uiElementContainer.Recalculate(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/MotionSensor.cs index 08dd799bc..8d7ec8a22 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/MotionSensor.cs @@ -12,9 +12,9 @@ namespace Barotrauma.Items.Components public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) { - if (!editing || !MapEntity.SelectedList.Contains(item)) return; + if (!editing || !MapEntity.SelectedList.Contains(item)) { return; } - Vector2 pos = item.WorldPosition + detectOffset; + Vector2 pos = item.WorldPosition + TransformedDetectOffset; pos.Y = -pos.Y; GUI.DrawRectangle(spriteBatch, pos - new Vector2(rangeX, rangeY), new Vector2(rangeX, rangeY) * 2.0f, Color.Cyan * 0.5f, isFilled: false, thickness: 2); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/WifiComponent.cs index 99da96f2a..dd8c385eb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/WifiComponent.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace Barotrauma.Items.Components @@ -19,5 +20,10 @@ namespace Barotrauma.Items.Components ShapeExtensions.DrawLine(spriteBatch, pos + Vector2.UnitX * range, pos - Vector2.UnitX * range, Color.Cyan * 0.5f, 2); ShapeExtensions.DrawCircle(spriteBatch, pos, range, 32, Color.Cyan * 0.5f, 3); } + + public void ClientRead(ServerNetObject type, 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 f83259306..f4b055a39 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs @@ -135,7 +135,13 @@ namespace Barotrauma.Items.Components wireSprite = overrideSprite ?? defaultWireSprite; } + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) + { + Draw(spriteBatch, editing, Vector2.Zero, itemDepth); + } + + public void Draw(SpriteBatch spriteBatch, bool editing, Vector2 offset, float itemDepth = -1) { if (sections.Count == 0 && !IsActive || Hidden) { @@ -156,6 +162,8 @@ namespace Barotrauma.Items.Components drawOffset = sub.DrawPosition + sub.HiddenSubPosition; } + drawOffset += offset; + float baseDepth = UseSpriteDepth ? item.SpriteDepth : wireSprite.Depth; float depth = item.IsSelected ? 0.0f : SubEditorScreen.IsWiringMode() ? 0.02f : baseDepth + (item.ID % 100) * 0.000001f;// item.GetDrawDepth(wireSprite.Depth, wireSprite); @@ -286,7 +294,6 @@ namespace Barotrauma.Items.Components item.Color, depth, 0.3f); } - public static void UpdateEditing(List wires) { var doubleClicked = PlayerInput.DoubleClicked(); @@ -509,6 +516,31 @@ namespace Barotrauma.Items.Components } } + public override void Move(Vector2 amount) + { + //only used in the sub editor, hence only in the client project + if (!item.IsSelected) { return; } + + Vector2 wireNodeOffset = item.Submarine == null ? Vector2.Zero : item.Submarine.HiddenSubPosition + amount; + for (int i = 0; i < nodes.Count; i++) + { + if (i == 0 || i == nodes.Count - 1) + { + if (connections[0]?.Item != null && !connections[0].Item.IsSelected && + (Submarine.RectContains(connections[0].Item.Rect, nodes[i] + wireNodeOffset) || Submarine.RectContains(connections[0].Item.Rect, nodes[i] + wireNodeOffset - amount))) + { + continue; + } + else if (connections[1]?.Item != null && !connections[1].Item.IsSelected && + (Submarine.RectContains(connections[1].Item.Rect, nodes[i] + wireNodeOffset) || Submarine.RectContains(connections[1].Item.Rect, nodes[i] + wireNodeOffset - amount))) + { + continue; + } + } + nodes[i] += amount; + } + UpdateSections(); + } public bool IsMouseOn() { if (GUI.MouseOn == null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs index d974c2044..88367f1a0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs @@ -301,7 +301,7 @@ namespace Barotrauma.Items.Components Dictionary combinedAfflictionStrengths = new Dictionary(); foreach (Affliction affliction in allAfflictions) { - if (affliction.Strength < affliction.Prefab.ShowInHealthScannerThreshold || affliction.Strength <= 0.0f) continue; + if (affliction.Strength < affliction.Prefab.ShowInHealthScannerThreshold || affliction.Strength <= 0.0f) { continue; } if (combinedAfflictionStrengths.ContainsKey(affliction.Prefab)) { combinedAfflictionStrengths[affliction.Prefab] += affliction.Strength; @@ -314,7 +314,7 @@ namespace Barotrauma.Items.Components foreach (AfflictionPrefab affliction in combinedAfflictionStrengths.Keys) { - texts.Add(TextManager.AddPunctuation(':', affliction.Name, Math.Max(((int)combinedAfflictionStrengths[affliction]), 1).ToString() + " %")); + texts.Add(TextManager.AddPunctuation(':', affliction.Name, Math.Max((int)combinedAfflictionStrengths[affliction], 1).ToString() + " %")); textColors.Add(Color.Lerp(GUI.Style.Orange, GUI.Style.Red, combinedAfflictionStrengths[affliction] / affliction.MaxStrength)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index 11c827e3a..98f9490aa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -580,14 +580,20 @@ namespace Barotrauma.Items.Components private void GetAvailablePower(out float availableCharge, out float availableCapacity) { - var batteries = item.GetConnectedComponents(); - availableCharge = 0.0f; availableCapacity = 0.0f; - foreach (PowerContainer battery in batteries) + if (item.Connections == null) { return; } + foreach (Connection c in item.Connections) { - availableCharge += battery.Charge; - availableCapacity += battery.Capacity; + var recipients = c.Recipients; + foreach (Connection recipient in recipients) + { + if (!recipient.IsPower || !recipient.IsOutput) { continue; } + var battery = recipient.Item?.GetComponent(); + if (battery == null) { continue; } + availableCharge += battery.Charge; + availableCapacity += battery.Capacity; + } } } @@ -647,7 +653,9 @@ namespace Barotrauma.Items.Components bool readyToFire = reload <= 0.0f && charged && availableAmmo.Any(p => p != null); if (ShowChargeIndicator && PowerConsumption > 0.0f) { - powerIndicator.Color = charged ? GUI.Style.Green : GUI.Style.Red; + powerIndicator.Color = charged ? + (HasPowerToShoot() ? GUI.Style.Green : GUI.Style.Orange) : + GUI.Style.Red; if (flashLowPower) { powerIndicator.BarSize = 1; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs index 10d0fc376..26665699e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs @@ -10,7 +10,13 @@ namespace Barotrauma.Items.Components int roundedValue = (int)Math.Round((1 - damageModifier.DamageMultiplier * damageModifier.ProbabilityMultiplier) * 100); if (roundedValue == 0) { return; } string colorStr = XMLExtensions.ColorToString(GUI.Style.Green); - description += $"\n ‖color:{colorStr}‖{roundedValue.ToString("-0;+#")}%‖color:end‖ {AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.Equals(afflictionIdentifier, StringComparison.OrdinalIgnoreCase))?.Name ?? afflictionIdentifier}"; + + string afflictionName = + AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.Equals(afflictionIdentifier, StringComparison.OrdinalIgnoreCase))?.Name ?? + TextManager.Get($"afflictiontype.{afflictionIdentifier}", returnNull: true) ?? + afflictionIdentifier; + + description += $"\n ‖color:{colorStr}‖{roundedValue.ToString("-0;+#")}%‖color:end‖ {afflictionName}"; } public override void AddTooltipInfo(ref string name, ref string description) @@ -33,9 +39,9 @@ namespace Barotrauma.Items.Components { GetDamageModifierText(ref description, damageModifier, afflictionIdentifier); } - foreach (string afflictionIdentifier in damageModifier.ParsedAfflictionTypes) + foreach (string afflictionType in damageModifier.ParsedAfflictionTypes) { - GetDamageModifierText(ref description, damageModifier, afflictionIdentifier); + GetDamageModifierText(ref description, damageModifier, afflictionType); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 0d443f65d..86116fbbd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -1584,6 +1584,10 @@ namespace Barotrauma { containedState = item.Condition / item.MaxCondition; } + else if (itemContainer.ShowTotalStackCapacityInContainedStateIndicator) + { + containedState = itemContainer.Inventory.AllItems.Count() / (float)(itemContainer.GetMaxStackSize(0) * itemContainer.Capacity); + } else { var containedItem = itemContainer.Inventory.slots[Math.Max(itemContainer.ContainedStateIndicatorSlot, 0)].FirstOrDefault(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 60d8a0fc6..86b76aebb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -17,7 +17,7 @@ namespace Barotrauma partial class Item : MapEntity, IDamageable, ISerializableEntity, IServerSerializable, IClientSerializable { public static bool ShowItems = true, ShowWires = true; - + private readonly List positionBuffer = new List(); private readonly List activeHUDs = new List(); @@ -89,8 +89,8 @@ namespace Barotrauma { if (itemInUseWarning == null) { - itemInUseWarning = new GUITextBlock(new RectTransform(new Point(10), GUI.Canvas), "", - textColor: GUI.Style.Orange, color: Color.Black, + itemInUseWarning = new GUITextBlock(new RectTransform(new Point(10), GUI.Canvas), "", + textColor: GUI.Style.Orange, color: Color.Black, textAlignment: Alignment.Center, style: "OuterGlow"); } return itemInUseWarning; @@ -105,6 +105,9 @@ namespace Barotrauma { return false; } + + if (!SubEditorScreen.IsLayerVisible(this)) { return false;} + return parentInventory == null && (body == null || body.Enabled) && ShowItems; } } @@ -154,7 +157,7 @@ namespace Barotrauma if (containedSprite.UseWhenAttached) { activeContainedSprite = containedSprite; - activeSprite = containedSprite.Sprite; + activeSprite = containedSprite.Sprite; UpdateSpriteStates(0.0f); return; } @@ -196,7 +199,7 @@ namespace Barotrauma { brokenSprite.Sprite.EnsureLazyLoaded(); } - + foreach (var decorativeSprite in ((ItemPrefab)prefab).DecorativeSprites) { decorativeSprite.Sprite.EnsureLazyLoaded(); @@ -255,7 +258,7 @@ namespace Barotrauma public override void Draw(SpriteBatch spriteBatch, bool editing, bool back = true) { - if (!Visible || (!editing && HiddenInGame)) { return; } + if (!Visible || (!editing && HiddenInGame) || !SubEditorScreen.IsLayerVisible(this)) { return; } if (editing) { @@ -265,7 +268,7 @@ namespace Barotrauma } else if (!ShowItems) { return; } } - + Color color = IsIncludedInSelection && editing ? GUI.Style.Blue : IsHighlighted && !GUI.DisableItemHighlights && Screen.Selected != GameMain.GameScreen ? GUI.Style.Orange * Math.Max(GetSpriteColor().A / (float) byte.MaxValue, 0.1f) : GetSpriteColor(); //if (IsSelected && editing) color = Color.Lerp(color, Color.Gold, 0.5f); @@ -273,7 +276,7 @@ namespace Barotrauma bool isWiringMode = editing && SubEditorScreen.TransparentWiringMode && SubEditorScreen.IsWiringMode() && !isWire && parentInventory == null; bool renderTransparent = isWiringMode && GetComponent() == null; if (renderTransparent) { color *= 0.15f; } - + BrokenItemSprite fadeInBrokenSprite = null; float fadeInBrokenSpriteAlpha = 0.0f; float displayCondition = FakeBroken ? 0.0f : ConditionPercentage; @@ -322,7 +325,7 @@ namespace Barotrauma Vector2 size = new Vector2(rect.Width, rect.Height); if (color.A > 0) { - activeSprite.DrawTiled(spriteBatch, new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)) + drawOffset, + activeSprite.DrawTiled(spriteBatch, new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)) + drawOffset, size, color: color, textureScale: Vector2.One * Scale, depth: depth); @@ -336,11 +339,11 @@ namespace Barotrauma } foreach (var decorativeSprite in Prefab.DecorativeSprites) { - if (!spriteAnimState[decorativeSprite].IsActive) { continue; } + if (!spriteAnimState[decorativeSprite].IsActive) { continue; } Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, flippedX && Prefab.CanSpriteFlipX ? rotationRad : -rotationRad) * Scale; if (flippedX && Prefab.CanSpriteFlipX) { offset.X = -offset.X; } if (flippedY && Prefab.CanSpriteFlipY) { offset.Y = -offset.Y; } - decorativeSprite.Sprite.DrawTiled(spriteBatch, + decorativeSprite.Sprite.DrawTiled(spriteBatch, new Vector2(DrawPosition.X + offset.X - rect.Width / 2, -(DrawPosition.Y + offset.Y + rect.Height / 2)), size, color: color, textureScale: Vector2.One * Scale, @@ -380,7 +383,7 @@ namespace Barotrauma Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, flippedX && Prefab.CanSpriteFlipX ? rotationRad : -rotationRad) * Scale; if (flippedX && Prefab.CanSpriteFlipX) { offset.X = -offset.X; } if (flippedY && Prefab.CanSpriteFlipY) { offset.Y = -offset.Y; } - decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color, + decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color, rotationRad + rot, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects, depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth), 0.999f)); } @@ -440,11 +443,11 @@ namespace Barotrauma depth: depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth)); } } - + foreach (var upgrade in Upgrades) { var upgradeSprites = GetUpgradeSprites(upgrade); - + foreach (var decorativeSprite in upgradeSprites) { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } @@ -456,7 +459,7 @@ namespace Barotrauma rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects, depth: depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth)); } - + } activeSprite.effects = oldEffects; @@ -466,7 +469,7 @@ namespace Barotrauma } } - //use a backwards for loop because the drawable components may disable drawing, + //use a backwards for loop because the drawable components may disable drawing, //causing them to be removed from the list for (int i = drawableComponents.Count - 1; i >= 0; i--) { @@ -501,7 +504,7 @@ namespace Barotrauma Vector2 drawPos = new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)); Vector2 drawSize = new Vector2(MathF.Ceiling(rect.Width + Math.Abs(drawPos.X - (int)drawPos.X)), MathF.Ceiling(rect.Height + Math.Abs(drawPos.Y - (int)drawPos.Y))); drawPos = new Vector2(MathF.Floor(drawPos.X), MathF.Floor(drawPos.Y)); - GUI.DrawRectangle(spriteBatch, drawPos, drawSize, + GUI.DrawRectangle(spriteBatch, drawPos, drawSize, Color.White, false, 0, thickness: Math.Max(1, (int)(2 / Screen.Selected.Cam.Zoom))); foreach (Rectangle t in Prefab.Triggers) @@ -582,19 +585,25 @@ namespace Barotrauma } } - DecorativeSprite.UpdateSpriteStates(Prefab.DecorativeSpriteGroups, spriteAnimState, ID, deltaTime, ConditionalMatches); - + if (Prefab.DecorativeSpriteGroups.Count > 0) + { + DecorativeSprite.UpdateSpriteStates(Prefab.DecorativeSpriteGroups, spriteAnimState, ID, deltaTime, ConditionalMatches); + } + foreach (var upgrade in Upgrades) { - var upgradeSprites = GetUpgradeSprites(upgrade); + var upgradeSprites = GetUpgradeSprites(upgrade); foreach (var decorativeSprite in upgradeSprites) { var spriteState = spriteAnimState[decorativeSprite]; spriteState.IsActive = true; - foreach (var _ in decorativeSprite.IsActiveConditionals.Where(conditional => !ConditionalMatches(conditional))) + foreach (var conditional in decorativeSprite.IsActiveConditionals) { - spriteState.IsActive = false; - break; + if (!ConditionalMatches(conditional)) + { + spriteState.IsActive = false; + break; + } } } } @@ -696,8 +705,8 @@ namespace Barotrauma foreach (string tag in ip.PreferredContainers.SelectMany(pc => pc.Primary)) { availableTags.Add(tag); } foreach (string tag in ip.PreferredContainers.SelectMany(pc => pc.Secondary)) { availableTags.Add(tag); } } - //remove identifiers from the available container tags - //(otherwise the list will include many irrelevant options, + //remove identifiers from the available container tags + //(otherwise the list will include many irrelevant options, //e.g. "weldingtool" because a welding fuel tank can be placed inside the container, etc) availableTags.RemoveWhere(t => MapEntityPrefab.List.Any(me => me.Identifier == t)); new GUIButton(new RectTransform(new Vector2(0.1f, 1), tagsField.RectTransform, Anchor.TopRight), "...") @@ -749,7 +758,7 @@ namespace Barotrauma { me.FlipY(relativeToSub: false); } - if (!SelectedList.Contains(this)) { FlipY(relativeToSub: false); } + if (!SelectedList.Contains(this)) { FlipY(relativeToSub: false); } return true; } }; @@ -805,9 +814,9 @@ namespace Barotrauma { if (!ic.AllowInGameEditing) { continue; } if (SerializableProperty.GetProperties(ic).Count == 0 && - !SerializableProperty.GetProperties(ic).Any(p => p.GetAttribute().IsEditable(ic))) + !SerializableProperty.GetProperties(ic).Any(p => p.GetAttribute().IsEditable(ic))) { - continue; + continue; } } else @@ -869,7 +878,7 @@ namespace Barotrauma textBox.Text = relatedItem.JoinedIdentifiers; return true; }; - } + } ic.CreateEditingHUD(componentEditor); componentEditor.Recalculate(); @@ -892,7 +901,7 @@ namespace Barotrauma return upgradeSprites; } - + public override bool AddUpgrade(Upgrade upgrade, bool createNetworkEvent = false) { if (upgrade.Prefab.IsWallUpgrade) { return false; } @@ -949,7 +958,7 @@ namespace Barotrauma //reset positions first List elementsToMove = new List(); - if (editingHUD != null && editingHUD.UserData == this && + if (editingHUD != null && editingHUD.UserData == this && ((HasInGameEditableProperties && Character.Controlled?.SelectedConstruction == this) || Screen.Selected == GameMain.SubEditorScreen)) { elementsToMove.Add(editingHUD); @@ -972,8 +981,8 @@ namespace Barotrauma int disallowedPadding = (int)(50 * GUI.Scale); disallowedAreas.Add(GameMain.GameSession.CrewManager.GetActiveCrewArea()); disallowedAreas.Add(new Rectangle( - HUDLayoutSettings.ChatBoxArea.X - disallowedPadding, HUDLayoutSettings.ChatBoxArea.Y, - HUDLayoutSettings.ChatBoxArea.Width + disallowedPadding, HUDLayoutSettings.ChatBoxArea.Height)); + HUDLayoutSettings.ChatBoxArea.X - disallowedPadding, HUDLayoutSettings.ChatBoxArea.Y, + HUDLayoutSettings.ChatBoxArea.Width + disallowedPadding, HUDLayoutSettings.ChatBoxArea.Height)); } if (Screen.Selected is SubEditorScreen editor) @@ -985,8 +994,8 @@ namespace Barotrauma GUI.PreventElementOverlap(elementsToMove, disallowedAreas, new Rectangle( - 0, 20, - GameMain.GraphicsWidth, + 0, 20, + GameMain.GraphicsWidth, HUDLayoutSettings.InventoryTopY > 0 ? HUDLayoutSettings.InventoryTopY - 40 : GameMain.GraphicsHeight - 80)); foreach (ItemComponent ic in activeHUDs) @@ -995,7 +1004,7 @@ namespace Barotrauma var linkUIToComponent = ic.GetLinkUIToComponent(); - if (linkUIToComponent == null) { continue; } + if (linkUIToComponent == null) { continue; } ic.GuiFrame.RectTransform.ScreenSpaceOffset = linkUIToComponent.GuiFrame.RectTransform.ScreenSpaceOffset; } @@ -1110,14 +1119,14 @@ namespace Barotrauma } } } - + public void DrawHUD(SpriteBatch spriteBatch, Camera cam, Character character) { if (HasInGameEditableProperties && (character.SelectedConstruction == this || EditableWhenEquipped)) { DrawEditing(spriteBatch, cam); } - + foreach (ItemComponent ic in activeHUDs) { if (ic.CanBeSelected) @@ -1138,9 +1147,9 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, debugInitialHudPositions[i], Color.Orange); GUI.DrawRectangle(spriteBatch, ic.GuiFrame.Rect, Color.LightGreen); GUI.DrawLine(spriteBatch, debugInitialHudPositions[i].Location.ToVector2(), ic.GuiFrame.Rect.Location.ToVector2(), Color.Orange); - + i++; - } + } } } @@ -1172,7 +1181,7 @@ namespace Barotrauma } texts.Add(new ColoredText(nameText, GUI.Style.TextColor, false, false)); - if (CampaignInteractionType != CampaignMode.InteractionType.None) + if (CampaignMode.BlocksInteraction(CampaignInteractionType)) { texts.Add(new ColoredText(TextManager.GetWithVariable($"CampaignInteraction.{CampaignInteractionType}", "[key]", GameMain.Config.KeyBindText(InputType.Use)), Color.Cyan, false, false)); } @@ -1262,7 +1271,7 @@ namespace Barotrauma NetEntityEvent.Type eventType = (NetEntityEvent.Type)msg.ReadRangedInteger(0, Enum.GetValues(typeof(NetEntityEvent.Type)).Length - 1); - + switch (eventType) { case NetEntityEvent.Type.ComponentState: @@ -1323,7 +1332,7 @@ namespace Barotrauma ItemComponent targetComponent = componentIndex < components.Count ? components[componentIndex] : null; Character targetCharacter = FindEntityByID(targetCharacterID) as Character; - Limb targetLimb = targetCharacter != null && targetLimbID < targetCharacter.AnimController.Limbs.Length ? + Limb targetLimb = targetCharacter != null && targetLimbID < targetCharacter.AnimController.Limbs.Length ? targetCharacter.AnimController.Limbs[targetLimbID] : null; Entity useTarget = FindEntityByID(useTargetID); @@ -1334,7 +1343,7 @@ namespace Barotrauma else { targetComponent.ApplyStatusEffects(actionType, 1.0f, targetCharacter, targetLimb, useTarget, worldPosition: worldPosition); - } + } } break; case NetEntityEvent.Type.ChangeProperty: @@ -1346,7 +1355,7 @@ namespace Barotrauma if (UpgradePrefab.Find(identifier) is { } upgradePrefab) { Upgrade upgrade = new Upgrade(this, upgradePrefab, level); - + byte targetCount = msg.ReadByte(); for (int i = 0; i < targetCount; i++) { @@ -1360,7 +1369,7 @@ namespace Barotrauma AddUpgrade(upgrade, false); } - break; + break; case NetEntityEvent.Type.Invalid: break; } @@ -1394,7 +1403,7 @@ namespace Barotrauma 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 == null ? (byte)255 : (byte)Array.IndexOf(targetCharacter.AnimController.Limbs, targetLimb)); break; case NetEntityEvent.Type.ChangeProperty: WritePropertyChange(msg, extraData, true); @@ -1541,6 +1550,7 @@ namespace Barotrauma Vector2 pos = Vector2.Zero; Submarine sub = null; + float rotation = 0.0f; int itemContainerIndex = -1; int inventorySlotIndex = -1; @@ -1552,7 +1562,7 @@ namespace Barotrauma else { pos = new Vector2(msg.ReadSingle(), msg.ReadSingle()); - + rotation = msg.ReadRangedSingle(0, MathHelper.TwoPi, 8); ushort subID = msg.ReadUInt16(); if (subID > 0) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs index 540d896d0..04169a6fa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs @@ -315,7 +315,7 @@ namespace Barotrauma.MapCreatures.Behavior } else { - RemoveClaim(itemId); + RemoveClaim(item); } } else diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs index 6eeda5f1f..afc015edf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs @@ -2,7 +2,6 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; -using System.Linq; namespace Barotrauma { @@ -14,7 +13,7 @@ namespace Barotrauma { get { - return ShowGaps; + return ShowGaps && SubEditorScreen.IsLayerVisible(this); } } @@ -25,11 +24,14 @@ namespace Barotrauma public override void Draw(SpriteBatch sb, bool editing, bool back = true) { + float depth = (ID % 255) * 0.000001f; + if (GameMain.DebugDraw && Screen.Selected.Cam.Zoom > 0.1f) { - Vector2 center = new Vector2(WorldRect.X + rect.Width / 2.0f, -(WorldRect.Y - rect.Height / 2.0f)); - GUI.DrawLine(sb, center, center + new Vector2(flowForce.X, -flowForce.Y) / 10.0f, GUI.Style.Red); - GUI.DrawLine(sb, center + Vector2.One * 5.0f, center + new Vector2(lerpedFlowForce.X, -lerpedFlowForce.Y) / 10.0f + Vector2.One * 5.0f, GUI.Style.Orange); + if (FlowTargetHull != null) + { + DrawArrow(FlowTargetHull, IsHorizontal ? rect.Height: rect.Width, Math.Abs(lerpedFlowForce.Length()), Color.Red * 0.3f); + } if (outsideCollisionBlocker.Enabled && Submarine != null) { @@ -42,12 +44,10 @@ namespace Barotrauma } } - if (!editing || !ShowGaps) { return; } + if (!editing || !ShowGaps || !SubEditorScreen.IsLayerVisible(this)) { return; } Color clr = (open == 0.0f) ? GUI.Style.Red : Color.Cyan; - if (IsHighlighted) clr = Color.Gold; - - float depth = (ID % 255) * 0.000001f; + if (IsHighlighted) { clr = Color.Gold; } GUI.DrawRectangle( sb, new Rectangle(WorldRect.X, -WorldRect.Y, rect.Width, rect.Height), @@ -81,33 +81,38 @@ namespace Barotrauma { for (int i = 0; i < linkedTo.Count; i++) { - Vector2 dir = IsHorizontal ? - new Vector2(Math.Sign(linkedTo[i].Rect.Center.X - rect.Center.X), 0.0f) - : new Vector2(0.0f, Math.Sign((rect.Y - rect.Height / 2.0f) - (linkedTo[i].Rect.Y - linkedTo[i].Rect.Height / 2.0f))); - - Vector2 arrowPos = new Vector2(WorldRect.Center.X, -(WorldRect.Y - WorldRect.Height / 2)); - arrowPos += new Vector2(dir.X * (WorldRect.Width / 2), dir.Y * (WorldRect.Height / 2)); - - float arrowWidth = 32.0f; - float arrowSize = 15.0f; - - bool invalidDir = false; - if (dir == Vector2.Zero) + if (linkedTo[i] is Hull hull) { - invalidDir = true; - dir = IsHorizontal ? Vector2.UnitX : Vector2.UnitY; + DrawArrow(hull, 32.0f, 15f, clr); } - - GUI.Arrow.Draw(sb, - arrowPos, invalidDir ? Color.Red : clr * 0.8f, - GUI.Arrow.Origin, MathUtils.VectorToAngle(dir) + MathHelper.PiOver2, - IsHorizontal ? - new Vector2(Math.Min(rect.Height, arrowWidth) / GUI.Arrow.size.X, arrowSize / GUI.Arrow.size.Y) : - new Vector2(Math.Min(rect.Width, arrowWidth) / GUI.Arrow.size.X, arrowSize / GUI.Arrow.size.Y), - SpriteEffects.None, depth); } } + void DrawArrow(Hull targetHull, float arrowWidth, float arrowLength, Color clr) + { + Vector2 dir = IsHorizontal ? + new Vector2(Math.Sign(targetHull.Rect.Center.X - rect.Center.X), 0.0f) + : new Vector2(0.0f, Math.Sign((rect.Y - rect.Height / 2.0f) - (targetHull.Rect.Y - targetHull.Rect.Height / 2.0f))); + + Vector2 arrowPos = new Vector2(WorldRect.Center.X, -(WorldRect.Y - WorldRect.Height / 2)); + arrowPos += new Vector2(dir.X * (WorldRect.Width / 2), dir.Y * (WorldRect.Height / 2)); + + bool invalidDir = false; + if (dir == Vector2.Zero) + { + invalidDir = true; + dir = IsHorizontal ? Vector2.UnitX : Vector2.UnitY; + } + + GUI.Arrow.Draw(sb, + arrowPos, invalidDir ? Color.Red : clr * 0.8f, + GUI.Arrow.Origin, MathUtils.VectorToAngle(dir) + MathHelper.PiOver2, + IsHorizontal ? + new Vector2(Math.Min(rect.Height, arrowWidth) / GUI.Arrow.size.X, arrowLength / GUI.Arrow.size.Y) : + new Vector2(Math.Min(rect.Width, arrowWidth) / GUI.Arrow.size.X, arrowLength / GUI.Arrow.size.Y), + SpriteEffects.None, depth); + } + if (IsSelected) { GUI.DrawRectangle(sb, @@ -128,8 +133,14 @@ namespace Barotrauma { //no flow particles between linked hulls (= rooms consisting of multiple hulls) if (hull1.linkedTo.Contains(hull2)) { return; } - if (hull1.linkedTo.Any(h => h.linkedTo.Contains(hull1) && h.linkedTo.Contains(hull2))) { return; } - if (hull2.linkedTo.Any(h => h.linkedTo.Contains(hull1) && h.linkedTo.Contains(hull2))) { return; } + foreach (var linkedEntity in hull1.linkedTo) + { + if (linkedEntity is Hull h && h.linkedTo.Contains(hull1) && h.linkedTo.Contains(hull2)) { return; } + } + foreach (var linkedEntity in hull2.linkedTo) + { + if (linkedEntity is Hull h && h.linkedTo.Contains(hull1) && h.linkedTo.Contains(hull2)) { return; } + } } Vector2 pos = Position; @@ -177,7 +188,7 @@ namespace Barotrauma "bubbles", (Submarine == null ? pos : pos + Submarine.Position), velocity, 0, flowTargetHull); - + particleTimer -= emitInterval; } } @@ -202,9 +213,9 @@ namespace Barotrauma "watersplash", (Submarine == null ? pos : pos + Submarine.Position) - Vector2.UnitY * Rand.Range(0.0f, 10.0f), velocity, 0, flowTargetHull); - if (particle != null) { + if (particle.CurrentHull == null) { GameMain.ParticleManager.RemoveParticle(particle); } particle.Size *= Math.Min(Math.Abs(flowForce.X / 500.0f), 5.0f); } } @@ -232,13 +243,13 @@ namespace Barotrauma } else { - if (Math.Sign(flowTargetHull.Rect.Y - rect.Y) != Math.Sign(lerpedFlowForce.Y)) return; + if (Math.Sign(flowTargetHull.Rect.Y - rect.Y) != Math.Sign(lerpedFlowForce.Y)) { return; } - float particlesPerSec = open * rect.Width * 0.3f * particleAmountMultiplier; + float particlesPerSec = Math.Max(open * rect.Width * 0.3f * particleAmountMultiplier, 20.0f); float emitInterval = 1.0f / particlesPerSec; while (particleTimer > emitInterval) { - pos.X = Rand.Range(rect.X, rect.X + rect.Width); + pos.X = Rand.Range(rect.X, rect.X + rect.Width + 1); Vector2 velocity = new Vector2( lerpedFlowForce.X * Rand.Range(0.5f, 0.7f), MathHelper.Clamp(lerpedFlowForce.Y, -500.0f, 1000.0f) * Rand.Range(0.5f, 0.7f)); @@ -246,17 +257,21 @@ namespace Barotrauma if (flowTargetHull.WaterVolume < flowTargetHull.Volume * 0.95f) { var splash = GameMain.ParticleManager.CreateParticle( - "watersplash", - Submarine == null ? pos : pos + Submarine.Position, - velocity, 0, FlowTargetHull); - if (splash != null) splash.Size = splash.Size * MathHelper.Clamp(rect.Width / 50.0f, 0.8f, 4.0f); + "watersplash", + Submarine == null ? pos : pos + Submarine.Position, + velocity, 0, FlowTargetHull); + if (splash != null) + { + if (splash.CurrentHull == null) { GameMain.ParticleManager.RemoveParticle(splash); } + splash.Size *= MathHelper.Clamp(rect.Width / 50.0f, 1.5f, 4.0f); + } } if (Math.Abs(flowForce.Y) > 190.0f && Rand.Range(0.0f, 1.0f) < 0.3f && flowTargetHull.WaterVolume > flowTargetHull.Volume * 0.1f) { GameMain.ParticleManager.CreateParticle( - "bubbles", - Submarine == null ? pos : pos + Submarine.Position, - flowForce / 2.0f, 0, FlowTargetHull); + "bubbles", + Submarine == null ? pos : pos + Submarine.Position, + flowForce / 2.0f, 0, FlowTargetHull); } particleTimer -= emitInterval; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index 13533eb7d..6a01d168d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -34,14 +34,27 @@ namespace Barotrauma private readonly List remoteDecals = new List(); private readonly HashSet pendingDecalUpdates = new HashSet(); - + private double lastAmbientLightEditTime; + private float drawSurface; + + public float DrawSurface + { + get { return drawSurface; } + set + { + if (Math.Abs(drawSurface - value) < 0.00001f) { return; } + drawSurface = MathHelper.Clamp(value, rect.Y - rect.Height, rect.Y); + update = true; + } + } + public override bool SelectableInEditor { get { - return ShowHulls; + return ShowHulls && SubEditorScreen.IsLayerVisible(this); } } @@ -133,91 +146,101 @@ namespace Barotrauma else { if (!entity.linkedTo.Contains(this)) { entity.linkedTo.Add(this); } - if (!linkedTo.Contains(this)) { linkedTo.Add(entity); } + if (!linkedTo.Contains(this)) { linkedTo.Add(entity); } } } } - partial void UpdateProjSpecific(float deltaTime, Camera cam) + partial void UpdateProjSpecific(float deltaTime, Camera _) { - serverUpdateDelay -= deltaTime; - if (serverUpdateDelay <= 0.0f) - { - ApplyRemoteState(); - } + float waterDepth = WaterVolume / rect.Width; + //interpolate the position of the rendered surface towards the "target surface" + drawSurface = Math.Max(MathHelper.Lerp( + drawSurface, + rect.Y - rect.Height + waterDepth, + deltaTime * 10.0f), rect.Y - rect.Height); - if (networkUpdatePending) + if (GameMain.Client != null) { - networkUpdateTimer += deltaTime; - if (networkUpdateTimer > 0.2f) + serverUpdateDelay -= deltaTime; + if (serverUpdateDelay <= 0.0f) { - if (!pendingSectionUpdates.Any() && !pendingDecalUpdates.Any()) - { - GameMain.NetworkMember?.CreateEntityEvent(this); - } - foreach (Decal decal in pendingDecalUpdates) - { - GameMain.NetworkMember?.CreateEntityEvent(this, new object[] { decal }); - } - foreach (int pendingSectionUpdate in pendingSectionUpdates) - { - GameMain.NetworkMember?.CreateEntityEvent(this, new object[] { pendingSectionUpdate }); - } - pendingSectionUpdates.Clear(); - networkUpdatePending = false; - networkUpdateTimer = 0.0f; + ApplyRemoteState(); } - } - - if (!IdFreed) - { - if (EditWater) + if (networkUpdatePending) { - Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition); - if (Submarine.RectContains(WorldRect, position)) + networkUpdateTimer += deltaTime; + if (networkUpdateTimer > 0.2f) { - if (PlayerInput.PrimaryMouseButtonHeld()) + if (!pendingSectionUpdates.Any() && !pendingDecalUpdates.Any()) { - WaterVolume += 1500.0f; - networkUpdatePending = true; - serverUpdateDelay = 0.5f; + GameMain.NetworkMember?.CreateEntityEvent(this); } - else if (PlayerInput.SecondaryMouseButtonHeld()) + foreach (Decal decal in pendingDecalUpdates) { - WaterVolume -= 1500.0f; - networkUpdatePending = true; - serverUpdateDelay = 0.5f; + GameMain.NetworkMember?.CreateEntityEvent(this, new object[] { decal }); } - } - } - else if (EditFire) - { - Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition); - if (Submarine.RectContains(WorldRect, position)) - { - if (PlayerInput.PrimaryMouseButtonClicked()) + foreach (int pendingSectionUpdate in pendingSectionUpdates) { - new FireSource(position, this, isNetworkMessage: true); - networkUpdatePending = true; - serverUpdateDelay = 0.5f; + GameMain.NetworkMember?.CreateEntityEvent(this, new object[] { pendingSectionUpdate }); } + pendingSectionUpdates.Clear(); + networkUpdatePending = false; + networkUpdateTimer = 0.0f; } } } - if (waterVolume < 1.0f) { return; } + /*if (waterVolume < 1.0f) { return; } for (int i = 1; i < waveY.Length - 1; i++) { float maxDelta = Math.Max(Math.Abs(rightDelta[i]), Math.Abs(leftDelta[i])); - if (maxDelta > 1.0f && maxDelta > Rand.Range(1.0f, 10.0f)) + if (maxDelta > 0.1f && maxDelta > Rand.Range(0.1f, 10.0f)) { var particlePos = new Vector2(rect.X + WaveWidth * i, surface + waveY[i]); - if (Submarine != null) particlePos += Submarine.Position; + if (Submarine != null) { particlePos += Submarine.Position; } GameMain.ParticleManager.CreateParticle("mist", particlePos, new Vector2(0.0f, -50.0f), 0.0f, this); } + }*/ + } + + public static void UpdateCheats(float deltaTime, Camera cam) + { + bool primaryMouseButtonHeld = PlayerInput.PrimaryMouseButtonHeld(); + bool secondaryMouseButtonHeld = PlayerInput.SecondaryMouseButtonHeld(); + if (!primaryMouseButtonHeld && !secondaryMouseButtonHeld) { return; } + + Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition); + Hull hull = FindHull(position); + + if (hull == null || hull.IdFreed) { return; } + if (EditWater) + { + if (primaryMouseButtonHeld) + { + hull.WaterVolume += 100000.0f * deltaTime; + hull.networkUpdatePending = true; + hull.serverUpdateDelay = 0.5f; + } + else if (secondaryMouseButtonHeld) + { + hull.WaterVolume -= 100000.0f * deltaTime; + hull.networkUpdatePending = true; + hull.serverUpdateDelay = 0.5f; + } + + } + else if (EditFire) + { + if (primaryMouseButtonHeld) + { + new FireSource(position, hull, isNetworkMessage: true); + hull.networkUpdatePending = true; + hull.serverUpdateDelay = 0.5f; + } } } @@ -243,7 +266,7 @@ namespace Barotrauma return; } - if (!ShowHulls && !GameMain.DebugDraw) { return; } + if ((!ShowHulls || !SubEditorScreen.IsLayerVisible(this)) && !GameMain.DebugDraw) { return; } if (!editing && (!GameMain.DebugDraw || Screen.Selected.Cam.Zoom < 0.1f)) { return; } @@ -385,45 +408,42 @@ namespace Barotrauma } } + private static readonly Vector3[] corners = new Vector3[6]; + private static readonly Vector2[] uvCoords = new Vector2[4]; + private static readonly Vector3[] prevCorners = new Vector3[2]; + private static readonly Vector2[] prevUVs = new Vector2[2]; + private void UpdateVertices(Camera cam, EntityGrid entityGrid, WaterRenderer renderer) { Vector2 submarinePos = Submarine == null ? Vector2.Zero : Submarine.DrawPosition; //if there's no more space in the buffer, don't render the water in the hull - //not an ideal solution, but this seems to only happen in cases where the missing + //not an ideal solution, but this seems to only happen in cases where the missing //water is not very noticeable (e.g. zoomed very far out so that multiple subs and ruins are visible) if (renderer.PositionInBuffer > renderer.vertices.Length - 6) { return; } - if (!renderer.IndoorsVertices.ContainsKey(entityGrid)) - { - renderer.IndoorsVertices[entityGrid] = new VertexPositionColorTexture[WaterRenderer.DefaultIndoorsBufferSize]; - renderer.PositionInIndoorsBuffer[entityGrid] = 0; - } - //calculate where the surface should be based on the water volume float top = rect.Y + submarinePos.Y; float bottom = top - rect.Height; float renderSurface = drawSurface + submarinePos.Y; - if (bottom > cam.WorldView.Y || top < cam.WorldView.Y - cam.WorldView.Height) return; + if (bottom > cam.WorldView.Y || top < cam.WorldView.Y - cam.WorldView.Height) { return; } + if (rect.X + submarinePos.X > cam.WorldView.Right || rect.Right + submarinePos.X < cam.WorldView.X) { return; } - Matrix transform = cam.Transform * Matrix.CreateOrthographic(GameMain.GraphicsWidth, GameMain.GraphicsHeight, -1, 1) * 0.5f; + Matrix transform = cam.Transform * Matrix.CreateOrthographic(GameMain.GraphicsWidth, GameMain.GraphicsHeight, -1, 1) * 0.5f; if (!update) { // create the four corners of our triangle. - Vector3[] corners = new Vector3[4]; - corners[0] = new Vector3(rect.X, rect.Y, 0.0f); corners[1] = new Vector3(rect.X + rect.Width, rect.Y, 0.0f); corners[2] = new Vector3(corners[1].X, rect.Y - rect.Height, 0.0f); corners[3] = new Vector3(corners[0].X, corners[2].Y, 0.0f); - Vector2[] uvCoords = new Vector2[4]; for (int i = 0; i < 4; i++) { corners[i] += new Vector3(submarinePos, 0.0f); @@ -443,6 +463,15 @@ namespace Barotrauma return; } + if (!renderer.IndoorsVertices.ContainsKey(entityGrid)) + { + renderer.IndoorsVertices[entityGrid] = new VertexPositionColorTexture[WaterRenderer.DefaultIndoorsBufferSize]; + } + if (!renderer.PositionInIndoorsBuffer.ContainsKey(entityGrid)) + { + renderer.PositionInIndoorsBuffer[entityGrid] = 0; + } + float x = rect.X; if (Submarine != null) { x += Submarine.DrawPosition.X; } @@ -454,20 +483,15 @@ namespace Barotrauma x += start * WaveWidth; - Vector3[] prevCorners = new Vector3[2]; - Vector2[] prevUVs = new Vector2[2]; - int width = WaveWidth; - + for (int i = start; i < end; i++) { - Vector3[] corners = new Vector3[6]; - //top left corners[0] = new Vector3(x, top, 0.0f); //watersurface left corners[3] = new Vector3(corners[0].X, renderSurface + waveY[i], 0.0f); - + //top right corners[1] = new Vector3(x + width, top, 0.0f); //watersurface right @@ -477,7 +501,7 @@ namespace Barotrauma corners[4] = new Vector3(x, bottom, 0.0f); //bottom right corners[5] = new Vector3(x + width, bottom, 0.0f); - + Vector2[] uvCoords = new Vector2[4]; for (int n = 0; n < 4; n++) { @@ -714,7 +738,7 @@ namespace Barotrauma } remoteBackgroundSections.Clear(); - if (remoteDecals.Any()) + if (remoteDecals.Count > 0) { decals.Clear(); foreach (RemoteDecal remoteDecal in remoteDecals) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs index 70d2d2358..b8ef5800c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs @@ -95,7 +95,7 @@ namespace Barotrauma var prefab = ToolBox.SelectWeightedRandom(availablePrefabs, availablePrefabs.Select(p => p.GetCommonness(level.GenerationParams)).ToList(), Rand.RandSync.ClientOnly); if (prefab == null) { break; } - int amount = Rand.Range(prefab.SwarmMin, prefab.SwarmMax, Rand.RandSync.ClientOnly); + int amount = Rand.Range(prefab.SwarmMin, prefab.SwarmMax + 1, Rand.RandSync.ClientOnly); List swarmMembers = new List(); for (int n = 0; n < amount; n++) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs index dab22d962..71df6f48a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs @@ -51,7 +51,7 @@ namespace Barotrauma public readonly WaterVertexData IndoorsSurfaceBottomColor = new WaterVertexData(0.2f, 0.1f, 0.9f, 1.0f); public VertexPositionTexture[] vertices = new VertexPositionTexture[DefaultBufferSize]; - public Dictionary IndoorsVertices = new Dictionary();// VertexPositionColorTexture[DefaultBufferSize * 2]; + public Dictionary IndoorsVertices = new Dictionary(); public Effect WaterEffect { @@ -81,13 +81,17 @@ namespace Barotrauma if (basicEffect == null) { - basicEffect = new BasicEffect(GameMain.Instance.GraphicsDevice); - basicEffect.VertexColorEnabled = false; - - basicEffect.TextureEnabled = true; + basicEffect = new BasicEffect(GameMain.Instance.GraphicsDevice) + { + VertexColorEnabled = false, + TextureEnabled = true + }; } } + private readonly VertexPositionColorTexture[] tempVertices = new VertexPositionColorTexture[6]; + private readonly Vector3[] tempCorners = new Vector3[4]; + public void RenderWater(SpriteBatch spriteBatch, RenderTarget2D texture, Camera cam) { spriteBatch.GraphicsDevice.BlendState = BlendState.NonPremultiplied; @@ -139,29 +143,26 @@ namespace Barotrauma WaterEffect.CurrentTechnique.Passes[0].Apply(); - VertexPositionColorTexture[] verts = new VertexPositionColorTexture[6]; - Rectangle view = cam != null ? cam.WorldView : spriteBatch.GraphicsDevice.Viewport.Bounds; - var corners = new Vector3[4]; - corners[0] = new Vector3(view.X, view.Y, 0.1f); - corners[1] = new Vector3(view.Right, view.Y, 0.1f); - corners[2] = new Vector3(view.Right, view.Y - view.Height, 0.1f); - corners[3] = new Vector3(view.X, view.Y - view.Height, 0.1f); + tempCorners[0] = new Vector3(view.X, view.Y, 0.1f); + tempCorners[1] = new Vector3(view.Right, view.Y, 0.1f); + tempCorners[2] = new Vector3(view.Right, view.Y - view.Height, 0.1f); + tempCorners[3] = new Vector3(view.X, view.Y - view.Height, 0.1f); WaterVertexData backGroundColor = new WaterVertexData(0.1f, 0.1f, 0.5f, 1.0f); - verts[0] = new VertexPositionColorTexture(corners[0], backGroundColor, Vector2.Zero); - verts[1] = new VertexPositionColorTexture(corners[1], backGroundColor, Vector2.Zero); - verts[2] = new VertexPositionColorTexture(corners[2], backGroundColor, Vector2.Zero); - verts[3] = new VertexPositionColorTexture(corners[0], backGroundColor, Vector2.Zero); - verts[4] = new VertexPositionColorTexture(corners[2], backGroundColor, Vector2.Zero); - verts[5] = new VertexPositionColorTexture(corners[3], backGroundColor, Vector2.Zero); + tempVertices[0] = new VertexPositionColorTexture(tempCorners[0], backGroundColor, Vector2.Zero); + tempVertices[1] = new VertexPositionColorTexture(tempCorners[1], backGroundColor, Vector2.Zero); + tempVertices[2] = new VertexPositionColorTexture(tempCorners[2], backGroundColor, Vector2.Zero); + tempVertices[3] = new VertexPositionColorTexture(tempCorners[0], backGroundColor, Vector2.Zero); + tempVertices[4] = new VertexPositionColorTexture(tempCorners[2], backGroundColor, Vector2.Zero); + tempVertices[5] = new VertexPositionColorTexture(tempCorners[3], backGroundColor, Vector2.Zero); - spriteBatch.GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, verts, 0, 2); + spriteBatch.GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, tempVertices, 0, 2); foreach (KeyValuePair subVerts in IndoorsVertices) { - if (!PositionInIndoorsBuffer.ContainsKey(subVerts.Key) || PositionInIndoorsBuffer[subVerts.Key] == 0) continue; + if (!PositionInIndoorsBuffer.ContainsKey(subVerts.Key) || PositionInIndoorsBuffer[subVerts.Key] == 0) { continue; } offset = WavePos; if (subVerts.Key.Submarine != null) { offset -= subVerts.Key.Submarine.WorldPosition; } @@ -207,11 +208,23 @@ namespace Barotrauma basicEffect.CurrentTechnique.Passes[0].Apply(); } + private readonly List buffersToRemove = new List(); public void ResetBuffers() { PositionInBuffer = 0; PositionInIndoorsBuffer.Clear(); - IndoorsVertices.Clear(); + buffersToRemove.Clear(); + foreach (var buffer in IndoorsVertices.Keys) + { + if (buffer.Submarine?.Removed ?? false) + { + buffersToRemove.Add(buffer); + } + } + foreach (var bufferToRemove in buffersToRemove) + { + IndoorsVertices.Remove(bufferToRemove); + } } public void Dispose() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index a47285cca..151b148dd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -517,7 +517,7 @@ namespace Barotrauma.Lights private void RefreshConvexHullList(ConvexHullList chList, Vector2 lightPos, Submarine sub) { var fullChList = ConvexHull.HullLists.Find(x => x.Submarine == sub); - if (fullChList == null) return; + if (fullChList == null) { return; } chList.List = fullChList.List.FindAll(ch => ch.Enabled && MathUtils.CircleIntersectsRectangle(lightPos, TextureRange, ch.BoundingBox)); @@ -530,105 +530,121 @@ namespace Barotrauma.Lights /// private void CheckHullsInRange() { - List subs = new List(Submarine.Loaded); - subs.Add(null); - - foreach (Submarine sub in subs) + foreach (Submarine sub in Submarine.Loaded) { - //find the list of convexhulls that belong to the sub - var chList = hullsInRange.Find(x => x.Submarine == sub); + CheckHullsInRange(sub); + } + //check convex hulls that aren't in any sub + CheckHullsInRange(null); + } - //not found -> create one - if (chList == null) + private void CheckHullsInRange(Submarine sub) + { + //find the list of convexhulls that belong to the sub + ConvexHullList chList = null; + foreach (var ch in hullsInRange) + { + if (ch.Submarine == sub) { - chList = new ConvexHullList(sub); - hullsInRange.Add(chList); - NeedsRecalculation = true; - } - - if (chList.List.Any(ch => ch.LastVertexChangeTime > lastRecalculationTime && !chList.IsHidden.Contains(ch))) - { - NeedsRecalculation = true; - } - - Vector2 lightPos = position; - if (ParentSub == null) - { - //light and the convexhulls are both outside - if (sub == null) - { - if (NeedsHullCheck) - { - RefreshConvexHullList(chList, lightPos, null); - } - } - //light is outside, convexhulls inside a sub - else - { - lightPos -= sub.Position; - - Rectangle subBorders = sub.Borders; - subBorders.Location += sub.HiddenSubPosition.ToPoint() - new Point(0, sub.Borders.Height); - - //only draw if the light overlaps with the sub - if (!MathUtils.CircleIntersectsRectangle(lightPos, TextureRange, subBorders)) - { - if (chList.List.Count > 0) NeedsRecalculation = true; - chList.List.Clear(); - continue; - } - - RefreshConvexHullList(chList, lightPos, sub); - } - } - else - { - //light is inside, convexhull outside - if (sub == null) continue; - - //light and convexhull are both inside the same sub - if (sub == ParentSub) - { - if (NeedsHullCheck) - { - RefreshConvexHullList(chList, lightPos, sub); - } - } - //light and convexhull are inside different subs - else - { - if (sub.DockedTo.Contains(ParentSub) && !NeedsHullCheck) continue; - - lightPos -= (sub.Position - ParentSub.Position); - - Rectangle subBorders = sub.Borders; - subBorders.Location += sub.HiddenSubPosition.ToPoint() - new Point(0, sub.Borders.Height); - - //don't draw any shadows if the light doesn't overlap with the borders of the sub - if (!MathUtils.CircleIntersectsRectangle(lightPos, TextureRange, subBorders)) - { - if (chList.List.Count > 0) NeedsRecalculation = true; - chList.List.Clear(); - continue; - } - - //recalculate vertices if the subs have moved > 5 px relative to each other - Vector2 diff = ParentSub.WorldPosition - sub.WorldPosition; - if (!diffToSub.TryGetValue(sub, out Vector2 prevDiff)) - { - diffToSub.Add(sub, diff); - NeedsRecalculation = true; - } - else if (Vector2.DistanceSquared(diff, prevDiff) > 5.0f * 5.0f) - { - diffToSub[sub] = diff; - NeedsRecalculation = true; - } - - RefreshConvexHullList(chList, lightPos, sub); - } + chList = ch; + break; } } + + //not found -> create one + if (chList == null) + { + chList = new ConvexHullList(sub); + hullsInRange.Add(chList); + NeedsRecalculation = true; + } + + foreach (var ch in chList.List) + { + if (ch.LastVertexChangeTime > lastRecalculationTime && !chList.IsHidden.Contains(ch)) + { + NeedsRecalculation = true; + break; + } + } + + Vector2 lightPos = position; + if (ParentSub == null) + { + //light and the convexhulls are both outside + if (sub == null) + { + if (NeedsHullCheck) + { + RefreshConvexHullList(chList, lightPos, null); + } + } + //light is outside, convexhulls inside a sub + else + { + lightPos -= sub.Position; + + Rectangle subBorders = sub.Borders; + subBorders.Location += sub.HiddenSubPosition.ToPoint() - new Point(0, sub.Borders.Height); + + //only draw if the light overlaps with the sub + if (!MathUtils.CircleIntersectsRectangle(lightPos, TextureRange, subBorders)) + { + if (chList.List.Count > 0) { NeedsRecalculation = true; } + chList.List.Clear(); + return; + } + + RefreshConvexHullList(chList, lightPos, sub); + } + } + else + { + //light is inside, convexhull outside + if (sub == null) { return; } + + //light and convexhull are both inside the same sub + if (sub == ParentSub) + { + if (NeedsHullCheck) + { + RefreshConvexHullList(chList, lightPos, sub); + } + } + //light and convexhull are inside different subs + else + { + if (sub.DockedTo.Contains(ParentSub) && !NeedsHullCheck) { return; } + + lightPos -= (sub.Position - ParentSub.Position); + + Rectangle subBorders = sub.Borders; + subBorders.Location += sub.HiddenSubPosition.ToPoint() - new Point(0, sub.Borders.Height); + + //don't draw any shadows if the light doesn't overlap with the borders of the sub + if (!MathUtils.CircleIntersectsRectangle(lightPos, TextureRange, subBorders)) + { + if (chList.List.Count > 0) { NeedsRecalculation = true; } + chList.List.Clear(); + return; + } + + //recalculate vertices if the subs have moved > 5 px relative to each other + Vector2 diff = ParentSub.WorldPosition - sub.WorldPosition; + if (!diffToSub.TryGetValue(sub, out Vector2 prevDiff)) + { + diffToSub.Add(sub, diff); + NeedsRecalculation = true; + } + else if (Vector2.DistanceSquared(diff, prevDiff) > 5.0f * 5.0f) + { + diffToSub[sub] = diff; + NeedsRecalculation = true; + } + + RefreshConvexHullList(chList, lightPos, sub); + } + } } private List FindRaycastHits() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs index 4c96fb6a2..2dc444032 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs @@ -75,10 +75,12 @@ namespace Barotrauma if (linkedTo.Contains(entity)) { linkedTo.Remove(entity); + entity.linkedTo.Remove(this); } else { linkedTo.Add(entity); + if (!entity.linkedTo.Contains(this)) { entity.linkedTo.Add(this); } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index 91eb1ce2a..2c34707c6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -130,18 +130,31 @@ namespace Barotrauma int tilesY = (int)Math.Ceiling(Height / tileSize.Y); mapTiles = new Sprite[tilesX, tilesY]; tileDiscovered = new bool[tilesX, tilesY]; + HashSet missingBiomes = new HashSet(); for (int x = 0; x < tilesX; x++) { for (int y = 0; y < tilesY; y++) { var biome = GetBiome(x * tileSize.X); - var tileList = generationParams.MapTiles.ContainsKey(biome.Identifier) ? - generationParams.MapTiles[biome.Identifier] : - generationParams.MapTiles.Values.First(); + List tileList = null; + if (generationParams.MapTiles.ContainsKey(biome.Identifier)) + { + tileList = generationParams.MapTiles[biome.Identifier]; + } + else + { + tileList = generationParams.MapTiles.Values.First(); + missingBiomes.Add(biome); + } mapTiles[x, y] = tileList[x % tileList.Count]; } } + foreach (var missingBiome in missingBiomes) + { + DebugConsole.ThrowError($"Could not find campaign map sprites for the biome \"{missingBiome.Identifier}\". Using the sprites of the first biome instead..."); + } + RemoveFogOfWar(StartLocation); GenerateLocationConnectionVisuals(); @@ -194,7 +207,11 @@ namespace Barotrauma private void RemoveFogOfWar(Location location, bool removeFromAdjacentLocations = true) { if (location == null) { return; } - Vector2 mapTileSize = mapTiles[0, 0].size * generationParams.MapTileScale; + + var mapTile = generationParams.MapTiles.Values.FirstOrDefault()?.FirstOrDefault(); + if (mapTile == null) { return; } + + Vector2 mapTileSize = mapTile.size * generationParams.MapTileScale; int startX = (int)Math.Max(Math.Floor(location.MapPosition.X / mapTileSize.X - 0.25f), 0); int startY = (int)Math.Max(Math.Floor(location.MapPosition.Y / mapTileSize.Y - 0.25f), 0); int endX = (int)Math.Min(Math.Floor(location.MapPosition.X / mapTileSize.X + 0.25f), mapTiles.GetLength(0)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index 642813f4f..1c9c15096 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -5,6 +5,7 @@ using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using Barotrauma.Lights; @@ -36,9 +37,6 @@ namespace Barotrauma private static List highlightedList = new List(); - // Test feature. Not yet saved. - public static Dictionary> SelectionGroups { get; private set; } = new Dictionary>(); - private static float highlightTimer; private static GUIListBox highlightedListBox; @@ -197,7 +195,7 @@ namespace Barotrauma { Paste(cam.ScreenToWorld(PlayerInput.MousePosition)); } - else if (PlayerInput.KeyHit(Keys.G)) + /*else if (PlayerInput.KeyHit(Keys.G)) { if (SelectedList.Any()) { @@ -217,7 +215,7 @@ namespace Barotrauma } } } - } + }*/ } } @@ -360,14 +358,15 @@ namespace Barotrauma { if (highLightedEntity != null) { - if (SelectionGroups.TryGetValue(highLightedEntity, out HashSet group)) + if (SubEditorScreen.IsLayerLinked(highLightedEntity)/*SelectionGroups.TryGetValue(highLightedEntity, out HashSet group)*/) { - foreach (MapEntity entity in group.Where(e => !newSelection.Contains(e))) + ImmutableHashSet entitiesInSameLayer = SubEditorScreen.GetEntitiesInSameLayer(highLightedEntity); + foreach (MapEntity entity in entitiesInSameLayer.Where(e => !newSelection.Contains(e))) { newSelection.Add(entity); } - foreach (MapEntity entity in group) + foreach (MapEntity entity in entitiesInSameLayer) { entity.IsIncludedInSelection = true; } @@ -769,34 +768,40 @@ namespace Barotrauma switch (e) { case Item item: - { - if (item.FlippedX && item.Prefab.CanSpriteFlipX) spriteEffects ^= SpriteEffects.FlipHorizontally; - if (item.flippedY && item.Prefab.CanSpriteFlipY) spriteEffects ^= SpriteEffects.FlipVertically; - break; - } + { + if (item.FlippedX && item.Prefab.CanSpriteFlipX) { spriteEffects ^= SpriteEffects.FlipHorizontally; } + if (item.flippedY && item.Prefab.CanSpriteFlipY) { spriteEffects ^= SpriteEffects.FlipVertically; } + var wire = item.GetComponent(); + if (wire != null && wire.Item.body != null && !wire.Item.body.Enabled) + { + wire.Draw(spriteBatch, editing: false, new Vector2(moveAmount.X, -moveAmount.Y)); + continue; + } + break; + } case Structure structure: - { - if (structure.FlippedX && structure.Prefab.CanSpriteFlipX) spriteEffects ^= SpriteEffects.FlipHorizontally; - if (structure.flippedY && structure.Prefab.CanSpriteFlipY) spriteEffects ^= SpriteEffects.FlipVertically; - break; - } + { + if (structure.FlippedX && structure.Prefab.CanSpriteFlipX) { spriteEffects ^= SpriteEffects.FlipHorizontally; } + if (structure.flippedY && structure.Prefab.CanSpriteFlipY) { spriteEffects ^= SpriteEffects.FlipVertically; } + break; + } case WayPoint wayPoint: - { - Vector2 drawPos = e.WorldPosition; - drawPos.Y = -drawPos.Y; - drawPos += moveAmount; - wayPoint.Draw(spriteBatch, drawPos); - continue; - } + { + Vector2 drawPos = e.WorldPosition; + drawPos.Y = -drawPos.Y; + drawPos += moveAmount; + wayPoint.Draw(spriteBatch, drawPos); + continue; + } case LinkedSubmarine linkedSub: - { - var ma = moveAmount; - ma.Y = -ma.Y; - Vector2 lPos = linkedSub.Position; - lPos += ma; - linkedSub.Draw(spriteBatch, lPos, alpha: 0.5f); - break; - } + { + var ma = moveAmount; + ma.Y = -ma.Y; + Vector2 lPos = linkedSub.Position; + lPos += ma; + linkedSub.Draw(spriteBatch, lPos, alpha: 0.5f); + break; + } } e.prefab?.DrawPlacing(spriteBatch, new Rectangle(e.WorldRect.Location + new Point((int)moveAmount.X, (int)-moveAmount.Y), e.WorldRect.Size), e.Scale, spriteEffects); @@ -1197,14 +1202,24 @@ namespace Barotrauma Rectangle selectionRect = Submarine.AbsRect(pos, size); - foreach (MapEntity e in mapEntityList) + foreach (MapEntity entity in mapEntityList) { - if (!e.SelectableInEditor) continue; + if (!entity.SelectableInEditor) { continue; } - if (Submarine.RectsOverlap(selectionRect, e.rect)) + if (Submarine.RectsOverlap(selectionRect, entity.rect)) { - foundEntities.Add(e); - e.IsIncludedInSelection = true; + foundEntities.Add(entity); + entity.IsIncludedInSelection = true; + + if (SubEditorScreen.IsLayerLinked(entity)) + { + ImmutableHashSet entitiesInSameLayer = SubEditorScreen.GetEntitiesInSameLayer(entity); + foreach (MapEntity layerEntity in entitiesInSameLayer.Where(e => !foundEntities.Contains(e))) + { + foundEntities.Add(layerEntity); + layerEntity.IsIncludedInSelection = true; + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index d8df8b2a9..55bba4cda 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -30,6 +30,9 @@ namespace Barotrauma { return false; } + + if (!SubEditorScreen.IsLayerVisible(this)) { return false; } + return HasBody ? ShowWalls : ShowStructures; } } @@ -244,8 +247,10 @@ namespace Barotrauma public override void Draw(SpriteBatch spriteBatch, bool editing, bool back = true) { if (prefab.sprite == null) { return; } + if (editing) { + if (!SubEditorScreen.IsLayerVisible(this)) { return; } if (!HasBody && !ShowStructures) { return; } if (HasBody && !ShowWalls) { return; } } @@ -273,6 +278,7 @@ namespace Barotrauma if (prefab.sprite == null) { return; } if (editing) { + if (!SubEditorScreen.IsLayerVisible(this)) { return; } if (!HasBody && !ShowStructures) { return; } if (HasBody && !ShowWalls) { return; } } @@ -285,13 +291,11 @@ namespace Barotrauma //color = Color.Lerp(color, Color.Gold, 0.5f); color = spriteColor; - - Vector2 rectSize = rect.Size.ToVector2(); if (BodyWidth > 0.0f) { rectSize.X = BodyWidth; } if (BodyHeight > 0.0f) { rectSize.Y = BodyHeight; } - Vector2 bodyPos = WorldPosition + BodyOffset; + Vector2 bodyPos = WorldPosition + BodyOffset * Scale; GUI.DrawRectangle(spriteBatch, new Vector2(bodyPos.X, -bodyPos.Y), rectSize.X, rectSize.Y, BodyRotation, Color.White, thickness: Math.Max(1, (int)(2 / Screen.Selected.Cam.Zoom))); @@ -465,7 +469,8 @@ namespace Barotrauma public void UpdateSpriteStates(float deltaTime) { - DecorativeSprite.UpdateSpriteStates(Prefab.DecorativeSpriteGroups, spriteAnimState, ID, deltaTime, ConditionalMatches); + if (Prefab.DecorativeSpriteGroups.Count == 0) { return; } + DecorativeSprite.UpdateSpriteStates(Prefab.DecorativeSpriteGroups, spriteAnimState, ID, deltaTime, ConditionalMatches); foreach (int spriteGroup in Prefab.DecorativeSpriteGroups.Keys) { for (int i = 0; i < Prefab.DecorativeSpriteGroups[spriteGroup].Count; i++) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index 2890e470f..9f35f13be 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -25,14 +25,11 @@ namespace Barotrauma public readonly bool Stream; public readonly bool IgnoreMuffling; - - public string Filename - { - get { return Sound?.Filename; } - } + public readonly string Filename; public RoundSound(XElement element, Sound sound) { + Filename = sound?.Filename; Sound = sound; Stream = sound.Stream; Range = element.GetAttributeFloat("range", 1000.0f); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineBody.cs index 2a63d6690..57bbe7a9e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineBody.cs @@ -55,9 +55,9 @@ namespace Barotrauma if (closestSub != null && subsToMove.Contains(closestSub)) { GameMain.GameScreen.Cam.Position += moveAmount; - if (GameMain.GameScreen.Cam.TargetPos != Vector2.Zero) GameMain.GameScreen.Cam.TargetPos += moveAmount; + if (GameMain.GameScreen.Cam.TargetPos != Vector2.Zero) { GameMain.GameScreen.Cam.TargetPos += moveAmount; } - if (Character.Controlled != null) Character.Controlled.CursorPosition += moveAmount; + if (Character.Controlled != null) { Character.Controlled.CursorPosition += moveAmount; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs index c50ae90a9..2977533a7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs @@ -1,6 +1,6 @@ using Microsoft.Xna.Framework; using System; -using System.Collections.Generic; +using System.Globalization; using System.Linq; namespace Barotrauma @@ -91,7 +91,7 @@ namespace Barotrauma public void CreateSpecsWindow(GUIListBox parent, ScalableFont font, bool includeTitle = true, bool includeClass = true, bool includeDescription = false) { float leftPanelWidth = 0.6f; - float rightPanelWidth = 0.4f; + float rightPanelWidth = 0.4f / leftPanelWidth; string className = !HasTag(SubmarineTag.Shuttle) ? TextManager.Get($"submarineclass.{SubmarineClass}") : TextManager.Get("shuttle"); int classHeight = (int)GUI.SubHeadingFont.MeasureString(className).Y; @@ -110,6 +110,17 @@ namespace Barotrauma submarineClassText = new GUITextBlock(new RectTransform(new Point(leftPanelWidthInt, classHeight), parent.Content.RectTransform), className, textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont) { CanBeFocused = false }; submarineClassText.RectTransform.MinSize = new Point(0, (int)submarineClassText.TextSize.Y); } + + if (Price > 0) + { + var priceText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), parent.Content.RectTransform), + TextManager.Get("subeditor.price"), textAlignment: Alignment.TopLeft, font: font, wrap: true) + { CanBeFocused = false }; + new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), priceText.RectTransform, Anchor.TopRight, Pivot.TopLeft), + TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", Price)), textAlignment: Alignment.TopLeft, font: font, wrap: true) + { CanBeFocused = false }; + } + Vector2 realWorldDimensions = Dimensions * Physics.DisplayToRealWorldRatio; if (realWorldDimensions != Vector2.Zero) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs index 5083fe7c1..900a6d355 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs @@ -316,11 +316,11 @@ namespace Barotrauma var srcRect = prefab.sprite.SourceRect; SpriteEffects spriteEffects = SpriteEffects.None; - if (flippedX) + if (flippedX && ((prefab as ItemPrefab)?.CanSpriteFlipX ?? true)) { spriteEffects |= SpriteEffects.FlipHorizontally; } - if (flippedY) + if (flippedY && ((prefab as ItemPrefab)?.CanSpriteFlipY ?? true)) { spriteEffects |= SpriteEffects.FlipVertically; } @@ -651,7 +651,11 @@ namespace Barotrauma public void Dispose() { - previewFrame = null; + if (previewFrame != null) + { + previewFrame.RectTransform.Parent = null; + previewFrame = null; + } spriteRecorder?.Dispose(); isDisposed = true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs index 0696a24a2..b2cbce8ff 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs @@ -90,14 +90,14 @@ namespace Barotrauma { GUI.DrawLine(spriteBatch, drawPos, - new Vector2(ConnectedGap.WorldPosition.X, -ConnectedGap.WorldPosition.Y), + new Vector2(ConnectedGap.DrawPosition.X, -ConnectedGap.DrawPosition.Y), GUI.Style.Green * 0.5f, width: 1); } if (Ladders != null) { GUI.DrawLine(spriteBatch, drawPos, - new Vector2(Ladders.Item.WorldPosition.X, -Ladders.Item.WorldPosition.Y), + new Vector2(Ladders.Item.DrawPosition.X, -Ladders.Item.DrawPosition.Y), GUI.Style.Green * 0.5f, width: 1); } @@ -146,6 +146,7 @@ namespace Barotrauma private bool IsHidden() { + if (!SubEditorScreen.IsLayerVisible(this)) { return false; } if (spawnType == SpawnType.Path) { return (!GameMain.DebugDraw && !ShowWayPoints); @@ -294,7 +295,7 @@ namespace Barotrauma else { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), TextManager.Get("Spawnpoint"), font: GUI.LargeFont); - + var spawnTypeContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), isHorizontal: true) { Stretch = true, @@ -318,7 +319,10 @@ namespace Barotrauma }; var descText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), - TextManager.Get("IDCardDescription"), font: GUI.SmallFont); + TextManager.Get("IDCardDescription"), font: GUI.SmallFont) + { + ToolTip = TextManager.Get("IDCardDescriptionTooltip") + }; GUITextBox propertyBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), descText.RectTransform, Anchor.CenterRight), IdCardDesc) { MaxTextLength = 150, @@ -342,7 +346,10 @@ namespace Barotrauma }; var idCardTagsText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), - TextManager.Get("IDCardTags"), font: GUI.SmallFont); + TextManager.Get("IDCardTags"), font: GUI.SmallFont) + { + ToolTip = TextManager.Get("IDCardTagsTooltip") + }; propertyBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), idCardTagsText.RectTransform, Anchor.CenterRight), string.Join(", ", idCardTags)) { MaxTextLength = 60, @@ -414,6 +421,6 @@ namespace Barotrauma PositionEditingHUD(); return editingHUD; - } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs index d76d85470..0e853822f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs @@ -46,6 +46,13 @@ namespace Barotrauma.Networking senderName = senderCharacter.Name; } } + + Color? textColor = null; + if (msg.ReadBoolean()) + { + textColor = msg.ReadColorR8G8B8A8(); + } + msg.ReadPadBits(); switch (type) @@ -78,7 +85,7 @@ namespace Barotrauma.Networking txt = orderPrefab.GetChatMessage(orderMessageInfo.TargetCharacter?.Name, targetRoom, givingOrderToSelf: orderMessageInfo.TargetCharacter == senderCharacter, orderOption: orderOption, - priority: orderMessageInfo.Priority); + isNewOrder: orderMessageInfo.IsNewOrder); if (GameMain.Client.GameStarted && Screen.Selected == GameMain.GameScreen) { @@ -135,14 +142,18 @@ namespace Barotrauma.Networking //only show the message box if the text differs from the text in the currently visible box if ((GUIMessageBox.VisibleBox as GUIMessageBox)?.Text?.Text != txt) { - new GUIMessageBox("", txt); + GUIMessageBox messageBox = new GUIMessageBox("", txt); + if (textColor != null) { messageBox.Text.TextColor = textColor.Value; } } break; case ChatMessageType.ServerMessageBoxInGame: - new GUIMessageBox("", txt, new string[0], type: GUIMessageBox.Type.InGame, iconStyle: styleSetting); + { + GUIMessageBox messageBox = new GUIMessageBox("", txt, new string[0], type: GUIMessageBox.Type.InGame, iconStyle: styleSetting); + if (textColor != null) { messageBox.Text.TextColor = textColor.Value; } + } break; case ChatMessageType.Console: - DebugConsole.NewMessage(txt, MessageColor[(int)ChatMessageType.Console]); + DebugConsole.NewMessage(txt, textColor == null ? MessageColor[(int)ChatMessageType.Console] : textColor.Value); break; case ChatMessageType.ServerLog: if (!Enum.TryParse(senderName, out ServerLog.MessageType messageType)) @@ -152,7 +163,7 @@ namespace Barotrauma.Networking GameMain.Client.ServerSettings.ServerLog?.WriteLine(txt, messageType); break; default: - GameMain.Client.AddChatMessage(txt, type, senderName, senderClient, senderCharacter, changeType); + GameMain.Client.AddChatMessage(txt, type, senderName, senderClient, senderCharacter, changeType, textColor: textColor); break; } LastID = id; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs index 9ab68965f..86897d268 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs @@ -1,4 +1,5 @@ -using Barotrauma.Networking; +using Barotrauma.Items.Components; +using Barotrauma.Networking; namespace Barotrauma { @@ -15,7 +16,11 @@ namespace Barotrauma var entity = FindEntityByID(entityId); if (entity != null) { - DebugConsole.Log("Received entity removal message for \"" + entity.ToString() + "\"."); + DebugConsole.Log($"Received entity removal message for \"{entity}\"."); + if (entity is Item item && item.Container?.GetComponent() != null) + { + GameAnalyticsManager.AddDesignEvent("ItemDeconstructed:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none") + ":" + item.prefab.Identifier); + } entity.Remove(); } else @@ -28,7 +33,11 @@ namespace Barotrauma switch (message.ReadByte()) { case (byte)SpawnableType.Item: - Item.ReadSpawnData(message, true); + var newItem = Item.ReadSpawnData(message, true); + if (newItem is Item item && item.Container?.GetComponent() != null) + { + GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none") + ":" + item.prefab.Identifier); + } break; case (byte)SpawnableType.Character: Character.ReadSpawnData(message); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index db3882907..c5e1c3f47 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -859,7 +859,7 @@ namespace Barotrauma.Networking } break; case ServerPacketHeader.STARTGAMEFINALIZE: - DebugConsole.Log("Received STARTGAMEFINALIZE packet."); + DebugConsole.NewMessage("Received STARTGAMEFINALIZE packet. Round init status: " + roundInitStatus); if (roundInitStatus == RoundInitStatus.WaitingForStartGameFinalize) { //waiting for a save file @@ -950,6 +950,9 @@ namespace Barotrauma.Networking case ServerPacketHeader.CREW: campaign?.ClientReadCrew(inc); break; + case ServerPacketHeader.MEDICAL: + campaign?.MedicalClinic?.ClientRead(inc); + break; case ServerPacketHeader.READY_CHECK: ReadyCheck.ClientRead(inc); break; @@ -1121,9 +1124,9 @@ namespace Barotrauma.Networking disconnectReason != DisconnectReason.InvalidVersion) { GameAnalyticsManager.AddErrorEventOnce( - "GameClient.HandleDisconnectMessage", - GameAnalyticsManager.ErrorSeverity.Debug, - "Client received a disconnect message. Reason: " + disconnectReason.ToString()); + "GameClient.HandleDisconnectMessage", + GameAnalyticsManager.ErrorSeverity.Debug, + "Client received a disconnect message. Reason: " + disconnectReason.ToString()); } if (disconnectReason == DisconnectReason.ServerFull) @@ -1276,7 +1279,15 @@ namespace Barotrauma.Networking private void ReadAchievement(IReadMessage inc) { string achievementIdentifier = inc.ReadString(); - SteamAchievementManager.UnlockAchievement(achievementIdentifier); + int amount = inc.ReadInt32(); + if (amount == 0) + { + SteamAchievementManager.UnlockAchievement(achievementIdentifier); + } + else + { + SteamAchievementManager.IncrementStat(achievementIdentifier, amount); + } } private void ReadTraitorMessage(IReadMessage inc) @@ -1476,7 +1487,7 @@ namespace Barotrauma.Networking serverSettings.LockAllDefaultWires = inc.ReadBoolean(); serverSettings.AllowRagdollButton = inc.ReadBoolean(); serverSettings.AllowLinkingWifiToChat = inc.ReadBoolean(); - GameMain.NetLobbyScreen.UsingShuttle = inc.ReadBoolean(); + bool usingShuttle = GameMain.NetLobbyScreen.UsingShuttle = inc.ReadBoolean(); GameMain.LightManager.LosMode = (LosMode)inc.ReadByte(); bool includesFinalize = inc.ReadBoolean(); inc.ReadPadBits(); GameMain.LightManager.LightingEnabled = true; @@ -1488,6 +1499,8 @@ namespace Barotrauma.Networking Task loadTask = null; var roundSummary = (GUIMessageBox.MessageBoxes.Find(c => c?.UserData is RoundSummary)?.UserData) as RoundSummary; + bool isOutpost = false; + if (gameMode != GameModePreset.MultiPlayerCampaign) { string levelSeed = inc.ReadString(); @@ -1626,6 +1639,7 @@ namespace Barotrauma.Networking { GameMain.GameSession.StartRound(levelData, mirrorLevel); } + isOutpost = levelData.Type == LevelData.LevelType.Outpost; } if (GameMain.Client?.ServerSettings?.Voting != null) @@ -1745,8 +1759,7 @@ namespace Barotrauma.Networking if (respawnAllowed) { - bool isOutpost = GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign && Level.Loaded?.Type == LevelData.LevelType.Outpost; - respawnManager = new RespawnManager(this, GameMain.NetLobbyScreen.UsingShuttle && !isOutpost ? GameMain.NetLobbyScreen.SelectedShuttle : null); + respawnManager = new RespawnManager(this, usingShuttle && !isOutpost ? GameMain.NetLobbyScreen.SelectedShuttle : null); } gameStarted = true; @@ -1877,7 +1890,7 @@ namespace Barotrauma.Networking if (int.TryParse(ownedIndexes[i], out int index)) { SubmarineInfo sub = GameMain.Client.ServerSubmarines[index]; - if (GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, "owned")) + if (GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, NetLobbyScreen.SubmarineDeliveryData.Owned)) { GameMain.GameSession.OwnedSubmarines.Add(sub); } @@ -1893,7 +1906,7 @@ namespace Barotrauma.Networking if (int.TryParse(ownedIndexes[i], out index)) { SubmarineInfo sub = GameMain.Client.ServerSubmarines[index]; - if (GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, "owned")) + if (GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, NetLobbyScreen.SubmarineDeliveryData.Owned)) { GameMain.NetLobbyScreen.ServerOwnedSubmarines.Add(sub); } @@ -2095,13 +2108,6 @@ namespace Barotrauma.Networking string selectShuttleName = inc.ReadString(); string selectShuttleHash = inc.ReadString(); - UInt16 campaignSubmarineIndexCount = inc.ReadUInt16(); - List campaignSubIndices = new List(); - for (int i = 0; i< campaignSubmarineIndexCount; i++) - { - campaignSubIndices.Add(inc.ReadUInt16()); - } - bool allowSubVoting = inc.ReadBoolean(); bool allowModeVoting = inc.ReadBoolean(); @@ -2162,16 +2168,11 @@ namespace Barotrauma.Networking if (GameMain.Client.IsServerOwner) RequestSelectMode(modeIndex); } - if (campaignSubIndices != null) + if (GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign) { - GameMain.NetLobbyScreen.CampaignSubmarines = new List(); - foreach (UInt16 campaignSubIndex in campaignSubIndices) + foreach (SubmarineInfo sub in ServerSubmarines.Where(s => !ServerSettings.HiddenSubs.Contains(s.Name))) { - SubmarineInfo sub = GameMain.Client.ServerSubmarines[campaignSubIndex]; - if (GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, "campaign")) - { - GameMain.NetLobbyScreen.CampaignSubmarines.Add(sub); - } + GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, NetLobbyScreen.SubmarineDeliveryData.Campaign); } } @@ -2604,7 +2605,6 @@ namespace Barotrauma.Networking NetLobbyScreen.FailedSubInfo failedCampaignSub = GameMain.NetLobbyScreen.FailedCampaignSubs.Find(s => s.Name == newSub.Name && s.Hash == newSub.MD5Hash.Hash); if (failedCampaignSub != default) { - GameMain.NetLobbyScreen.CampaignSubmarines.Add(newSub); GameMain.NetLobbyScreen.FailedCampaignSubs.Remove(failedCampaignSub); } @@ -2881,19 +2881,16 @@ namespace Barotrauma.Networking public void SendCampaignState() { - MultiPlayerCampaign campaign = GameMain.GameSession.GameMode as MultiPlayerCampaign; - if (campaign == null) + if (!(GameMain.GameSession.GameMode is MultiPlayerCampaign campaign)) { DebugConsole.ThrowError("Failed send campaign state to the server (no campaign active).\n" + Environment.StackTrace.CleanupStackTrace()); return; } - IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ClientPacketHeader.SERVER_COMMAND); msg.Write((UInt16)ClientPermissions.ManageCampaign); campaign.ClientWrite(msg); msg.Write((byte)ServerNetObject.END_OF_MESSAGE); - clientPeer.Send(msg, DeliveryMethod.Reliable); } @@ -3571,6 +3568,11 @@ namespace Barotrauma.Networking case ClientNetError.MISSING_ENTITY: outMsg.Write(eventID); outMsg.Write(entityID); + outMsg.Write((byte)Submarine.Loaded.Count); + foreach (Submarine sub in Submarine.Loaded) + { + outMsg.Write(sub.Info.Name); + } break; } clientPeer.Send(outMsg, DeliveryMethod.Reliable); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs index 70e9b0728..4100bb358 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs @@ -100,7 +100,7 @@ namespace Barotrauma.Networking if (!isActive) { return; } if (steamId != hostSteamId) { return; } Close($"SteamP2P connection failed: {error}"); - OnDisconnectMessageReceived?.Invoke($"SteamP2P connection failed: {error}"); + OnDisconnectMessageReceived?.Invoke($"{DisconnectReason.SteamP2PError}/SteamP2P connection failed: {error}"); } private void OnP2PData(ulong steamId, byte[] data, int dataLength) @@ -167,14 +167,14 @@ namespace Barotrauma.Networking if (state == null) { Close("SteamP2P connection could not be established"); - OnDisconnectMessageReceived?.Invoke("SteamP2P connection could not be established"); + OnDisconnectMessageReceived?.Invoke(DisconnectReason.SteamP2PError.ToString()); } else { if (state?.P2PSessionError != Steamworks.P2PSessionError.None) { Close($"SteamP2P error code: {state?.P2PSessionError}"); - OnDisconnectMessageReceived?.Invoke($"SteamP2P error code: {state?.P2PSessionError}"); + OnDisconnectMessageReceived?.Invoke($"{DisconnectReason.SteamP2PError}/SteamP2P error code: {state?.P2PSessionError}"); } } connectionStatusTimer = 1.0f; @@ -210,7 +210,7 @@ namespace Barotrauma.Networking if (timeout < 0.0) { Close("Timed out"); - OnDisconnectMessageReceived?.Invoke(""); + OnDisconnectMessageReceived?.Invoke(DisconnectReason.SteamP2PTimeOut.ToString()); return; } @@ -349,13 +349,19 @@ namespace Barotrauma.Networking outMsg.Write((byte)PacketHeader.IsDisconnectMessage); outMsg.Write(msg ?? "Disconnected"); - Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable); - sentBytes += outMsg.LengthBytes; + try + { + Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable); + sentBytes += outMsg.LengthBytes; + } + catch (Exception e) + { + DebugConsole.ThrowError("Failed to send a disconnect message to the server using SteamP2P.", e); + } Thread.Sleep(100); Steamworks.SteamNetworking.ResetActions(); - Steamworks.SteamNetworking.CloseP2PSessionWithUser(hostSteamId); steamAuthTicket?.Cancel(); steamAuthTicket = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs index 2b25e89a2..f0abd892a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs @@ -81,16 +81,15 @@ namespace Barotrauma.Networking public bool ContentPackagesMatch() { - var myContentPackages = ContentPackage.AllPackages; //make sure we have all the packages the server requires if (ContentPackageHashes.Count != ContentPackageWorkshopIds.Count) { return false; } for (int i = 0; i < ContentPackageWorkshopIds.Count; i++) { string hash = ContentPackageHashes[i]; UInt64 id = ContentPackageWorkshopIds[i]; - if (!myContentPackages.Any(myPackage => myPackage.MD5hash.Hash == hash)) + if (!GameMain.ServerListScreen.ContentPackagesByHash.ContainsKey(hash)) { - if (myContentPackages.Any(p => p.SteamWorkshopId == id)) { return false; } + if (GameMain.ServerListScreen.ContentPackagesByWorkshopId.ContainsKey(id)) { return false; } if (id == 0) { return false; } } } @@ -98,12 +97,6 @@ namespace Barotrauma.Networking return true; } - public bool ContentPackagesMatch(IEnumerable myContentPackageHashes) - { - HashSet contentPackageHashes = new HashSet(ContentPackageHashes); - return contentPackageHashes.SetEquals(myContentPackageHashes); - } - public void CreatePreviewWindow(GUIFrame frame) { if (frame == null) { return; } @@ -428,7 +421,7 @@ namespace Barotrauma.Networking return; } - var rules = ((Task>)t).Result; + t.TryGetResult(out Dictionary rules); SteamManager.AssignServerRulesToServerInfo(rules, this); onServerRulesReceived(this); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index d345d2a98..783d52326 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -94,10 +94,10 @@ namespace Barotrauma.Networking public void ClientAdminRead(IReadMessage incMsg) { - int count = incMsg.ReadUInt16(); - for (int i = 0; i < count; i++) + while (true) { UInt32 key = incMsg.ReadUInt32(); + if (key == 0) { break; } if (netProperties.ContainsKey(key)) { bool changedLocally = netProperties[key].ChangedLocally; @@ -153,8 +153,11 @@ namespace Barotrauma.Networking { ReadExtraCargo(incMsg); } - - ReadHiddenSubs(incMsg); + + if (requiredFlags.HasFlag(NetFlags.HiddenSubs)) + { + ReadHiddenSubs(incMsg); + } GameMain.NetLobbyScreen.UpdateSubVisibility(); bool isAdmin = incMsg.ReadBoolean(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs index 29295f520..a72a282f0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs @@ -143,8 +143,7 @@ namespace Barotrauma.Steam return; } - currentLobby = ((Task)lobby).Result; - + lobby.TryGetResult(out currentLobby); if (currentLobby == null) { DebugConsole.ThrowError("Failed to create Steam lobby: returned lobby was null"); @@ -250,7 +249,7 @@ namespace Barotrauma.Steam TaskPool.Add("JoinLobbyAsync", Steamworks.SteamMatchmaking.JoinLobbyAsync(lobbyID), (lobby) => { - currentLobby = ((Task)lobby).Result; + lobby.TryGetResult(out currentLobby); lobbyState = LobbyState.Joined; lobbyID = (currentLobby?.Id).Value; if (joinServer) @@ -293,10 +292,11 @@ namespace Barotrauma.Steam taskDone(); return; } - var lobbies = ((Task>)t).Result; - if (lobbies != null) + t.TryGetResult(out List lobbies); + IEnumerable lobbyAddCoroutine() { - foreach (var lobby in lobbies) + int i = 0; + foreach (var lobby in lobbies ?? Enumerable.Empty()) { if (string.IsNullOrEmpty(lobby.GetData("name"))) { continue; } @@ -312,9 +312,13 @@ namespace Barotrauma.Steam AssignLobbyDataToServerInfo(lobby, serverInfo); addToServerList(serverInfo); + i++; + if (i >= 16) { yield return CoroutineStatus.Running; i = 0; } } + taskDone(); + yield return CoroutineStatus.Success; } - taskDone(); + CoroutineManager.StartCoroutine(lobbyAddCoroutine()); }); Steamworks.ServerList.Internet serverQuery = new Steamworks.ServerList.Internet(); @@ -344,13 +348,10 @@ namespace Barotrauma.Steam return; } - var rules = ((Task>)t).Result; + t.TryGetResult(out Dictionary rules); AssignServerRulesToServerInfo(rules, serverInfo); - CrossThread.RequestExecutionOnMainThread(() => - { - addToServerList(serverInfo); - }); + addToServerList(serverInfo); }); } else @@ -618,7 +619,10 @@ namespace Barotrauma.Steam .WithLongDescription(); if (requireTags != null) { query = query.WithTags(requireTags); } - TaskPool.Add("GetSubscribedWorkshopItems", GetWorkshopItemsAsync(query), (task) => { onItemsFound?.Invoke(((Task>)task).Result); }); + TaskPool.Add("GetSubscribedWorkshopItems", GetWorkshopItemsAsync(query), (task) => + { + task.TryGetResult(out List result); onItemsFound?.Invoke(result); + }); } public static void GetPopularWorkshopItems(Action> onItemsFound, int amount, List requireTags = null) @@ -632,7 +636,7 @@ namespace Barotrauma.Steam TaskPool.Add("GetPopularWorkshopItems", GetWorkshopItemsAsync(query, amount, (item) => !item.IsSubscribed), (task) => { - var entries = ((Task>)task).Result; + task.TryGetResult(out List entries); //count the number of each unique tag foreach (var item in entries) @@ -677,7 +681,10 @@ namespace Barotrauma.Steam .WithLongDescription(); if (requireTags != null) query.WithTags(requireTags); - TaskPool.Add("GetPublishedWorkshopItems", GetWorkshopItemsAsync(query), (task) => { onItemsFound?.Invoke(((Task>)task).Result); }); + TaskPool.Add("GetPublishedWorkshopItems", GetWorkshopItemsAsync(query), (task) => + { + task.TryGetResult(out List result); onItemsFound?.Invoke(result); + }); } private static readonly HashSet pendingWorkshopSubscriptions = new HashSet(); @@ -724,7 +731,7 @@ namespace Barotrauma.Steam } else { - var item = ((Task)t).Result; + t.TryGetResult(out Steamworks.Ugc.Item? item); if (item != null) { if (item?.IsInstalled ?? false) @@ -1077,7 +1084,7 @@ namespace Barotrauma.Steam GameMain.SteamWorkshopScreen?.SetReinstallButtonStatus(item, true, GUI.Style.Red); return; } - string errorMsg = ((Task)task).Result; + task.TryGetResult(out string errorMsg); if (!string.IsNullOrWhiteSpace(errorMsg)) { DebugConsole.ThrowError($"Failed to copy \"{item.Title}\": {errorMsg}"); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs index 441f71846..97099d85f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs @@ -196,6 +196,19 @@ namespace Barotrauma.Particles particles[particleCount] = swap; } + + public void RemoveParticle(Particle particle) + { + for (int i = 0; i < particleCount; i++) + { + if (particles[i] == particle) + { + RemoveParticle(i); + return; + } + } + } + public void Update(float deltaTime) { MaxParticles = GameMain.Config.ParticleLimit; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs index afe8fb55a..2bc4bee2b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs @@ -1,13 +1,10 @@ -using Barotrauma.Tutorials; +using Barotrauma.Extensions; +using Barotrauma.IO; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using Barotrauma.IO; -using System.Linq; -using System.Xml.Linq; using System.Globalization; -using Barotrauma.Extensions; -using Barotrauma.Networking; +using System.Linq; namespace Barotrauma { @@ -64,6 +61,7 @@ namespace Barotrauma maxMissionCount = MathHelper.Clamp(maxMissionCount, CampaignSettings.MinMissionCountLimit, CampaignSettings.MaxMissionCountLimit); + maxMissionCountText.Text = maxMissionCount.ToString(CultureInfo.InvariantCulture); } maxMissionCountButtons[1] @@ -246,6 +244,12 @@ namespace Barotrauma foreach (string saveFile in saveFiles) { + if (string.IsNullOrEmpty(saveFile)) + { + DebugConsole.AddWarning("Error when updating campaign load menu: path to a save file was empty.\n" + Environment.StackTrace); + continue; + } + string fileName = saveFile; string subName = ""; string saveTime = ""; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index 27efd044d..e8bf31049 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -45,6 +45,8 @@ namespace Barotrauma public UpgradeStore UpgradeStore { get; set; } + public MedicalClinicUI MedicalClinic { get; set; } + public CampaignUI(CampaignMode campaign, GUIComponent container) { Campaign = campaign; @@ -149,6 +151,7 @@ namespace Barotrauma if (Campaign.Money >= CampaignMode.HullRepairCost) { Campaign.Money -= CampaignMode.HullRepairCost; + GameAnalyticsManager.AddMoneySpentEvent(CampaignMode.HullRepairCost, GameAnalyticsManager.MoneySink.Service, "hullrepairs"); Campaign.PurchasedHullRepairs = true; } } @@ -194,6 +197,7 @@ namespace Barotrauma if (Campaign.Money >= CampaignMode.ItemRepairCost) { Campaign.Money -= CampaignMode.ItemRepairCost; + GameAnalyticsManager.AddMoneySpentEvent(CampaignMode.ItemRepairCost, GameAnalyticsManager.MoneySink.Service, "devicerepairs"); Campaign.PurchasedItemRepairs = true; } } @@ -246,6 +250,7 @@ namespace Barotrauma if (Campaign.Money >= CampaignMode.ShuttleReplaceCost) { Campaign.Money -= CampaignMode.ShuttleReplaceCost; + GameAnalyticsManager.AddMoneySpentEvent(CampaignMode.ShuttleReplaceCost, GameAnalyticsManager.MoneySink.Service, "retrieveshuttle"); Campaign.PurchasedLostShuttles = true; } } @@ -270,6 +275,9 @@ namespace Barotrauma // Submarine buying tab tabs[(int)CampaignMode.InteractionType.PurchaseSub] = new GUIFrame(new RectTransform(Vector2.One, container.RectTransform, Anchor.TopLeft), color: Color.Black * 0.9f); + tabs[(int)CampaignMode.InteractionType.MedicalClinic] = new GUIFrame(new RectTransform(Vector2.One, container.RectTransform), color: Color.Black * 0.9f); + MedicalClinic = new MedicalClinicUI(Campaign.MedicalClinic, GetTabContainer(CampaignMode.InteractionType.MedicalClinic)); + // mission info ------------------------------------------------------------------------- locationInfoPanel = new GUIFrame(new RectTransform(new Vector2(0.35f, 0.75f), GetTabContainer(CampaignMode.InteractionType.Map).RectTransform, Anchor.CenterRight) @@ -355,6 +363,10 @@ namespace Barotrauma case CampaignMode.InteractionType.Store: Store?.Update(deltaTime); break; + + case CampaignMode.InteractionType.MedicalClinic: + MedicalClinic?.Update(deltaTime); + break; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index 32e05dfa4..41b8e927c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -251,7 +251,7 @@ namespace Barotrauma.CharacterEditor GUI.ForceMouseOn(null); if (isEndlessRunner) { - Submarine.MainSub.Remove(); + Submarine.MainSub?.Remove(); GameMain.World.ProcessChanges(); isEndlessRunner = false; Reset(); @@ -500,29 +500,34 @@ namespace Barotrauma.CharacterEditor int index = 0; bool isSwimming = character.AnimController.ForceSelectAnimationType == AnimationType.SwimFast || character.AnimController.ForceSelectAnimationType == AnimationType.SwimSlow; bool isMovingFast = character.AnimController.ForceSelectAnimationType == AnimationType.Run || character.AnimController.ForceSelectAnimationType == AnimationType.SwimFast; - if (isMovingFast) + if (character.AnimController.CanWalk) { - if (isSwimming || !character.AnimController.CanWalk) + if (isMovingFast) { - index = !character.AnimController.CanWalk ? (int)AnimationType.SwimFast : (int)AnimationType.SwimSlow; + if (isSwimming) + { + index = 2; + } + else + { + index = 0; + } } else { - index = (int)AnimationType.Walk; + if (isSwimming) + { + index = 3; + } + else + { + index = 1; + } } - index -= 1; } else { - if (isSwimming || !character.AnimController.CanWalk) - { - index = !character.AnimController.CanWalk ? (int)AnimationType.SwimSlow : (int)AnimationType.SwimFast; - } - else - { - index = (int)AnimationType.Run; - } - index -= 1; + index = isMovingFast ? 0 : 1; } if (animSelection.SelectedIndex != index) { @@ -536,17 +541,13 @@ namespace Barotrauma.CharacterEditor bool isSwimming = character.AnimController.ForceSelectAnimationType == AnimationType.SwimFast || character.AnimController.ForceSelectAnimationType == AnimationType.SwimSlow; if (isSwimming) { - animSelection.Select((int)AnimationType.Walk - 1); + animSelection.Select(0); } else { - animSelection.Select((int)AnimationType.SwimSlow - 1); + animSelection.Select(2); } } - if (PlayerInput.KeyHit(Keys.F)) - { - SetToggle(freezeToggle, !freezeToggle.Selected); - } if (PlayerInput.SecondaryMouseButtonClicked() || PlayerInput.KeyHit(Keys.Escape)) { bool reset = false; @@ -853,6 +854,16 @@ namespace Barotrauma.CharacterEditor { DrawRagdoll(spriteBatch, (float)deltaTime); } + // Mouth + Limb head = character.AnimController.GetLimb(LimbType.Head); + if (head != null && character.CanEat && selectedLimbs.Contains(head)) + { + var mouthPos = character.AnimController.GetMouthPosition(); + if (mouthPos.HasValue) + { + ShapeExtensions.DrawPoint(spriteBatch, SimToScreen(mouthPos.Value), GUI.Style.Red, size: 8); + } + } if (showSpritesheet) { DrawSpritesheetEditor(spriteBatch, (float)deltaTime); @@ -2606,13 +2617,13 @@ namespace Barotrauma.CharacterEditor { animSelection.AddItem(AnimationType.Walk.ToString(), AnimationType.Walk); animSelection.AddItem(AnimationType.Run.ToString(), AnimationType.Run); - if (character.IsHumanoid) - { - animSelection.AddItem(AnimationType.Crouch.ToString(), AnimationType.Crouch); - } } animSelection.AddItem(AnimationType.SwimSlow.ToString(), AnimationType.SwimSlow); animSelection.AddItem(AnimationType.SwimFast.ToString(), AnimationType.SwimFast); + if (character.AnimController.CanWalk && character.IsHumanoid) + { + animSelection.AddItem(AnimationType.Crouch.ToString(), AnimationType.Crouch); + } if (character.AnimController.ForceSelectAnimationType == AnimationType.NotDefined) { animSelection.SelectItem(character.AnimController.CanWalk ? AnimationType.Walk : AnimationType.SwimSlow); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs index d85bf42ee..bc639deb0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs @@ -54,7 +54,7 @@ namespace Barotrauma private void CreateGUI() { - GuiFrame = new GUIFrame(new RectTransform(new Vector2(0.2f, 0.4f), GUICanvas.Instance) { MinSize = new Point(300, 400) }); + GuiFrame = new GUIFrame(new RectTransform(new Vector2(0.2f, 0.4f), GUI.Canvas) { MinSize = new Point(300, 400) }); GUILayoutGroup layoutGroup = new GUILayoutGroup(RectTransform(0.9f, 0.9f, GuiFrame, Anchor.Center)) { Stretch = true }; // === BUTTONS === // @@ -247,16 +247,6 @@ namespace Barotrauma return msgBox; } - private void NotifyPrompt(string header, string body) - { - GUIMessageBox msgBox = new GUIMessageBox(header, body, new[] { TextManager.Get("Ok") }, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175)); - msgBox.Buttons[0].OnClicked = delegate - { - msgBox.Close(); - return true; - }; - } - private bool SaveProjectToFile(GUIButton button, object o) { string directory = Path.GetFullPath("EventProjects"); @@ -315,7 +305,7 @@ namespace Barotrauma CreateNodes(prefab.ConfigElement, ref hadNodes); if (!hadNodes) { - NotifyPrompt(TextManager.Get("EventEditor.RandomGenerationHeader"), TextManager.Get("EventEditor.RandomGenerationBody")); + GUI.NotifyPrompt(TextManager.Get("EventEditor.RandomGenerationHeader"), TextManager.Get("EventEditor.RandomGenerationBody")); } return true; }); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index 053376261..c4f519f40 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -115,7 +115,7 @@ namespace Barotrauma #if TEST_REMOTE_CONTENT - var doc = XMLExtensions.TryLoadXml("Content/UI/MenuTextTest.xml"); + var doc = XMLExtensions.TryLoadXml("Content/UI/MenuContent.xml"); if (doc?.Root != null) { foreach (XElement subElement in doc?.Root.Elements()) @@ -564,8 +564,6 @@ namespace Barotrauma ResetButtonStates(null); - GameAnalyticsManager.SetCustomDimension01(""); - if (GameMain.SteamWorkshopScreen != null) { CoroutineManager.StartCoroutine(GameMain.SteamWorkshopScreen.RefreshDownloadState()); @@ -782,7 +780,7 @@ namespace Barotrauma } #endregion - public void QuickStart(bool fixedSeed = false, string sub = null, float difficulty = 40, LevelGenerationParams levelGenerationParams = null) + public void QuickStart(bool fixedSeed = false, string sub = null, float difficulty = 50, LevelGenerationParams levelGenerationParams = null) { if (fixedSeed) { @@ -1082,7 +1080,10 @@ namespace Barotrauma if (backgroundSprite == null) { - backgroundSprite = (LocationType.List.Where(l => l.UseInMainMenu).GetRandom())?.GetPortrait(0); +#if UNSTABLE + backgroundSprite = new Sprite("Content/UnstableBackground.png", sourceRectangle: null); +#endif + backgroundSprite ??= LocationType.List.Where(l => l.UseInMainMenu).GetRandom()?.GetPortrait(0); } if (backgroundSprite != null) @@ -1226,7 +1227,7 @@ namespace Barotrauma GameMain.Lua.Initialize(); } - #region UI Methods +#region UI Methods private void CreateCampaignSetupUI() { menuTabs[(int)Tab.NewGame].ClearChildren(); @@ -1462,7 +1463,7 @@ namespace Barotrauma return false; } - if (ForbiddenWordFilter.IsForbidden(name, out string forbiddenWord)) + if (isPublicBox.Selected && ForbiddenWordFilter.IsForbidden(name, out string forbiddenWord)) { var msgBox = new GUIMessageBox("", TextManager.GetWithVariables("forbiddenservernameverification", new string[] { "[forbiddenword]", "[servername]" }, new string[] { forbiddenWord, name }), @@ -1499,7 +1500,7 @@ namespace Barotrauma playstyleDescription.TextAlignment = playstyleDescription.WrappedText.Contains('\n') ? Alignment.CenterLeft : Alignment.Center; } - #endregion +#endregion private void FetchRemoteContent() { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 794643a21..8d80957a6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -1,20 +1,16 @@ using Barotrauma.Extensions; using Barotrauma.Networking; +using Barotrauma.Steam; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; -using Barotrauma.IO; using System.Linq; -using System.Xml.Linq; -using Barotrauma.Steam; namespace Barotrauma { partial class NetLobbyScreen : Screen { - //private readonly List jobPreferenceSprites = new List(); - private readonly GUIFrame infoFrame, modeFrame; private readonly GUILayoutGroup infoFrameContent; private readonly GUIFrame myCharacterFrame; @@ -221,7 +217,6 @@ namespace Barotrauma public SubmarineInfo SelectedShuttle => ShuttleList.SelectedData as SubmarineInfo; public MultiPlayerCampaignSetupUI CampaignSetupUI; - public List CampaignSubmarines = new List(); // Passed onto the gamesession when created public List ServerOwnedSubmarines = new List(); @@ -611,6 +606,7 @@ namespace Barotrauma { OnClicked = (btn, obj) => { + if (GameMain.Client == null) { return true; } GameMain.Client.RequestStartRound(); CoroutineManager.StartCoroutine(WaitForStartRound(StartButton), "WaitForStartRound"); return true; @@ -628,7 +624,7 @@ namespace Barotrauma { OnSelected = (tickBox) => { - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, autoRestart: tickBox.Selected); + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, autoRestart: tickBox.Selected); return true; } }; @@ -655,6 +651,7 @@ namespace Barotrauma }; ServerName.OnDeselected += (textBox, key) => { + if (GameMain.Client == null) { return; } if (!textBox.Readonly) { GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Name); @@ -669,6 +666,7 @@ namespace Barotrauma ToolTip = TextManager.Get("addtofavorites"), OnSelected = (tickbox) => { + if (GameMain.Client == null) { return true; } ServerInfo info = GameMain.Client.ServerSettings.GetServerListInfo(); if (tickbox.Selected) { @@ -766,6 +764,7 @@ namespace Barotrauma }; ServerMessage.OnDeselected += (textBox, key) => { + if (GameMain.Client == null) { return; } if (!textBox.Readonly) { GameMain.Client?.ServerSettings?.ClientAdminWrite(ServerSettings.NetFlags.Message); @@ -849,7 +848,7 @@ namespace Barotrauma Selected = true, OnSelected = (GUITickBox box) => { - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, useRespawnShuttle: box.Selected); + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, useRespawnShuttle: box.Selected); return true; } }; @@ -868,7 +867,7 @@ namespace Barotrauma { OnSelected = (component, obj) => { - GameMain.Client.RequestSelectSub(component.Parent.GetChildIndex(component), isShuttle: true); + GameMain.Client?.RequestSelectSub(component.Parent.GetChildIndex(component), isShuttle: true); return true; } }; @@ -970,7 +969,7 @@ namespace Barotrauma { OnClicked = (_, __) => { - GameMain.Client.RequestSelectMode(ModeList.Content.GetChildIndex(ModeList.Content.GetChildByUserData(GameModePreset.Sandbox))); + GameMain.Client?.RequestSelectMode(ModeList.Content.GetChildIndex(ModeList.Content.GetChildByUserData(GameModePreset.Sandbox))); return true; } }; @@ -1026,7 +1025,7 @@ namespace Barotrauma { int missionTypeOr = tickbox.Selected ? (int)tickbox.UserData : (int)MissionType.None; int missionTypeAnd = (int)MissionType.All & (!tickbox.Selected ? (~(int)tickbox.UserData) : (int)MissionType.All); - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, (int)missionTypeOr, (int)missionTypeAnd); + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, (int)missionTypeOr, (int)missionTypeAnd); return true; } }; @@ -1059,7 +1058,7 @@ namespace Barotrauma SeedBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), seedLabel.RectTransform, Anchor.CenterRight)); SeedBox.OnDeselected += (textBox, key) => { - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.LevelSeed); + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.LevelSeed); }; clientDisabledElements.Add(SeedBox); LevelSeed = ToolBox.RandomSeed(8); @@ -1080,7 +1079,7 @@ namespace Barotrauma ToolTip = TextManager.Get("leveldifficultyexplanation"), OnReleased = (scrollbar, value) => { - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, levelDifficulty: scrollbar.BarScrollValue); + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, levelDifficulty: scrollbar.BarScrollValue); return true; } }; @@ -1112,8 +1111,7 @@ namespace Barotrauma { OnClicked = (button, obj) => { - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, traitorSetting: -1); - + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, traitorSetting: -1); return true; } }; @@ -1124,8 +1122,7 @@ namespace Barotrauma { OnClicked = (button, obj) => { - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, traitorSetting: 1); - + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, traitorSetting: 1); return true; } }; @@ -1143,7 +1140,7 @@ namespace Barotrauma { OnClicked = (button, obj) => { - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, botCount: -1); + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, botCount: -1); return true; } }; @@ -1153,7 +1150,7 @@ namespace Barotrauma { OnClicked = (button, obj) => { - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, botCount: 1); + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, botCount: 1); return true; } }; @@ -1169,7 +1166,7 @@ namespace Barotrauma { OnClicked = (button, obj) => { - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, botSpawnMode: -1); + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, botSpawnMode: -1); return true; } }; @@ -1179,7 +1176,7 @@ namespace Barotrauma { OnClicked = (button, obj) => { - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, botSpawnMode: 1); + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, botSpawnMode: 1); return true; } }; @@ -1345,9 +1342,9 @@ namespace Barotrauma StartButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ManageRound) && !GameMain.Client.GameStarted && !CampaignSetupFrame.Visible && !CampaignFrame.Visible; ServerName.Readonly = !GameMain.Client.HasPermission(ClientPermissions.ManageSettings); ServerMessage.Readonly = !GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - shuttleTickBox.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + shuttleTickBox.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings) && !GameMain.Client.GameStarted; SubList.Enabled = !CampaignFrame.Visible && (GameMain.Client.ServerSettings.Voting.AllowSubVoting || GameMain.Client.HasPermission(ClientPermissions.SelectSub)); - ShuttleList.Enabled = ShuttleList.ButtonEnabled = GameMain.Client.HasPermission(ClientPermissions.SelectSub); + ShuttleList.Enabled = ShuttleList.ButtonEnabled = GameMain.Client.HasPermission(ClientPermissions.SelectSub) && !GameMain.Client.GameStarted; ModeList.Enabled = GameMain.Client.ServerSettings.Voting.AllowModeVoting || GameMain.Client.HasPermission(ClientPermissions.SelectMode); LogButtons.Visible = GameMain.Client.HasPermission(ClientPermissions.ServerLog); GameMain.Client.ShowLogButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ServerLog); @@ -3576,7 +3573,13 @@ namespace Barotrauma return false; } - public bool CheckIfCampaignSubMatches(SubmarineInfo serverSubmarine, string deliveryData) + public enum SubmarineDeliveryData + { + Owned, + Campaign + } + + public bool CheckIfCampaignSubMatches(SubmarineInfo serverSubmarine, SubmarineDeliveryData deliveryData) { if (GameMain.Client == null) return false; @@ -3630,11 +3633,11 @@ namespace Barotrauma { FailedSubInfo fileInfo = (FailedSubInfo)userdata; - if (deliveryData == "owned") //owned!!!! + if (deliveryData == SubmarineDeliveryData.Owned) { FailedOwnedSubs.Add(fileInfo); } - else if (deliveryData == "campaign") + else if (deliveryData == SubmarineDeliveryData.Campaign) { FailedCampaignSubs.Add(fileInfo); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs index 6d2513216..3093092c7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs @@ -14,7 +14,7 @@ namespace Barotrauma { if (frame == null) { - frame = new GUIFrame(new RectTransform(GUICanvas.Instance.RelativeSize, GUICanvas.Instance), style: null) + frame = new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas), style: null) { CanBeFocused = false }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs index 9c004b7e3..461a8ba29 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs @@ -7,6 +7,7 @@ using Microsoft.Xna.Framework.Graphics; using RestSharp; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Net; using System.Net.NetworkInformation; @@ -21,6 +22,11 @@ namespace Barotrauma //how often the client is allowed to refresh servers private readonly TimeSpan AllowedRefreshInterval = new TimeSpan(0, 0, 3); + public ImmutableDictionary ContentPackagesByWorkshopId { get; private set; } + = ImmutableDictionary.Empty; + public ImmutableDictionary ContentPackagesByHash { get; private set; } + = ImmutableDictionary.Empty; + private GUIFrame menu; private GUIListBox serverList; @@ -1011,6 +1017,17 @@ namespace Barotrauma public override void Select() { base.Select(); + + ContentPackagesByWorkshopId = ContentPackage.AllPackages + .Select(p => new KeyValuePair(p.SteamWorkshopId, p)) + .Where(p => p.Key != 0) + .GroupBy(x => x.Key).Select(g => g.First()) + .ToImmutableDictionary(); + ContentPackagesByHash = ContentPackage.AllPackages + .Select(p => new KeyValuePair(p.MD5hash.Hash, p)) + .GroupBy(x => x.Key).Select(g => g.First()) + .ToImmutableDictionary(); + SelectedTab = ServerListTab.All; LoadServerFilters(GameMain.Config.ServerFilterElement); if (GameSettings.ShowOffensiveServerPrompt) @@ -1039,6 +1056,8 @@ namespace Barotrauma public override void Deselect() { + ContentPackagesByWorkshopId = ImmutableDictionary.Empty; + ContentPackagesByHash = ImmutableDictionary.Empty; base.Deselect(); GameMain.Config.SaveNewPlayerConfig(); @@ -1491,7 +1510,7 @@ namespace Barotrauma } TaskPool.Add($"Get{avatarSize}AvatarAsync", avatarFunc(friend.Id), (task) => { - Steamworks.Data.Image? img = ((Task)task).Result; + if (!task.TryGetResult(out Steamworks.Data.Image? img)) { return; } if (!img.HasValue) { return; } var avatarImage = img.Value; @@ -2203,7 +2222,7 @@ namespace Barotrauma TaskPool.PrintTaskExceptions(t, $"Failed to retrieve Workshop item info (ID {entry.Id})"); return; } - Steamworks.Ugc.Item? item = ((Task)t).Result; + t.TryGetResult(out Steamworks.Ugc.Item? item); if (!item.HasValue) { @@ -2313,7 +2332,7 @@ namespace Barotrauma { var info = obj.Item1; var text = obj.Item2; - info.Ping = ((Task)rtt).Result; info.PingChecked = true; + rtt.TryGetResult(out info.Ping); info.PingChecked = true; text.TextColor = GetPingTextColor(info.Ping); text.Text = info.Ping > -1 ? info.Ping.ToString() : "?"; lock (activePings) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs index 94064f687..60cdf08a0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs @@ -854,7 +854,7 @@ namespace Barotrauma (var it, var lb) = tuple; if (lb.Content.FindChild(item)?.GetChildByUserData("previewimage") is GUIImage previewImage) { - previewImage.Sprite = ((Task)task).Result; + if (task.TryGetResult(out Sprite sprite)) { previewImage.Sprite = sprite; } } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 1550a1afa..6b6b947dc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Xml.Linq; @@ -21,6 +22,38 @@ namespace Barotrauma { class SubEditorScreen : EditorScreen { + private enum LayerVisibility + { + Visible, + Invisible + } + + private enum LayerLinkage + { + Unlinked, + Linked + } + + private readonly struct LayerData + { + public readonly LayerVisibility Visible; + public readonly LayerLinkage Linkage; + + public static readonly LayerData Default = new LayerData(LayerVisibility.Visible, LayerLinkage.Unlinked); + + public LayerData(LayerVisibility visible, LayerLinkage linkage) + { + Visible = visible; + Linkage = linkage; + } + + public void Deconstruct(out LayerVisibility isvisible, out LayerLinkage islinked) + { + isvisible = Visible; + islinked = Linkage; + } + } + private static readonly string[] crewExperienceLevels = { "CrewExperienceLow", @@ -94,11 +127,16 @@ namespace Barotrauma private GUIFrame hullVolumeFrame; private GUIFrame saveAssemblyFrame; + private GUIFrame snapToGridFrame; const int PreviouslyUsedCount = 10; private GUIFrame previouslyUsedPanel; private GUIListBox previouslyUsedList; + private GUIButton visibilityButton; + private GUIFrame layerPanel; + private GUIListBox layerList; + private GUIFrame undoBufferPanel; private GUIFrame undoBufferDisclaimer; private GUIListBox undoBufferList; @@ -106,7 +144,7 @@ namespace Barotrauma private GUIDropDown linkedSubBox; private static GUIComponent autoSaveLabel; - private static int maxAutoSaves = GameSettings.MaximumAutoSaves; + private readonly static int maxAutoSaves = GameSettings.MaximumAutoSaves; public static readonly object ItemAddMutex = new object(), ItemRemoveMutex = new object(); @@ -234,6 +272,8 @@ namespace Barotrauma public bool WiringMode => mode == Mode.Wiring; + private static readonly Dictionary Layers = new Dictionary(); + public SubEditorScreen() { cam = new Camera @@ -320,19 +360,34 @@ namespace Barotrauma new GUIFrame(new RectTransform(new Vector2(0.01f, 0.9f), paddedTopPanel.RectTransform), style: "VerticalLine"); - var visibilityButton = new GUIButton(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "SetupVisibilityButton") + visibilityButton = new GUIButton(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "SetupVisibilityButton") { ToolTip = TextManager.Get("SubEditorVisibilityButton") + '\n' + TextManager.Get("SubEditorVisibilityToolTip"), OnClicked = (btn, userData) => { previouslyUsedPanel.Visible = false; undoBufferPanel.Visible = false; + layerPanel.Visible = false; showEntitiesPanel.Visible = !showEntitiesPanel.Visible; showEntitiesPanel.RectTransform.AbsoluteOffset = new Point(Math.Max(Math.Max(btn.Rect.X, entityCountPanel.Rect.Right), saveAssemblyFrame.Rect.Right), TopPanel.Rect.Height); return true; } }; + new GUIButton(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "EditorLayerButton") + { + ToolTip = TextManager.Get("editor.layer.button") + '\n' + TextManager.Get("editor.layer.tooltip"), + OnClicked = (btn, userData) => + { + previouslyUsedPanel.Visible = false; + showEntitiesPanel.Visible = false; + undoBufferPanel.Visible = false; + layerPanel.Visible = !layerPanel.Visible; + layerPanel.RectTransform.AbsoluteOffset = new Point(Math.Max(Math.Max(btn.Rect.X, entityCountPanel.Rect.Right), saveAssemblyFrame.Rect.Right), TopPanel.Rect.Height); + return true; + } + }; + var previouslyUsedButton = new GUIButton(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "RecentlyUsedButton") { ToolTip = TextManager.Get("PreviouslyUsedLabel"), @@ -340,6 +395,7 @@ namespace Barotrauma { showEntitiesPanel.Visible = false; undoBufferPanel.Visible = false; + layerPanel.Visible = false; previouslyUsedPanel.Visible = !previouslyUsedPanel.Visible; previouslyUsedPanel.RectTransform.AbsoluteOffset = new Point(Math.Max(Math.Max(btn.Rect.X, entityCountPanel.Rect.Right), saveAssemblyFrame.Rect.Right), TopPanel.Rect.Height); return true; @@ -353,6 +409,7 @@ namespace Barotrauma { showEntitiesPanel.Visible = false; previouslyUsedPanel.Visible = false; + layerPanel.Visible = false; undoBufferPanel.Visible = !undoBufferPanel.Visible; undoBufferPanel.RectTransform.AbsoluteOffset = new Point(Math.Max(Math.Max(btn.Rect.X, entityCountPanel.Rect.Right), saveAssemblyFrame.Rect.Right), TopPanel.Rect.Height); return true; @@ -484,14 +541,84 @@ namespace Barotrauma //----------------------------------------------- + layerPanel = new GUIFrame(new RectTransform(new Vector2(0.25f, 0.4f), GUI.Canvas, minSize: new Point(300, 320))) + { + Visible = false + }; + + GUILayoutGroup layerGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f), layerPanel.RectTransform, anchor: Anchor.Center)); + + layerList = new GUIListBox(new RectTransform(new Vector2(1f, 0.8f), layerGroup.RectTransform)) + { + ScrollBarVisible = true, + AutoHideScrollBar = false, + OnSelected = (component, o) => + { + if (GUI.MouseOn is GUITickBox) { return false; } // lol + if (!(o is string layer)) { return false; } + + MapEntity.SelectedList.Clear(); + foreach (MapEntity entity in MapEntity.mapEntityList.Where(me => !me.Removed && me.Layer == layer)) + { + if (entity.IsSelected) { continue; } + + MapEntity.SelectedList.Add(entity); + } + return true; + } + }; + + GUILayoutGroup layerButtonGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.2f), layerGroup.RectTransform)); + + GUILayoutGroup layerButtonTopGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), layerButtonGroup.RectTransform), isHorizontal: true); + + GUIButton layerAddButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), layerButtonTopGroup.RectTransform), text: TextManager.Get("editor.layer.newlayer"), style: "GUIButtonFreeScale") + { + OnClicked = (button, o) => + { + CreateNewLayer(null, MapEntity.SelectedList.ToList()); + return true; + } + }; + + GUIButton layerDeleteButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), layerButtonTopGroup.RectTransform), text: TextManager.Get("editor.layer.deletelayer"), style: "GUIButtonFreeScale") + { + OnClicked = (button, o) => + { + if (layerList.SelectedData is string layer) + { + RenameLayer(layer, null); + } + return true; + } + }; + + GUIButton layerRenameButton = new GUIButton(new RectTransform(new Vector2(1f, 0.5f), layerButtonGroup.RectTransform), text: TextManager.Get("editor.layer.renamelayer"), style: "GUIButtonFreeScale") + { + OnClicked = (button, o) => + { + if (layerList.SelectedData is string layer) + { + GUI.PromptTextInput(TextManager.Get("editor.layer.renamelayer"), layer, newName => + { + RenameLayer(layer, newName); + }); + } + return true; + } + }; + + GUITextBlock.AutoScaleAndNormalize(layerAddButton.TextBlock, layerDeleteButton.TextBlock, layerRenameButton.TextBlock); + + + Vector2 subPanelSize = new Vector2(0.925f, 0.9f); + undoBufferPanel = new GUIFrame(new RectTransform(new Vector2(0.15f, 0.2f), GUI.Canvas) { MinSize = new Point(200, 200) }) { Visible = false }; - Vector2 undoSize = new Vector2(0.925f, 0.9f); - - undoBufferList = new GUIListBox(new RectTransform(undoSize, undoBufferPanel.RectTransform, Anchor.Center)) + undoBufferList = new GUIListBox(new RectTransform(subPanelSize, undoBufferPanel.RectTransform, Anchor.Center)) { ScrollBarVisible = true, OnSelected = (_, userData) => @@ -522,7 +649,7 @@ namespace Barotrauma } }; - undoBufferDisclaimer = new GUIFrame(new RectTransform(undoSize, undoBufferPanel.RectTransform, Anchor.Center), style: null) + undoBufferDisclaimer = new GUIFrame(new RectTransform(subPanelSize, undoBufferPanel.RectTransform, Anchor.Center), style: null) { Color = Color.Black, Visible = false @@ -757,6 +884,19 @@ namespace Barotrauma }; saveAssemblyFrame.RectTransform.MinSize = new Point(saveAssemblyFrame.Rect.Width, (int)(saveAssemblyButton.Rect.Height / saveAssemblyButton.RectTransform.RelativeSize.Y)); + snapToGridFrame = new GUIFrame(new RectTransform(new Vector2(0.08f, 0.5f), TopPanel.RectTransform, Anchor.BottomLeft, Pivot.TopLeft) + { MinSize = new Point((int)(250 * GUI.Scale), (int)(80 * GUI.Scale)), AbsoluteOffset = new Point((int)(10 * GUI.Scale), -saveAssemblyFrame.Rect.Height - entityCountPanel.Rect.Height - (int)(10 * GUI.Scale)) }, "InnerFrame") + { + Visible = false + }; + var saveStampButton = new GUIButton(new RectTransform(new Vector2(0.9f, 0.8f), snapToGridFrame.RectTransform, Anchor.Center), TextManager.Get("subeditor.snaptogrid", fallBackTag: "spriteeditor.snaptogrid")); + saveStampButton.TextBlock.AutoScaleHorizontal = true; + saveStampButton.OnClicked += (btn, userdata) => + { + SnapToGrid(); + return true; + }; + snapToGridFrame.RectTransform.MinSize = new Point(snapToGridFrame.Rect.Width, (int)(saveStampButton.Rect.Height / saveStampButton.RectTransform.RelativeSize.Y)); //Entity menu //------------------------------------------------ @@ -851,7 +991,7 @@ namespace Barotrauma }; paddedTab.Recalculate(); - + UpdateLayerPanel(); screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); } @@ -1071,6 +1211,7 @@ namespace Barotrauma { CanBeFocused = false, LoadAsynchronously = true, + SpriteEffects = icon.effects, Color = legacy ? iconColor * 0.6f : iconColor }; } @@ -1254,8 +1395,8 @@ namespace Barotrauma } ImageManager.OnEditorSelected(); + ReconstructLayers(); - GameAnalyticsManager.SetCustomDimension01("editor"); if (!GameMain.Config.EditorDisclaimerShown) { GameMain.Instance.ShowEditorDisclaimer(); @@ -1354,7 +1495,7 @@ namespace Barotrauma TimeSpan timeInEditor = DateTime.Now - editorSelectedTime; #if USE_STEAM - Steam.SteamManager.IncrementStat("hoursineditor", (float)timeInEditor.TotalHours); + SteamAchievementManager.IncrementStat("hoursineditor", (float)timeInEditor.TotalHours); #endif GUI.ForceMouseOn(null); @@ -1365,7 +1506,6 @@ namespace Barotrauma loadFrame = null; MapEntity.DeselectAll(); - MapEntity.SelectionGroups.Clear(); ClearUndoBuffer(); SetMode(Mode.Default); @@ -1400,6 +1540,7 @@ namespace Barotrauma }); ClearFilter(); + ClearLayers(); } private void CreateDummyCharacter() @@ -1752,11 +1893,11 @@ namespace Barotrauma DebugConsole.ThrowError($"Saving the preview image of the submarine \"{Submarine.MainSub.Info.Name}\" failed.", e); savePreviewImage = false; } - Submarine.MainSub.SaveAs(savePath, savePreviewImage ? imgStream : null); + Submarine.MainSub.TrySaveAs(savePath, savePreviewImage ? imgStream : null); } else { - Submarine.MainSub.SaveAs(savePath); + Submarine.MainSub.TrySaveAs(savePath); } Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; @@ -2114,7 +2255,7 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), priceGroup.RectTransform), TextManager.Get("subeditor.price"), textAlignment: Alignment.CenterLeft, wrap: true); - int basePrice = GameMain.DebugDraw ? 0 : Submarine.MainSub?.CalculateBasePrice() ?? 1000; + int basePrice = (GameMain.DebugDraw ? 0 : Submarine.MainSub?.CalculateBasePrice()) ?? 1000; new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), priceGroup.RectTransform), GUINumberInput.NumberType.Int, hidePlusMinusButtons: true) { IntValue = Math.Max(Submarine.MainSub?.Info?.Price ?? basePrice, basePrice), @@ -2591,6 +2732,38 @@ namespace Barotrauma return false; } + private void SnapToGrid() + { + // First move components + foreach (Item item in MapEntity.SelectedList.Where(entity => entity is Item).Cast()) + { + var wire = item.GetComponent(); + if (wire == null) + { + // Items snap to centre of nearest grid square + Vector2 offset = item.Position; + offset = new Vector2((MathF.Floor(offset.X / Submarine.GridSize.X) + .5f) * Submarine.GridSize.X - offset.X, (MathF.Floor(offset.Y / Submarine.GridSize.Y) + .5f) * Submarine.GridSize.Y - offset.Y); + item.Move(offset); + } + } + + // Then move wires, separated as moving components also moves the start and end node of wires + foreach (Item item in MapEntity.SelectedList.Where(entity => entity is Item).Cast()) + { + var wire = item.GetComponent(); + if (wire != null) + { + for (int i = 0; i < wire.GetNodes().Count; i++) + { + // Items wire nodes to centre of nearest grid square + Vector2 offset = wire.GetNodes()[i] + Submarine.MainSub.HiddenSubPosition; + offset = new Vector2((MathF.Floor(offset.X / Submarine.GridSize.X) + .5f) * Submarine.GridSize.X - offset.X, (MathF.Floor(offset.Y / Submarine.GridSize.Y) + .5f) * Submarine.GridSize.Y - offset.Y); + wire.MoveNode(i, offset); + } + } + } + } + private void CreateLoadScreen() { CloseItem(); @@ -2890,6 +3063,8 @@ namespace Barotrauma }; adjustLightsPrompt.Buttons[1].OnClicked += adjustLightsPrompt.Close; } + + ReconstructLayers(); } private void TryDeleteSub(SubmarineInfo sub) @@ -3075,6 +3250,8 @@ namespace Barotrauma if (container == null || container.DrawInventory) { target = item; } } + bool hasTargets = targets.Count > 0; + // Holding shift brings up special context menu options if (PlayerInput.IsShiftDown()) { @@ -3083,7 +3260,7 @@ namespace Barotrauma new ContextMenuOption("SubEditor.ToggleTransparency", isEnabled: true, onSelected: () => TransparentWiringMode = !TransparentWiringMode), new ContextMenuOption("SubEditor.ToggleGrid", isEnabled: true, onSelected: () => ShouldDrawGrid = !ShouldDrawGrid), new ContextMenuOption("SubEditor.PasteAssembly", isEnabled: true, () => PasteAssembly()), - new ContextMenuOption("Editor.SelectSame", isEnabled: targets.Count > 0, onSelected: delegate + new ContextMenuOption("Editor.SelectSame", isEnabled: hasTargets, onSelected: delegate { bool doorGapSelected = targets.Any(t => t is Gap gap && gap.ConnectedDoor != null); foreach (MapEntity match in MapEntity.mapEntityList.Where(e => e.prefab != null && targets.Any(t => t.prefab?.Identifier == e.prefab.Identifier) && !MapEntity.SelectedList.Contains(e))) @@ -3115,12 +3292,43 @@ namespace Barotrauma } else { + + List availableLayerOptions = new List + { + new ContextMenuOption("editor.layer.nolayer", true, onSelected: () => { MoveToLayer(null, targets); }) + }; + + availableLayerOptions.AddRange(Layers.Select(layer => new ContextMenuOption(layer.Key, true, onSelected: () => { MoveToLayer(layer.Key, targets); }))); + + ContextMenuOption[] layerOptions = + { + new ContextMenuOption("editor.layer.movetolayer", isEnabled: hasTargets, availableLayerOptions.ToArray()), + new ContextMenuOption("editor.layer.createlayer", isEnabled: hasTargets, onSelected: () => { CreateNewLayer(null, targets); }), + new ContextMenuOption("editor.layer.selectall", isEnabled: hasTargets, onSelected: () => + { + foreach (MapEntity match in MapEntity.mapEntityList.Where(e => targets.Any(t => !string.IsNullOrWhiteSpace(t.Layer) && t.Layer == e.Layer && !MapEntity.SelectedList.Contains(e)))) + { + if (MapEntity.SelectedList.Contains(match)) { continue; } + MapEntity.SelectedList.Add(match); + } + }), + new ContextMenuOption("editor.layer.openlayermenu", isEnabled: true, onSelected: () => + { + previouslyUsedPanel.Visible = false; + undoBufferPanel.Visible = false; + showEntitiesPanel.Visible = false; + layerPanel.Visible = !layerPanel.Visible; + layerPanel.RectTransform.AbsoluteOffset = new Point(Math.Max(Math.Max(visibilityButton.Rect.X, entityCountPanel.Rect.Right), saveAssemblyFrame.Rect.Right), TopPanel.Rect.Height); + }) + }; + GUIContextMenu.CreateContextMenu( - new ContextMenuOption("label.openlabel", isEnabled: target != null, onSelected: () => OpenItem(target)), - new ContextMenuOption("editor.cut", isEnabled: targets.Count > 0, onSelected: () => MapEntity.Cut(targets)), - new ContextMenuOption("editor.copytoclipboard", isEnabled: targets.Count > 0, onSelected: () => MapEntity.Copy(targets)), - new ContextMenuOption("editor.paste", isEnabled: MapEntity.CopiedList.Any(), onSelected: () => MapEntity.Paste(cam.ScreenToWorld(PlayerInput.MousePosition))), - new ContextMenuOption("delete", isEnabled: targets.Count > 0, onSelected: delegate + new ContextMenuOption("label.openlabel", isEnabled: target != null, onSelected: () => OpenItem(target)), + new ContextMenuOption("editor.layer", isEnabled: hasTargets, layerOptions), + new ContextMenuOption("editor.cut", isEnabled: hasTargets, onSelected: () => MapEntity.Cut(targets)), + new ContextMenuOption("editor.copytoclipboard", isEnabled: hasTargets, onSelected: () => MapEntity.Copy(targets)), + new ContextMenuOption("editor.paste", isEnabled: MapEntity.CopiedList.Any(), onSelected: () => MapEntity.Paste(cam.ScreenToWorld(PlayerInput.MousePosition))), + new ContextMenuOption("delete", isEnabled: hasTargets, onSelected: delegate { StoreCommand(new AddOrDeleteCommand(targets, true)); foreach (var me in targets) @@ -3131,6 +3339,76 @@ namespace Barotrauma } } + private void MoveToLayer(string layer, List content) + { + layer ??= string.Empty; + + foreach (MapEntity entity in content) + { + entity.Layer = layer; + } + } + + private void CreateNewLayer(string name, List content) + { + if (string.IsNullOrWhiteSpace(name)) + { + name = TextManager.Get("editor.layer.newlayer"); + } + + string incrementedName = name; + + for (int i = 1; Layers.ContainsKey(incrementedName); i++) + { + incrementedName = $"{name} ({i})"; + } + + name = incrementedName; + + if (content != null) + { + MoveToLayer(name, content); + } + + Layers.Add(name, LayerData.Default); + UpdateLayerPanel(); + } + + private void RenameLayer(string original, string newName) + { + Layers.Remove(original); + + foreach (MapEntity entity in MapEntity.mapEntityList.Where(entity => entity.Layer == original)) + { + entity.Layer = newName ?? string.Empty; + } + + if (!string.IsNullOrWhiteSpace(newName)) + { + Layers.TryAdd(newName, LayerData.Default); + } + UpdateLayerPanel(); + } + + private void ReconstructLayers() + { + ClearLayers(); + foreach (MapEntity entity in MapEntity.mapEntityList) + { + if (!string.IsNullOrWhiteSpace(entity.Layer)) + { + Layers.TryAdd(entity.Layer, LayerData.Default); + } + } + UpdateLayerPanel(); + } + + private void ClearLayers() + { + Layers.Clear(); + UpdateLayerPanel(); + } + private void PasteAssembly(string text = null, Vector2? pos = null) { pos ??= cam.ScreenToWorld(PlayerInput.MousePosition); @@ -4044,6 +4322,7 @@ namespace Barotrauma previouslyUsedPanel.AddToGUIUpdateList(); undoBufferPanel.AddToGUIUpdateList(); entityCountPanel.AddToGUIUpdateList(); + layerPanel.AddToGUIUpdateList(); TopPanel.AddToGUIUpdateList(); if (WiringMode) @@ -4147,9 +4426,92 @@ namespace Barotrauma GameMain.SubEditorScreen.UpdateUndoHistoryPanel(); } + private void UpdateLayerPanel() + { + if (layerPanel is null || layerList is null) { return; } + + layerList.Content.ClearChildren(); + + layerList.Deselect(); + GUILayoutGroup buttonHeaders = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.075f), layerList.Content.RectTransform), isHorizontal: true, childAnchor: Anchor.BottomLeft); + + new GUIButton(new RectTransform(new Vector2(0.25f, 1f), buttonHeaders.RectTransform), TextManager.Get("editor.layer.headervisible"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = true }; + new GUIButton(new RectTransform(new Vector2(0.15f, 1f), buttonHeaders.RectTransform), TextManager.Get("editor.layer.headerlink"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = true }; + new GUIButton(new RectTransform(new Vector2(0.6f, 1f), buttonHeaders.RectTransform), TextManager.Get("name"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = true }; + + foreach (var (layer, (visibility, linkage)) in Layers) + { + GUIFrame parent = new GUIFrame(new RectTransform(new Vector2(1f, 0.1f), layerList.Content.RectTransform), style: "ListBoxElement") + { + UserData = layer + }; + + GUILayoutGroup layerGroup = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + + GUILayoutGroup layerVisibilityLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.25f, 1f), layerGroup.RectTransform), childAnchor: Anchor.Center); + GUITickBox layerVisibleButton = new GUITickBox(new RectTransform(Vector2.One, layerVisibilityLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), string.Empty) + { + Selected = visibility == LayerVisibility.Visible, + OnSelected = box => + { + if (!Layers.TryGetValue(layer, out LayerData data)) + { + UpdateLayerPanel(); + return false; + } + + Layers[layer] = new LayerData(box.Selected ? LayerVisibility.Visible : LayerVisibility.Invisible, data.Linkage); + return true; + } + }; + + GUILayoutGroup layerChainLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.15f, 1f), layerGroup.RectTransform), childAnchor: Anchor.Center); + GUITickBox layerChainButton = new GUITickBox(new RectTransform(Vector2.One, layerChainLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), string.Empty) + { + Selected = linkage == LayerLinkage.Linked, + OnSelected = box => + { + if (!Layers.TryGetValue(layer, out LayerData data)) + { + UpdateLayerPanel(); + return false; + } + + Layers[layer] = new LayerData(data.Visible, box.Selected ? LayerLinkage.Linked : LayerLinkage.Unlinked); + return true; + } + }; + + layerGroup.Recalculate(); + + new GUITextBlock(new RectTransform(new Vector2(0.6f, 1f), layerGroup.RectTransform), layer, textAlignment: Alignment.CenterLeft) + { + CanBeFocused = false + }; + + layerGroup.Recalculate(); + layerChainLayout.Recalculate(); + layerVisibilityLayout.Recalculate(); + } + + layerList.RecalculateChildren(); + buttonHeaders.Recalculate(); + foreach (var child in buttonHeaders.Children) + { + var btn = child as GUIButton; + string originalBtnText = btn.Text; + btn.Text = ToolBox.LimitString(btn.Text, btn.Font, btn.Rect.Width); + if (originalBtnText != btn.Text) + { + btn.ToolTip = originalBtnText; + } + } + + } + public void UpdateUndoHistoryPanel() { - if (undoBufferPanel == null) { return; } + if (undoBufferPanel is null) { return; } undoBufferDisclaimer.Visible = mode == Mode.Wiring; @@ -4203,13 +4565,14 @@ namespace Barotrauma public override void Update(double deltaTime) { SkipInventorySlotUpdate = false; - ImageManager.Update((float) deltaTime); + ImageManager.Update((float)deltaTime); if (GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y) { saveFrame = null; loadFrame = null; saveAssemblyFrame = null; + snapToGridFrame = null; CreateUI(); UpdateEntityList(); } @@ -4257,6 +4620,7 @@ namespace Barotrauma hullVolumeFrame.Visible = MapEntity.SelectedList.Any(s => s is Hull); hullVolumeFrame.RectTransform.AbsoluteOffset = new Point(Math.Max(showEntitiesPanel.Rect.Right, previouslyUsedPanel.Rect.Right), 0); saveAssemblyFrame.Visible = MapEntity.SelectedList.Count > 0; + snapToGridFrame.Visible = MapEntity.SelectedList.Count > 0; var offset = cam.WorldView.Top - cam.ScreenToWorld(new Vector2(0, GameMain.GraphicsHeight - EntityMenu.Rect.Top)).Y; @@ -4718,8 +5082,14 @@ namespace Barotrauma MouseDragStart = Vector2.Zero; } - if (!saveAssemblyFrame.Rect.Contains(PlayerInput.MousePosition) && dummyCharacter?.SelectedConstruction == null && !WiringMode && GUI.MouseOn == null) + if (!saveAssemblyFrame.Rect.Contains(PlayerInput.MousePosition) && !snapToGridFrame.Rect.Contains(PlayerInput.MousePosition) && + dummyCharacter?.SelectedConstruction == null && !WiringMode && GUI.MouseOn == null) { + if (layerList is { Visible: true } && GUI.KeyboardDispatcher.Subscriber == layerList) + { + GUI.KeyboardDispatcher.Subscriber = null; + } + MapEntity.UpdateSelecting(cam); } @@ -4998,7 +5368,7 @@ namespace Barotrauma var prevScissorRect = GameMain.Instance.GraphicsDevice.ScissorRectangle; - Rectangle subDimensions = Submarine.MainSub.CalculateDimensions(false); + Rectangle subDimensions = Submarine.MainSub.CalculateDimensions(onlyHulls: false); Vector2 viewPos = subDimensions.Center.ToVector2(); float scale = Math.Min(width / (float)subDimensions.Width, height / (float)subDimensions.Height); @@ -5087,5 +5457,36 @@ namespace Barotrauma public static bool IsSubEditor() => Screen.Selected is SubEditorScreen && !Submarine.Unloading; public static bool IsWiringMode() => Screen.Selected == GameMain.SubEditorScreen && GameMain.SubEditorScreen.WiringMode && !Submarine.Unloading; + public static bool IsLayerVisible(MapEntity entity) + { + if (!IsSubEditor() || string.IsNullOrWhiteSpace(entity.Layer)) { return true; } + + if (!Layers.TryGetValue(entity.Layer, out LayerData data)) + { + Layers.TryAdd(entity.Layer, LayerData.Default); + return true; + } + + return data.Visible == LayerVisibility.Visible; + } + + public static bool IsLayerLinked(MapEntity entity) + { + if (!IsSubEditor() || string.IsNullOrWhiteSpace(entity.Layer)) { return false; } + + if (!Layers.TryGetValue(entity.Layer, out LayerData data)) + { + Layers.TryAdd(entity.Layer, LayerData.Default); + return true; + } + + return data.Linkage == LayerLinkage.Linked; + } + + public static ImmutableHashSet GetEntitiesInSameLayer(MapEntity entity) + { + if (string.IsNullOrWhiteSpace(entity.Layer)) { return ImmutableHashSet.Empty; } + return MapEntity.mapEntityList.Where(me => me.Layer == entity.Layer).ToImmutableHashSet(); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs index 38b34c0a0..4d71cfc0a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs @@ -23,6 +23,7 @@ namespace Barotrauma private Submarine? submarine; private Character? dummyCharacter; public static Effect BlueprintEffect; + private GUIFrame container; private TabMenu tabMenu; @@ -42,34 +43,37 @@ namespace Barotrauma return true; } }; + } 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()); + // dummyCharacter.Info.Name = "Galldren"; + // dummyCharacter.Inventory.CreateSlots(); Character.Controlled = dummyCharacter; GameMain.World.ProcessChanges(); - TabMenu.selectedTab = TabMenu.InfoFrameTab.Talents; tabMenu = new TabMenu(); } public override void AddToGUIUpdateList() { Frame.AddToGUIUpdateList(); - CharacterHUD.AddToGUIUpdateList(dummyCharacter); - dummyCharacter?.SelectedConstruction?.AddToGUIUpdateList(); - tabMenu.AddToGUIUpdateList(); + container.AddToGUIUpdateList(); + // CharacterHUD.AddToGUIUpdateList(dummyCharacter); + // dummyCharacter?.SelectedConstruction?.AddToGUIUpdateList(); } public override void Update(double deltaTime) @@ -92,12 +96,12 @@ namespace Barotrauma graphics.Clear(BackgroundColor); spriteBatch.Begin(SpriteSortMode.BackToFront, transformMatrix: Cam.Transform); - miniMapItem?.Draw(spriteBatch, false); - if (dummyCharacter is { } dummy) - { - dummyCharacter.DrawFront(spriteBatch, Cam); - dummyCharacter.Draw(spriteBatch, Cam); - } + // miniMapItem?.Draw(spriteBatch, false); + // if (dummyCharacter is { } dummy) + // { + // dummyCharacter.DrawFront(spriteBatch, Cam); + // dummyCharacter.Draw(spriteBatch, Cam); + // } spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index 66e9507f3..ab8ef0e8a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -567,8 +567,6 @@ namespace Barotrauma { if (SetPropertyValue(property, entity, numInput.FloatValue)) { - // This causes stack overflow. What's the purpose of it? - //numInput.FloatValue = (float)property.GetValue(entity); TrySendNetworkUpdate(entity, property); } }; @@ -674,7 +672,16 @@ namespace Barotrauma Text = value, OverflowClip = true }; - + + HashSet editedEntities = new HashSet(); + propertyBox.OnTextChanged += (textBox, text) => + { + foreach (var entity in MapEntity.SelectedList) + { + editedEntities.Add(entity); + } + return true; + }; propertyBox.OnDeselected += (textBox, keys) => OnApply(textBox); propertyBox.OnEnterPressed += (box, text) => OnApply(box); refresh += () => @@ -684,12 +691,25 @@ namespace Barotrauma bool OnApply(GUITextBox textBox) { + List prevSelected = MapEntity.SelectedList.ToList(); + //reselect the entities that were selected during editing + //otherwise multi-editing won't work when we deselect the entities with unapplied changes in the textbox + foreach (var entity in editedEntities) + { + MapEntity.SelectedList.Add(entity); + } if (SetPropertyValue(property, entity, textBox.Text)) { TrySendNetworkUpdate(entity, property); textBox.Text = (string) property.GetValue(entity); textBox.Flash(GUI.Style.Green, flashDuration: 1f); } + //restore the entities that were selected before applying + MapEntity.SelectedList.Clear(); + foreach (var entity in prevSelected) + { + MapEntity.SelectedList.Add(entity); + } return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index 9c8e2f0f4..57920dea6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -1157,7 +1157,7 @@ namespace Barotrauma public static void PlaySplashSound(Vector2 worldPosition, float strength) { if (SplashSounds.Count == 0) { return; } - int splashIndex = MathHelper.Clamp((int)(strength + Rand.Range(-2, 2)), 0, SplashSounds.Count - 1); + int splashIndex = MathHelper.Clamp((int)(strength + Rand.Range(-2.0f, 2.0f)), 0, SplashSounds.Count - 1); float range = 800.0f; var channel = SplashSounds[splashIndex].Play(1.0f, range, worldPosition, muffle: ShouldMuffleSound(Character.Controlled, worldPosition, range, null)); } @@ -1178,7 +1178,7 @@ namespace Barotrauma if ((s.damageRange == Vector2.Zero || (damage >= s.damageRange.X && damage <= s.damageRange.Y)) && string.Equals(s.damageType, damageType, StringComparison.OrdinalIgnoreCase) && - (tags == null ? string.IsNullOrEmpty(s.requiredTag) : tags.Contains(s.requiredTag))) + (string.IsNullOrEmpty(s.requiredTag) || (tags == null ? string.IsNullOrEmpty(s.requiredTag) : tags.Contains(s.requiredTag)))) { tempList.Add(s); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs index ac6b119c5..818255063 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs @@ -148,14 +148,8 @@ namespace Barotrauma return t; } string fullPath = Path.GetFullPath(file); - foreach (Sprite s in LoadedSprites) - { - if (s.FullPath == fullPath && s.texture != null && !s.texture.IsDisposed) - { - reusedSprite = s; - return s.texture; - } - } + reusedSprite = FindMatchingSprite(fullPath, requireTexture: true); + if (reusedSprite != null) { return reusedSprite.texture; } if (File.Exists(file)) { @@ -176,6 +170,22 @@ namespace Barotrauma return null; } + private static Sprite FindMatchingSprite(string fullPath, bool requireTexture) + { + lock (list) + { + foreach (var wRef in list) + { + if (wRef.TryGetTarget(out Sprite sprite)) + { + bool hasTexture = sprite.texture != null && !sprite.texture.IsDisposed; + if (sprite.FullPath == fullPath && (hasTexture || !requireTexture)) { return sprite; } + } + } + } + return null; + } + public void Draw(ISpriteBatch spriteBatch, Vector2 pos, float rotate = 0.0f, float scale = 1.0f, SpriteEffects spriteEffect = SpriteEffects.None) { this.Draw(spriteBatch, pos, Color.White, rotate, scale, spriteEffect); @@ -371,15 +381,9 @@ namespace Barotrauma //check if another sprite is using the same texture if (!string.IsNullOrEmpty(FilePath)) //file can be empty if the sprite is created directly from a Texture2D instance { - lock (list) - { - foreach (Sprite s in LoadedSprites) - { - if (s.FullPath == FullPath) { return; } - } - } + if (FindMatchingSprite(FullPath, requireTexture: false) != null) { return; } } - + //if not, free the texture if (texture != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs index 3d23f6590..2b06523f6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs @@ -14,9 +14,10 @@ namespace Barotrauma { private List particleEmitters; - private static HashSet ActiveLoopingSounds = new HashSet(); + private readonly static HashSet ActiveLoopingSounds = new HashSet(); private static double LastMuffleCheckTime; private readonly List sounds = new List(); + public IEnumerable Sounds { get { return sounds; } } private SoundSelectionMode soundSelectionMode; private SoundChannel soundChannel; private Entity soundEmitter; @@ -53,7 +54,7 @@ namespace Barotrauma } } - partial void ApplyProjSpecific(float deltaTime, Entity entity, IEnumerable targets, Hull hull, Vector2 worldPosition, bool playSound) + partial void ApplyProjSpecific(float deltaTime, Entity entity, IReadOnlyList targets, Hull hull, Vector2 worldPosition, bool playSound) { if (playSound) { @@ -84,7 +85,14 @@ namespace Barotrauma } else { - targetLimb = targets.FirstOrDefault(t => t is Limb) as Limb; + for (int i = 0; i < targets.Count; i++) + { + if (targets[i] is Limb limb) + { + targetLimb = limb; + break; + } + } } if (targetLimb != null && !targetLimb.Removed) { @@ -147,10 +155,6 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("StatusEffect.ApplyProjSpecific:SoundNull2" + Environment.StackTrace.CleanupStackTrace(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } - if (selectedSound.Sound.Disposed) - { - Submarine.ReloadRoundSound(selectedSound); - } soundChannel = SoundPlayer.PlaySound(selectedSound.Sound, worldPosition, selectedSound.Volume, selectedSound.Range, hullGuess: hull, ignoreMuffling: selectedSound.IgnoreMuffling); ignoreMuffling = selectedSound.IgnoreMuffling; if (soundChannel != null) { soundChannel.Looping = loopSound; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs index d8abe8504..34aa5e0ca 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs @@ -89,7 +89,17 @@ namespace Barotrauma for (int j = 0; j < infoTextFiles.Count; j++) { - List xmlContent = ConvertInfoTextToXML(File.ReadAllLines(infoTextFiles[j], Encoding.UTF8), language); + + List xmlContent = null; + try + { + xmlContent = ConvertInfoTextToXML(File.ReadAllLines(infoTextFiles[j], Encoding.UTF8), language); + } + catch (Exception e) + { + DebugConsole.ThrowError("InfoText Localization .csv to .xml conversion failed for: " + infoTextFiles[j], e); + continue; + } if (xmlContent == null) { DebugConsole.ThrowError("InfoText Localization .csv to .xml conversion failed for: " + infoTextFiles[j]); diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 85440866b..a1d359ed7 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.15.23.0 + 0.16.6.1 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index fedcfcf5d..4409934a5 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.15.23.0 + 0.16.6.1 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index abedb7b96..afe774c74 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.15.23.0 + 0.16.6.1 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 4233bf615..4378d2dfa 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.15.23.0 + 0.16.6.1 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 4aed46e5e..abf32b4e5 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.15.23.0 + 0.16.6.1 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs index d1bd9faf7..6c261ebbb 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs @@ -65,9 +65,9 @@ namespace Barotrauma GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.UpdateMoney }); } - partial void OnTalentGiven(string talentIdentifier) + partial void OnTalentGiven(TalentPrefab talentPrefab) { - GameServer.Log($"{GameServer.CharacterLogName(this)} has gained the talent '{talentIdentifier}'", ServerLog.MessageType.Talent); + GameServer.Log($"{GameServer.CharacterLogName(this)} has gained the talent '{talentPrefab.DisplayName}'", ServerLog.MessageType.Talent); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index cefa50456..14ffdf005 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -288,15 +288,18 @@ namespace Barotrauma { UInt32 talentIdentifier = msg.ReadUInt32(); var prefab = TalentPrefab.TalentPrefabs.Find(p => p.UIntIdentifier == talentIdentifier); - if (prefab != null) { talentSelection.Add(prefab.Identifier); } + if (prefab == null) { continue; } + + if (TalentTree.IsViableTalentForCharacter(this, prefab.Identifier, talentSelection)) + { + GiveTalent(prefab.Identifier); + talentSelection.Add(prefab.Identifier); + } } - talentSelection = TalentTree.CheckTalentSelection(this, talentSelection); - - foreach (string talent in talentSelection) + if (talentSelection.Count != talentCount) { - GiveTalent(talent); + DebugConsole.AddWarning($"Failed to unlock talents: the amount of unlocked talents doesn't match (client: {talentCount}, server: {talentSelection.Count})"); } - break; } break; diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index d132e2a59..121457d5c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -28,11 +28,11 @@ namespace Barotrauma if (!CheatsEnabled && IsCheat) { NewMessage("Client \"" + client.Name + "\" attempted to use the command \"" + names[0] + "\". Cheats must be enabled using \"enablecheats\" before the command can be used.", Color.Red); - GameMain.Server.SendConsoleMessage("You need to enable cheats using the command \"enablecheats\" before you can use the command \"" + names[0] + "\".", client); + GameMain.Server.SendConsoleMessage("You need to enable cheats using the command \"enablecheats\" before you can use the command \"" + names[0] + "\".", client, Color.Red); #if USE_STEAM NewMessage("Enabling cheats will disable Steam achievements during this play session.", Color.Red); - GameMain.Server.SendConsoleMessage("Enabling cheats will disable Steam achievements during this play session.", client); + GameMain.Server.SendConsoleMessage("Enabling cheats will disable Steam achievements during this play session.", client, Color.Red); #endif return; @@ -367,7 +367,7 @@ namespace Barotrauma } else { - GameMain.Server.SendConsoleMessage("\"" + args[0] + "\" is not a valid bot spawn mode. (Valid modes are Fill and Normal)", client); + GameMain.Server.SendConsoleMessage("\"" + args[0] + "\" is not a valid bot spawn mode. (Valid modes are Fill and Normal)", client, Color.Red); } }); @@ -1046,12 +1046,12 @@ namespace Barotrauma })); AssignOnClientRequestExecute("clientlist", (Client client, Vector2 cursorWorldPos, string[] args) => { - GameMain.Server.SendConsoleMessage("***************", client); + GameMain.Server.SendConsoleMessage("***************", client, Color.Cyan); foreach (Client c in GameMain.Server.ConnectedClients) { - GameMain.Server.SendConsoleMessage("- " + c.ID.ToString() + ": " + c.Name + ", " + c.Connection.EndPointString + $", ping {c.Ping} ms", client); + GameMain.Server.SendConsoleMessage("- " + c.ID.ToString() + ": " + c.Name + ", " + c.Connection.EndPointString + $", ping {c.Ping} ms", client, Color.Cyan); } - GameMain.Server.SendConsoleMessage("***************", client); + GameMain.Server.SendConsoleMessage("***************", client, Color.Cyan); }); commands.Add(new Command("enablecheats", "enablecheats: Enables cheat commands and disables Steam achievements during this play session.", (string[] args) => @@ -1164,13 +1164,13 @@ namespace Barotrauma if (GameMain.Server == null || args.Length == 0) return; if (!int.TryParse(args[0], out int maxPlayers)) { - GameMain.Server.SendConsoleMessage(args[0] + " is not a valid player count.", client); + GameMain.Server.SendConsoleMessage(args[0] + " is not a valid player count.", client, Color.Red); } else { if (maxPlayers > NetConfig.MaxPlayers) { - GameMain.Server.SendConsoleMessage($"Setting the maximum amount of players to {maxPlayers} failed due to exceeding the limit of {NetConfig.MaxPlayers} players per server. Using the maximum of {NetConfig.MaxPlayers} instead.", client); + GameMain.Server.SendConsoleMessage($"Setting the maximum amount of players to {maxPlayers} failed due to exceeding the limit of {NetConfig.MaxPlayers} players per server. Using the maximum of {NetConfig.MaxPlayers} instead.", client, Color.Red); maxPlayers = NetConfig.MaxPlayers; } @@ -1471,7 +1471,7 @@ namespace Barotrauma } else { - GameMain.Server.SendConsoleMessage("\"" + args[1] + "\" is not a valid ban duration.", client); + GameMain.Server.SendConsoleMessage("\"" + args[1] + "\" is not a valid ban duration.", client, Color.Red); return; } } @@ -1591,7 +1591,7 @@ namespace Barotrauma } else { - GameMain.Server.SendConsoleMessage("\"" + args[0] + "\" is not a valid bot spawn mode. (Valid modes are Fill and Normal)", client); + GameMain.Server.SendConsoleMessage("\"" + args[0] + "\" is not a valid bot spawn mode. (Valid modes are Fill and Normal)", client, Color.Red); } } ); @@ -1615,7 +1615,7 @@ namespace Barotrauma if (Submarine.MainSub == null || Level.Loaded == null) return; if (Level.Loaded.Type == LevelData.LevelType.Outpost) { - GameMain.Server.SendConsoleMessage("The teleportsub command is unavailable in outpost levels!", client); + GameMain.Server.SendConsoleMessage("The teleportsub command is unavailable in outpost levels!", client, Color.Red); return; } @@ -1639,7 +1639,7 @@ namespace Barotrauma { if (!(GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign)) { - GameMain.Server.SendConsoleMessage("No campaign active.", client); + GameMain.Server.SendConsoleMessage("No campaign active.", client, Color.Red); return; } mpCampaign.LastUpdateID++; @@ -1687,13 +1687,13 @@ namespace Barotrauma a.Identifier.Equals(args[0], StringComparison.OrdinalIgnoreCase)); if (afflictionPrefab == null) { - GameMain.Server.SendConsoleMessage("Affliction \"" + args[0] + "\" not found.", client); + GameMain.Server.SendConsoleMessage("Affliction \"" + args[0] + "\" 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); + GameMain.Server.SendConsoleMessage("\"" + args[1] + "\" is not a valid affliction strength.", client, Color.Red); return; } @@ -1776,7 +1776,7 @@ namespace Barotrauma c.DisplayName.Equals(args[0], StringComparison.OrdinalIgnoreCase)); if (talentPrefab == null) { - GameMain.Server.SendConsoleMessage("Couldn't find the talent \"" + args[0] + "\".", client); + GameMain.Server.SendConsoleMessage("Couldn't find the talent \"" + args[0] + "\".", client, Color.Red); return; } targetCharacter.GiveTalent(talentPrefab); @@ -1802,12 +1802,12 @@ namespace Barotrauma var job = JobPrefab.Prefabs.Find(jp => jp.Name != null && jp.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase)); if (job == null) { - GameMain.Server.SendConsoleMessage($"Failed to find the job \"{args[0]}\".", client); + GameMain.Server.SendConsoleMessage($"Failed to find the job \"{args[0]}\".", client, Color.Red); return; } if (!TalentTree.JobTalentTrees.TryGetValue(job.Identifier, out TalentTree talentTree)) { - GameMain.Server.SendConsoleMessage($"No talents configured for the job \"{args[0]}\".", client); + GameMain.Server.SendConsoleMessage($"No talents configured for the job \"{args[0]}\".", client, Color.Red); return; } talentTrees.Add(talentTree); @@ -1857,7 +1857,7 @@ namespace Barotrauma (Client client, Vector2 cursorWorldPos, string[] args) => { Vector2 explosionPos = cursorWorldPos; - float range = 500, force = 10, damage = 50, structureDamage = 10, itemDamage = 100, empStrength = 0.0f, ballastFloraStrength = 50f; + float range = 500, force = 10, damage = 50, structureDamage = 20, itemDamage = 100, empStrength = 0.0f, ballastFloraStrength = 50f; if (args.Length > 0) float.TryParse(args[0], out range); if (args.Length > 1) float.TryParse(args[1], out force); if (args.Length > 2) float.TryParse(args[2], out damage); @@ -1874,6 +1874,10 @@ namespace Barotrauma (Client client, Vector2 cursorWorldPos, string[] args) => { Character killedCharacter = (args.Length == 0) ? client.Character : FindMatchingCharacter(args); + if (killedCharacter == null) + { + GameMain.Server.SendConsoleMessage("Could not find the specified character.", client, Color.Red); + } killedCharacter?.SetAllDamage(200.0f, 0.0f, 0.0f); } ); @@ -1889,6 +1893,10 @@ namespace Barotrauma GameMain.Server.SetClientCharacter(client, character); client.SpectateOnly = false; } + else + { + GameMain.Server.SendConsoleMessage("Could not find the specified character.", client, Color.Red); + } } ); @@ -1915,7 +1923,7 @@ namespace Barotrauma } else { - GameMain.Server.SendConsoleMessage(args[0] + " is not a valid difficulty setting (enter a value between 0-100)", client); + GameMain.Server.SendConsoleMessage(args[0] + " is not a valid difficulty setting (enter a value between 0-100)", client, Color.Red); NewMessage(args[0] + " is not a valid difficulty setting (enter a value between 0-100)", Color.Red); } } @@ -1939,7 +1947,7 @@ namespace Barotrauma ClientPermissions permission = ClientPermissions.None; if (!Enum.TryParse(perm, true, out permission)) { - GameMain.Server.SendConsoleMessage(perm + " is not a valid permission!", senderClient); + GameMain.Server.SendConsoleMessage(perm + " is not a valid permission!", senderClient, Color.Red); return; } @@ -1971,7 +1979,7 @@ namespace Barotrauma } if (client.Connection == GameMain.Server.OwnerConnection) { - GameMain.Server.SendConsoleMessage("Cannot revoke permissions from the server owner!", senderClient); + GameMain.Server.SendConsoleMessage("Cannot revoke permissions from the server owner!", senderClient, Color.Red); return; } @@ -1980,7 +1988,7 @@ namespace Barotrauma ClientPermissions permission = ClientPermissions.None; if (!Enum.TryParse(perm, true, out permission)) { - GameMain.Server.SendConsoleMessage(perm + " is not a valid permission!", senderClient); + GameMain.Server.SendConsoleMessage(perm + " is not a valid permission!", senderClient, Color.Red); return; } client.RemovePermission(permission); @@ -2004,7 +2012,7 @@ namespace Barotrauma } if (client.Connection == GameMain.Server.OwnerConnection) { - GameMain.Server.SendConsoleMessage("Cannot modify the rank of the server owner!", senderClient); + GameMain.Server.SendConsoleMessage("Cannot modify the rank of the server owner!", senderClient, Color.Red); return; } @@ -2012,7 +2020,7 @@ namespace Barotrauma PermissionPreset preset = PermissionPreset.List.Find(p => p.Name.Equals(rank, StringComparison.OrdinalIgnoreCase)); if (preset == null) { - GameMain.Server.SendConsoleMessage("Rank \"" + rank + "\" not found.", senderClient); + GameMain.Server.SendConsoleMessage("Rank \"" + rank + "\" not found.", senderClient, Color.Red); return; } @@ -2032,12 +2040,12 @@ namespace Barotrauma var client = FindClient(args[0]); if (client == null) { - GameMain.Server.SendConsoleMessage("Client \"" + args[0] + "\" not found.", senderClient); + GameMain.Server.SendConsoleMessage("Client \"" + args[0] + "\" not found.", senderClient, Color.Red); return; } if (client.Connection == GameMain.Server.OwnerConnection) { - GameMain.Server.SendConsoleMessage("Cannot modify the command permissions of the server owner!", senderClient); + GameMain.Server.SendConsoleMessage("Cannot modify the command permissions of the server owner!", senderClient, Color.Red); return; } @@ -2056,7 +2064,7 @@ namespace Barotrauma Command matchingCommand = commands.Find(c => c.names.Contains(splitCommands[i])); if (matchingCommand == null) { - GameMain.Server.SendConsoleMessage("Could not find the command \"" + splitCommands[i] + "\"!", senderClient); + GameMain.Server.SendConsoleMessage("Could not find the command \"" + splitCommands[i] + "\"!", senderClient, Color.Red); } else { @@ -2094,7 +2102,7 @@ namespace Barotrauma } if (client.Connection == GameMain.Server.OwnerConnection) { - GameMain.Server.SendConsoleMessage("Cannot revoke command permissions from the server owner!", senderClient); + GameMain.Server.SendConsoleMessage("Cannot revoke command permissions from the server owner!", senderClient, Color.Red); return; } List revokedCommands = new List(); @@ -2112,7 +2120,7 @@ namespace Barotrauma Command matchingCommand = commands.Find(c => c.names.Contains(splitCommands[i])); if (matchingCommand == null) { - GameMain.Server.SendConsoleMessage("Could not find the command \"" + splitCommands[i] + "\"!", senderClient); + GameMain.Server.SendConsoleMessage("Could not find the command \"" + splitCommands[i] + "\"!", senderClient, Color.Red); } else { @@ -2192,7 +2200,7 @@ namespace Barotrauma { if (args.Length < 2) { - GameMain.Server.SendConsoleMessage("Invalid parameters. The command should be formatted as \"setclientcharacter [client] [character]\". If the names consist of multiple words, you should surround them with quotation marks.", senderClient); + GameMain.Server.SendConsoleMessage("Invalid parameters. The command should be formatted as \"setclientcharacter [client] [character]\". If the names consist of multiple words, you should surround them with quotation marks.", senderClient, Color.Red); return; } @@ -2205,7 +2213,7 @@ namespace Barotrauma var client = GameMain.Server.ConnectedClients.Find(c => c.Name == args[0]); if (client == null) { - GameMain.Server.SendConsoleMessage("Client \"" + args[0] + "\" not found.", senderClient); + GameMain.Server.SendConsoleMessage("Client \"" + args[0] + "\" not found.", senderClient, Color.Red); return; } @@ -2222,17 +2230,18 @@ namespace Barotrauma if (args.Length == 0) { return; } if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign)) { - GameMain.Server.SendConsoleMessage("No campaign active!", senderClient); + GameMain.Server.SendConsoleMessage("No campaign active!", senderClient, Color.Red); return; } if (int.TryParse(args[0], out int money)) { campaign.Money += money; + GameAnalyticsManager.AddMoneyGainedEvent(money, GameAnalyticsManager.MoneySource.Cheat, "console"); campaign.LastUpdateID++; } else { - GameMain.Server.SendConsoleMessage($"\"{args[0]}\" is not a valid numeric value.", senderClient); + GameMain.Server.SendConsoleMessage($"\"{args[0]}\" is not a valid numeric value.", senderClient, Color.Red); } } ); @@ -2242,7 +2251,7 @@ namespace Barotrauma { if (!(GameMain.GameSession?.GameMode is CampaignMode campaign)) { - GameMain.Server.SendConsoleMessage("No campaign active!", senderClient); + GameMain.Server.SendConsoleMessage("No campaign active!", senderClient, Color.Red); return; } @@ -2250,7 +2259,7 @@ namespace Barotrauma if (args.Length < 1 || !int.TryParse(args[0], out destinationIndex)) return; if (destinationIndex < 0 || destinationIndex >= campaign.Map.CurrentLocation.Connections.Count) { - GameMain.Server.SendConsoleMessage("Index out of bounds!", senderClient); + GameMain.Server.SendConsoleMessage("Index out of bounds!", senderClient, Color.Red); return; } Location location = campaign.Map.CurrentLocation.Connections[destinationIndex].OtherLocation(campaign.Map.CurrentLocation); @@ -2267,14 +2276,41 @@ namespace Barotrauma NewMessage(tag, Color.Yellow); } })); - + + commands.Add(new Command("sendchatmessage", "Sends a chat message with specified type and color.", (string[] args) => + { + if (args.Length < 2) { return; } + + ChatMessageType chatMessageType = ChatMessageType.Default; + Color? chatMessageColor = null; + + if (args.Length >= 3 && int.TryParse(args[2], out int result)) + { + chatMessageType = (ChatMessageType)result; + } + + if (args.Length >= 7 && + int.TryParse(args[3], out int r) && + int.TryParse(args[4], out int g) && + int.TryParse(args[5], out int b) && + int.TryParse(args[6], out int a)) + { + chatMessageColor = new Color(r, g, b, a); + } + + foreach (var client in GameMain.Server.ConnectedClients) + { + GameMain.Server.SendDirectChatMessage(ChatMessage.Create(args[0], args[1], chatMessageType, null, null, textColor: chatMessageColor), client); + } + })); + AssignOnClientRequestExecute( "setskill", (senderClient, cursorWorldPos, args) => { if (args.Length < 2) { - GameMain.Server.SendConsoleMessage($"Missing arguments. Expected at least 2 but got {args.Length} (skill, level, name)", senderClient); + GameMain.Server.SendConsoleMessage($"Missing arguments. Expected at least 2 but got {args.Length} (skill, level, name)", senderClient, Color.Red); return; } @@ -2284,7 +2320,7 @@ namespace Barotrauma if (character?.Info?.Job == null) { - GameMain.Server.SendConsoleMessage("Character is not valid.", senderClient); + GameMain.Server.SendConsoleMessage("Character is not valid.", senderClient, Color.Red); return; } @@ -2311,7 +2347,7 @@ namespace Barotrauma } else { - GameMain.Server.SendConsoleMessage($"{levelString} is not a valid level. Expected number or \"max\".", senderClient); + GameMain.Server.SendConsoleMessage($"{levelString} is not a valid level. Expected number or \"max\".", senderClient, Color.Red); } } ); @@ -2405,7 +2441,7 @@ namespace Barotrauma if (string.IsNullOrWhiteSpace(command)) return; if (!client.HasPermission(ClientPermissions.ConsoleCommands) && client.Connection != GameMain.Server.OwnerConnection) { - GameMain.Server.SendConsoleMessage("You are not permitted to use console commands!", client); + GameMain.Server.SendConsoleMessage("You are not permitted to use console commands!", client, Color.Red); GameServer.Log(GameServer.ClientLogName(client) + " attempted to execute the console command \"" + command + "\" without a permission to use console commands.", ServerLog.MessageType.ConsoleUsage); return; } @@ -2414,19 +2450,19 @@ namespace Barotrauma Command matchingCommand = commands.Find(c => c.names.Contains(splitCommand[0].ToLowerInvariant())); if (matchingCommand != null && !client.PermittedConsoleCommands.Contains(matchingCommand) && client.Connection != GameMain.Server.OwnerConnection) { - GameMain.Server.SendConsoleMessage("You are not permitted to use the command\"" + matchingCommand.names[0] + "\"!", client); + GameMain.Server.SendConsoleMessage("You are not permitted to use the command\"" + matchingCommand.names[0] + "\"!", client, Color.Red); GameServer.Log(GameServer.ClientLogName(client) + " attempted to execute the console command \"" + command + "\" without a permission to use the command.", ServerLog.MessageType.ConsoleUsage); return; } else if (matchingCommand == null) { - GameMain.Server.SendConsoleMessage("Command \"" + splitCommand[0] + "\" not found.", client); + GameMain.Server.SendConsoleMessage("Command \"" + splitCommand[0] + "\" not found.", client, Color.Red); return; } if (!MathUtils.IsValid(cursorWorldPos)) { - GameMain.Server.SendConsoleMessage("Could not execute command \"" + command + "\" - invalid cursor position.", client); + GameMain.Server.SendConsoleMessage("Could not execute command \"" + command + "\" - invalid cursor position.", client, Color.Red); NewMessage(GameServer.ClientLogName(client) + " attempted to execute the console command \"" + command + "\" with invalid cursor position.", Color.White); return; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index 367cef101..589881d1c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -19,6 +19,9 @@ namespace Barotrauma { public static readonly Version Version = Assembly.GetEntryAssembly().GetName().Version; + public static bool IsSingleplayer => NetworkMember == null; + public static bool IsMultiplayer => NetworkMember != null; + private static World world; public static World World @@ -36,7 +39,7 @@ namespace Barotrauma public static GameServer Server; public static NetworkMember NetworkMember { - get { return Server as NetworkMember; } + get { return Server; } } public static GameSession GameSession; @@ -64,6 +67,9 @@ namespace Barotrauma private static Stopwatch stopwatch; + private static Queue prevUpdateRates = new Queue(); + private static int updateCount = 0; + private static ContentPackage vanillaContent; public static ContentPackage VanillaContent { @@ -373,7 +379,9 @@ namespace Barotrauma { DebugConsole.NewMessage("WARNING: Stopwatch frequency under 1500 ticks per second. Expect significant syncing accuracy issues.", Color.Yellow); } - Stopwatch performanceMeasurement = new Stopwatch(); + + Stopwatch performanceCounterTimer = Stopwatch.StartNew(); + stopwatch = Stopwatch.StartNew(); long prevTicks = stopwatch.ElapsedTicks; while (ShouldRun) @@ -381,9 +389,11 @@ namespace Barotrauma long currTicks = stopwatch.ElapsedTicks; double elapsedTime = Math.Max(currTicks - prevTicks, 0) / frequency; Timing.Accumulator += elapsedTime; - if (Timing.Accumulator > 1.0) + if (Timing.Accumulator > Timing.AccumulatorMax) { - //prevent spiral of death + //prevent spiral of death: + //if the game's running too slowly then we have no choice but to skip a bunch of steps + //otherwise it snowballs and becomes unplayable Timing.Accumulator = Timing.Step; } prevTicks = currTicks; @@ -410,6 +420,7 @@ namespace Barotrauma performanceMeasurement.Reset(); Timing.Accumulator -= Timing.Step; + updateCount++; } #if !DEBUG @@ -425,10 +436,37 @@ namespace Barotrauma DebugConsole.UpdateCommandLine((int)(Timing.Accumulator * 800)); #endif - int frameTime = (int)(((double)(stopwatch.ElapsedTicks - prevTicks) / frequency) * 1000.0); + int frameTime = (int)((stopwatch.ElapsedTicks - prevTicks) / frequency * 1000.0); frameTime = Math.Max(0, frameTime); Thread.Sleep(Math.Max(((int)(Timing.Step * 1000.0) - frameTime) / 2, 0)); + + if (performanceCounterTimer.ElapsedMilliseconds > 1000) + { + int updateRate = (int)Math.Round(updateCount / (double)(performanceCounterTimer.ElapsedMilliseconds / 1000.0)); + prevUpdateRates.Enqueue(updateRate); + if (prevUpdateRates.Count >= 10) + { + int avgUpdateRate = (int)prevUpdateRates.Average(); + if (avgUpdateRate < Timing.FixedUpdateRate * 0.98 && GameSession != null && Timing.TotalTime > GameSession.RoundStartTime + 1.0) + { + DebugConsole.AddWarning($"Running slowly ({avgUpdateRate} updates/s)!"); + if (Server != null) + { + foreach (Client c in Server.ConnectedClients) + { + if (c.Connection == Server.OwnerConnection || c.Permissions != ClientPermissions.None) + { + Server.SendConsoleMessage($"Server running slowly ({avgUpdateRate} updates/s)!", c, Color.Orange); + } + } + } + } + prevUpdateRates.Clear(); + } + performanceCounterTimer.Restart(); + updateCount = 0; + } } stopwatch.Stop(); @@ -447,8 +485,9 @@ namespace Barotrauma public static void ResetFrameTime() { Timing.Accumulator = 0.0f; - stopwatch?.Reset(); - stopwatch?.Start(); + stopwatch?.Restart(); + prevUpdateRates.Clear(); + updateCount = 0; } public CoroutineHandle ShowLoading(IEnumerable loader, bool waitKeyHit = true) diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs index fff520d8f..1fe87b072 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using Barotrauma.Extensions; +using System.Collections.Generic; using System.Linq; namespace Barotrauma @@ -23,10 +24,10 @@ namespace Barotrauma { // Check all the prices before starting the transaction // to make sure the modifiers stay the same for the whole transaction - Dictionary sellValues = GetSellValuesAtCurrentLocation(itemsToBuy.Select(i => i.ItemPrefab)); - foreach (SoldItem item in itemsToBuy) + var sellValues = GetSellValuesAtCurrentLocation(itemsToBuy.Select(i => i.ItemPrefab)); + foreach (var item in itemsToBuy) { - var itemValue = sellValues[item.ItemPrefab]; + int itemValue = sellValues[item.ItemPrefab]; if (Location.StoreCurrentBalance < itemValue || item.Removed) { continue; } Location.StoreCurrentBalance += itemValue; campaign.Money -= itemValue; @@ -36,17 +37,29 @@ namespace Barotrauma public void SellItems(List itemsToSell) { + bool canAddToRemoveQueue = (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) && Entity.Spawner != null; + IEnumerable sellableItemsInSub = Enumerable.Empty(); + if (canAddToRemoveQueue && itemsToSell.Any(i => i.Origin == SoldItem.SellOrigin.Submarine && i.ID == Entity.NullEntityID && !i.Removed)) + { + sellableItemsInSub = GetSellableItemsFromSub(); + } // Check all the prices before starting the transaction // to make sure the modifiers stay the same for the whole transaction - Dictionary sellValues = GetSellValuesAtCurrentLocation(itemsToSell.Select(i => i.ItemPrefab)); - var canAddToRemoveQueue = (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) && Entity.Spawner != null; - foreach (SoldItem item in itemsToSell) + var sellValues = GetSellValuesAtCurrentLocation(itemsToSell.Select(i => i.ItemPrefab)); + foreach (var item in itemsToSell) { - var itemValue = sellValues[item.ItemPrefab]; - + int itemValue = sellValues[item.ItemPrefab]; // check if the store can afford the item and if the item hasn't been removed already if (Location.StoreCurrentBalance < itemValue || item.Removed) { continue; } - + // Server determines the items that are sold from the sub in multiplayer + if (item.Origin == SoldItem.SellOrigin.Submarine && item.ID == Entity.NullEntityID && !item.Removed) + { + var matchingItem = sellableItemsInSub.FirstOrDefault(i => !i.Removed && i.Prefab == item.ItemPrefab && + itemsToSell.None(itemToSell => itemToSell.ItemPrefab == i.Prefab && itemToSell.ID == i.ID)); + // This is a failsafe for scenarios where a client is trying to sell more items than there's available on the sub + if (matchingItem == null) { continue; } + item.SetItemId(matchingItem.ID); + } if (!item.Removed && canAddToRemoveQueue && Entity.FindEntityByID(item.ID) is Item entity) { item.Removed = true; @@ -55,6 +68,7 @@ namespace Barotrauma SoldItems.Add(item); Location.StoreCurrentBalance -= itemValue; campaign.Money += itemValue; + GameAnalyticsManager.AddMoneyGainedEvent(itemValue, GameAnalyticsManager.MoneySource.Store, item.ItemPrefab.Identifier); } OnSoldItemsChanged?.Invoke(); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs index 7f2316759..3dcc595ba 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs @@ -53,7 +53,7 @@ namespace Barotrauma foreach (var activeOrder in ActiveOrders) { if (!(activeOrder?.First is Order order) || activeOrder.Second.HasValue) { continue; } - OrderChatMessage.WriteOrder(msg, order, null, order.TargetSpatialEntity, null, 0, order.WallSectionIndex); + OrderChatMessage.WriteOrder(msg, order, targetCharacter: null, order.TargetSpatialEntity, orderOption: null, orderPriority: 0, order.WallSectionIndex, isNewOrder: true); bool hasOrderGiver = order.OrderGiver != null; msg.Write(hasOrderGiver); if (hasOrderGiver) diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 34883971f..fc2eaf90e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -166,16 +166,15 @@ namespace Barotrauma /// /// There is a client-side implementation of the method in /// - public bool AllowedToManageCampaign(Client client) + public bool AllowedToManageCampaign(Client client, ClientPermissions permissions = ClientPermissions.ManageCampaign) { - //allow ending the round if the client has permissions, is the owner, or the only client in the server, + //allow managing the campaign if the client has permissions, is the owner, or the only client in the server, //or if no-one has management permissions return - client.HasPermission(ClientPermissions.ManageCampaign) || + client.HasPermission(permissions) || GameMain.Server.ConnectedClients.Count == 1 || IsOwner(client) || - GameMain.Server.ConnectedClients.None(c => - c.InGame && (IsOwner(c) || c.HasPermission(ClientPermissions.ManageCampaign))); + GameMain.Server.ConnectedClients.None(c => c.InGame && (IsOwner(c) || c.HasPermission(permissions))); } public void SaveExperiencePoints(Client client) @@ -213,7 +212,14 @@ namespace Barotrauma } //use the info of the character the client is currently controlling // or the previously saved info if not (e.g. if the client has been spectating or died) - var characterInfo = c.Character?.Info ?? characterData.Find(d => d.MatchesClient(c))?.CharacterInfo; + var characterInfo = c.Character?.Info; + var matchingCharacterData = characterData.Find(d => d.MatchesClient(c)); + if (matchingCharacterData != null) + { + //hasn't spawned this round -> don't touch the data + if (!matchingCharacterData.HasSpawned) { continue; } + characterInfo ??= matchingCharacterData.CharacterInfo; + } if (characterInfo == null) { continue; } //reduce skills if the character has died if (characterInfo.CauseOfDeath != null && characterInfo.CauseOfDeath.Type != CauseOfDeathType.Disconnected) @@ -285,11 +291,15 @@ namespace Barotrauma break; case TransitionType.ProgressToNextLocation: Map.MoveToNextLocation(); + TotalPassedLevels++; break; case TransitionType.End: EndCampaign(); IsFirstRound = true; break; + case TransitionType.ProgressToNextEmptyLocation: + TotalPassedLevels++; + break; } Map.ProgressWorld(transitionType, (float)(Timing.TotalTime - GameMain.GameSession.RoundStartTime)); @@ -348,7 +358,6 @@ namespace Barotrauma } } } - UpdateCampaignSubs(); SaveUtil.SaveGame(GameMain.GameSession.SavePath); PendingSubmarineSwitch = null; @@ -399,44 +408,12 @@ namespace Barotrauma Map.OnMissionsSelected += (loc, mission) => { LastUpdateID++; }; Reputation.OnAnyReputationValueChanged += () => { LastUpdateID++; }; - UpdateCampaignSubs(); - //increment save ID so clients know they're lacking the most up-to-date save file LastSaveID++; } - public static void UpdateCampaignSubs() - { - bool isSubmarineVisible(SubmarineInfo s) - => !GameMain.Server.ServerSettings.HiddenSubs.Any(h - => s.Name.Equals(h, StringComparison.OrdinalIgnoreCase)); - - List availableSubs = - SubmarineInfo.SavedSubmarines - .Where(s => - s.IsCampaignCompatible - && isSubmarineVisible(s)) - .ToList(); - - if (!availableSubs.Any()) - { - //None of the available subs were marked as campaign-compatible, just include all visible subs - availableSubs.AddRange( - SubmarineInfo.SavedSubmarines - .Where(isSubmarineVisible)); - } - - if (!availableSubs.Any()) - { - //No subs are visible at all! Just make the selected one available - availableSubs.Add(GameMain.NetLobbyScreen.SelectedSub); - } - - GameMain.NetLobbyScreen.CampaignSubmarines = availableSubs; - } - public bool CanPurchaseSub(SubmarineInfo info) - => info.Price <= Money && GameMain.NetLobbyScreen.CampaignSubmarines.Contains(info); + => info.Price <= Money && GetCampaignSubs().Contains(info); public void DiscardClientCharacterData(Client client) { @@ -591,14 +568,21 @@ namespace Barotrauma foreach (PurchasedItem pi in CargoManager.ItemsInBuyCrate) { msg.Write(pi.ItemPrefab.Identifier); - msg.WriteRangedInteger(pi.Quantity, 0, 100); + msg.WriteRangedInteger(pi.Quantity, 0, CargoManager.MaxQuantity); + } + + msg.Write((UInt16)CargoManager.ItemsInSellFromSubCrate.Count); + foreach (PurchasedItem pi in CargoManager.ItemsInSellFromSubCrate) + { + msg.Write(pi.ItemPrefab.Identifier); + msg.WriteRangedInteger(pi.Quantity, 0, CargoManager.MaxQuantity); } msg.Write((UInt16)CargoManager.PurchasedItems.Count); foreach (PurchasedItem pi in CargoManager.PurchasedItems) { msg.Write(pi.ItemPrefab.Identifier); - msg.WriteRangedInteger(pi.Quantity, 0, 100); + msg.WriteRangedInteger(pi.Quantity, 0, CargoManager.MaxQuantity); } msg.Write((UInt16)CargoManager.SoldItems.Count); @@ -608,6 +592,7 @@ namespace Barotrauma msg.Write((UInt16)si.ID); msg.Write(si.Removed); msg.Write(si.SellerID); + msg.Write((byte)si.Origin); } msg.Write((ushort)UpgradeManager.PendingUpgrades.Count); @@ -662,6 +647,15 @@ namespace Barotrauma buyCrateItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity)); } + UInt16 subSellCrateItemCount = msg.ReadUInt16(); + List subSellCrateItems = new List(); + for (int i = 0; i < subSellCrateItemCount; i++) + { + string itemPrefabIdentifier = msg.ReadString(); + int itemQuantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity); + subSellCrateItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity)); + } + UInt16 purchasedItemCount = msg.ReadUInt16(); List purchasedItems = new List(); for (int i = 0; i < purchasedItemCount; i++) @@ -679,7 +673,8 @@ namespace Barotrauma UInt16 id = msg.ReadUInt16(); bool removed = msg.ReadBoolean(); byte sellerId = msg.ReadByte(); - soldItems.Add(new SoldItem(ItemPrefab.Prefabs[itemPrefabIdentifier], id, removed, sellerId)); + byte origin = msg.ReadByte(); + soldItems.Add(new SoldItem(ItemPrefab.Prefabs[itemPrefabIdentifier], id, removed, sellerId, (SoldItem.SellOrigin)origin)); } ushort purchasedUpgradeCount = msg.ReadUInt16(); @@ -703,122 +698,146 @@ namespace Barotrauma for (int i = 0; i < purchasedItemSwapCount; i++) { UInt16 itemToRemoveID = msg.ReadUInt16(); - Item itemToRemove = Entity.FindEntityByID(itemToRemoveID) as Item; - string itemToInstallIdentifier = msg.ReadString(); ItemPrefab itemToInstall = string.IsNullOrEmpty(itemToInstallIdentifier) ? null : ItemPrefab.Find(string.Empty, itemToInstallIdentifier); - - if (itemToRemove == null) { continue; } - + if (!(Entity.FindEntityByID(itemToRemoveID) is Item itemToRemove)) { continue; } purchasedItemSwaps.Add(new PurchasedItemSwap(itemToRemove, itemToInstall)); } - if (!AllowedToManageCampaign(sender)) + bool allowedToManageCampaign = AllowedToManageCampaign(sender); + if (AllowedToManageCampaign(sender)) { - DebugConsole.ThrowError("Client \"" + sender.Name + "\" does not have a permission to manage the campaign"); - return; - } - - Location location = Map.CurrentLocation; - int hullRepairCost = location?.GetAdjustedMechanicalCost(HullRepairCost) ?? HullRepairCost; - int itemRepairCost = location?.GetAdjustedMechanicalCost(ItemRepairCost) ?? ItemRepairCost; - int shuttleRetrieveCost = location?.GetAdjustedMechanicalCost(ShuttleReplaceCost) ?? ShuttleReplaceCost; - - if (purchasedHullRepairs != this.PurchasedHullRepairs) - { - if (purchasedHullRepairs && Money >= hullRepairCost) + Location location = Map.CurrentLocation; + int hullRepairCost = location?.GetAdjustedMechanicalCost(HullRepairCost) ?? HullRepairCost; + int itemRepairCost = location?.GetAdjustedMechanicalCost(ItemRepairCost) ?? ItemRepairCost; + int shuttleRetrieveCost = location?.GetAdjustedMechanicalCost(ShuttleReplaceCost) ?? ShuttleReplaceCost; + if (purchasedHullRepairs != this.PurchasedHullRepairs) { - this.PurchasedHullRepairs = true; - Money -= hullRepairCost; + if (purchasedHullRepairs && Money >= hullRepairCost) + { + this.PurchasedHullRepairs = true; + Money -= hullRepairCost; + GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "hullrepairs"); + } + else if (!purchasedHullRepairs) + { + this.PurchasedHullRepairs = false; + Money += hullRepairCost; + } } - else if (!purchasedHullRepairs) + if (purchasedItemRepairs != this.PurchasedItemRepairs) { - this.PurchasedHullRepairs = false; - Money += hullRepairCost; + if (purchasedItemRepairs && Money >= itemRepairCost) + { + this.PurchasedItemRepairs = true; + Money -= itemRepairCost; + GameAnalyticsManager.AddMoneySpentEvent(itemRepairCost, GameAnalyticsManager.MoneySink.Service, "devicerepairs"); + } + else if (!purchasedItemRepairs) + { + this.PurchasedItemRepairs = false; + Money += itemRepairCost; + } } - } - if (purchasedItemRepairs != this.PurchasedItemRepairs) - { - if (purchasedItemRepairs && Money >= itemRepairCost) + if (purchasedLostShuttles != this.PurchasedLostShuttles) { - this.PurchasedItemRepairs = true; - Money -= itemRepairCost; + if (GameMain.GameSession?.SubmarineInfo != null && + GameMain.GameSession.SubmarineInfo.LeftBehindSubDockingPortOccupied) + { + GameMain.Server.SendDirectChatMessage(TextManager.FormatServerMessage("ReplaceShuttleDockingPortOccupied"), sender, ChatMessageType.MessageBox); + } + else if (purchasedLostShuttles && Money >= shuttleRetrieveCost) + { + this.PurchasedLostShuttles = true; + Money -= shuttleRetrieveCost; + GameAnalyticsManager.AddMoneySpentEvent(shuttleRetrieveCost, GameAnalyticsManager.MoneySink.Service, "retrieveshuttle"); + } + else if (!purchasedItemRepairs) + { + this.PurchasedLostShuttles = false; + Money += shuttleRetrieveCost; + } } - else if (!purchasedItemRepairs) + if (currentLocIndex < Map.Locations.Count && Map.AllowDebugTeleport) { - this.PurchasedItemRepairs = false; - Money += itemRepairCost; - } - } - if (purchasedLostShuttles != this.PurchasedLostShuttles) - { - if (GameMain.GameSession?.SubmarineInfo != null && - GameMain.GameSession.SubmarineInfo.LeftBehindSubDockingPortOccupied) - { - GameMain.Server.SendDirectChatMessage(TextManager.FormatServerMessage("ReplaceShuttleDockingPortOccupied"), sender, ChatMessageType.MessageBox); - } - else if (purchasedLostShuttles && Money >= shuttleRetrieveCost) - { - this.PurchasedLostShuttles = true; - Money -= shuttleRetrieveCost; - } - else if (!purchasedItemRepairs) - { - this.PurchasedLostShuttles = false; - Money += shuttleRetrieveCost; + 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); } + CheckTooManyMissions(Map.CurrentLocation, sender); } - if (currentLocIndex < Map.Locations.Count && Map.AllowDebugTeleport) + bool allowedToUseStore = AllowedToManageCampaign(sender, ClientPermissions.CampaignStore); + if (allowedToManageCampaign || allowedToUseStore || AllowedToManageCampaign(sender, ClientPermissions.BuyItems)) { - Map.SetLocation(currentLocIndex); + var currentBuyCrateItems = new List(CargoManager.ItemsInBuyCrate); + currentBuyCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInBuyCrate(i.ItemPrefab, -i.Quantity)); + buyCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInBuyCrate(i.ItemPrefab, i.Quantity)); + CargoManager.SellBackPurchasedItems(new List(CargoManager.PurchasedItems)); + CargoManager.PurchaseItems(purchasedItems, false); } - Map.SelectLocation(selectedLocIndex == UInt16.MaxValue ? -1 : selectedLocIndex); - if (Map.SelectedLocation == null) { Map.SelectRandomLocation(preferUndiscovered: true); } - if (Map.SelectedConnection != null) { Map.SelectMission(selectedMissionIndices); } - - CheckTooManyMissions(Map.CurrentLocation, sender); - - List currentBuyCrateItems = new List(CargoManager.ItemsInBuyCrate); - currentBuyCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInBuyCrate(i.ItemPrefab, -i.Quantity)); - buyCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInBuyCrate(i.ItemPrefab, i.Quantity)); - - CargoManager.SellBackPurchasedItems(new List(CargoManager.PurchasedItems)); - CargoManager.PurchaseItems(purchasedItems, false); - - // 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); - - foreach (var (prefab, category, _) in purchasedUpgrades) + bool allowedToSellSubItems = AllowedToManageCampaign(sender, ClientPermissions.SellSubItems); + if (allowedToManageCampaign || allowedToUseStore || allowedToSellSubItems) { - UpgradeManager.PurchaseUpgrade(prefab, category); - - // unstable logging - int price = prefab.Price.GetBuyprice(UpgradeManager.GetUpgradeLevel(prefab, category), Map?.CurrentLocation); - int level = UpgradeManager.GetUpgradeLevel(prefab, category); - GameServer.Log($"SERVER: Purchased level {level} {category.Identifier}.{prefab.Identifier} for {price}", ServerLog.MessageType.ServerMessage); + var currentSubSellCrateItems = new List(CargoManager.ItemsInSellFromSubCrate); + currentSubSellCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInSubSellCrate(i.ItemPrefab, -i.Quantity)); + subSellCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInSubSellCrate(i.ItemPrefab, i.Quantity)); } - foreach (var purchasedItemSwap in purchasedItemSwaps) + bool allowedToSellInventoryItems = AllowedToManageCampaign(sender, ClientPermissions.SellInventoryItems); + if (allowedToManageCampaign || allowedToUseStore || (allowedToSellInventoryItems && allowedToSellSubItems)) { - if (purchasedItemSwap.ItemToInstall == null) + // 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); + } + else if (allowedToSellInventoryItems || allowedToSellSubItems) + { + if (allowedToSellInventoryItems) { - UpgradeManager.CancelItemSwap(purchasedItemSwap.ItemToRemove); + CargoManager.BuyBackSoldItems(new List(CargoManager.SoldItems.Where(i => i.Origin == SoldItem.SellOrigin.Character))); + soldItems.RemoveAll(i => i.Origin != SoldItem.SellOrigin.Character); } else { - UpgradeManager.PurchaseItemSwap(purchasedItemSwap.ItemToRemove, purchasedItemSwap.ItemToInstall); + CargoManager.BuyBackSoldItems(new List(CargoManager.SoldItems.Where(i => i.Origin == SoldItem.SellOrigin.Submarine))); + soldItems.RemoveAll(i => i.Origin != SoldItem.SellOrigin.Submarine); } + CargoManager.SellItems(soldItems); } - foreach (Item item in Item.ItemList) + + if (allowedToManageCampaign) { - if (item.PendingItemSwap != null && !purchasedItemSwaps.Any(it => it.ItemToRemove == item)) + foreach (var (prefab, category, _) in purchasedUpgrades) { - UpgradeManager.CancelItemSwap(item); - item.PendingItemSwap = null; + UpgradeManager.PurchaseUpgrade(prefab, category); + + // unstable logging + int price = prefab.Price.GetBuyprice(UpgradeManager.GetUpgradeLevel(prefab, category), Map?.CurrentLocation); + int level = UpgradeManager.GetUpgradeLevel(prefab, category); + GameServer.Log($"SERVER: Purchased level {level} {category.Identifier}.{prefab.Identifier} for {price}", ServerLog.MessageType.ServerMessage); + } + foreach (var purchasedItemSwap in purchasedItemSwaps) + { + if (purchasedItemSwap.ItemToInstall == null) + { + UpgradeManager.CancelItemSwap(purchasedItemSwap.ItemToRemove); + } + else + { + UpgradeManager.PurchaseItemSwap(purchasedItemSwap.ItemToRemove, purchasedItemSwap.ItemToInstall); + } + } + foreach (Item item in Item.ItemList) + { + if (item.PendingItemSwap != null && !purchasedItemSwaps.Any(it => it.ItemToRemove == item)) + { + UpgradeManager.CancelItemSwap(item); + item.PendingItemSwap = null; + } } } } @@ -1031,7 +1050,9 @@ namespace Barotrauma new XAttribute("purchasedhullrepairs", PurchasedHullRepairs), new XAttribute("purchaseditemrepairs", PurchasedItemRepairs), new XAttribute("cheatsenabled", CheatsEnabled)); + modeElement.Add(Settings.Save()); + modeElement.Add(SaveStats()); 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 new file mode 100644 index 000000000..3e2afe845 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs @@ -0,0 +1,191 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using Barotrauma.Networking; + +namespace Barotrauma +{ + internal partial class MedicalClinic + { + private enum RateLimitResult + { + OK, + LimitReached + } + + private struct RateLimitInfo + { + public int Requests; + public const int MaxRequests = 5; + public DateTimeOffset Expiry; + } + + private readonly Dictionary rateLimits = new Dictionary(); + + public void ServerRead(IReadMessage inc, Client sender) + { + NetworkHeader header = (NetworkHeader)inc.ReadByte(); + + switch (header) + { + case NetworkHeader.REQUEST_AFFLICTIONS: + ProcessRequestedAfflictions(inc, sender); + break; + case NetworkHeader.REQUEST_PENDING: + ProcessRequestedPending(sender); + break; + case NetworkHeader.ADD_PENDING: + ProcessNewAddition(inc, sender); + break; + case NetworkHeader.REMOVE_PENDING: + ProcessNewRemoval(inc, sender); + break; + case NetworkHeader.HEAL_PENDING: + ProcessHealing(sender); + break; + case NetworkHeader.CLEAR_PENDING: + ProcessClearing(sender); + break; + } + } + + private void ProcessNewAddition(IReadMessage inc, Client client) + { + if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + + NetCrewMember newCrewMember = INetSerializableStruct.Read(inc); + InsertPendingCrewMember(newCrewMember); + ServerSend(newCrewMember, NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable, reponseClient: client); + } + + private void ProcessNewRemoval(IReadMessage inc, Client client) + { + if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + + NetRemovedAffliction removed = INetSerializableStruct.Read(inc); + RemovePendingAffliction(removed.CrewMember, removed.Affliction); + ServerSend(removed, NetworkHeader.REMOVE_PENDING, DeliveryMethod.Reliable, reponseClient: client); + } + + private void ProcessRequestedPending(Client client) + { + if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + + INetSerializableStruct writeCrewMember = new NetPendingCrew + { + CrewMembers = PendingHeals.ToArray() + }; + + ServerSend(writeCrewMember, NetworkHeader.REQUEST_PENDING, DeliveryMethod.Reliable, targetClient: client); + } + + private void ProcessHealing(Client client) + { + if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + + HealRequestResult result = HealAllPending(); + ServerSend(new NetHealRequest { Result = result }, NetworkHeader.HEAL_PENDING, DeliveryMethod.Reliable, reponseClient: client); + } + + private void ProcessClearing(Client client) + { + if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + + if (!PendingHeals.Any()) { return; } + + ClearPendingHeals(); + ServerSend(null, NetworkHeader.CLEAR_PENDING, DeliveryMethod.Reliable, reponseClient: client); + } + + private void ProcessRequestedAfflictions(IReadMessage inc, Client client) + { + if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + + NetCrewMember crewMember = INetSerializableStruct.Read(inc); + + CharacterInfo? foundInfo = crewMember.FindCharacterInfo(GetCrewCharacters()); + + NetAffliction[] pendingAfflictions = Array.Empty(); + int infoId = 0; + + if (foundInfo is { Character: { CharacterHealth: { } health } }) + { + pendingAfflictions = GetAllAfflictions(health); + infoId = foundInfo.GetIdentifierUsingOriginalName(); + } + + INetSerializableStruct writeCrewMember = new NetCrewMember + { + CharacterInfoID = infoId, + Afflictions = pendingAfflictions + }; + + ServerSend(writeCrewMember, NetworkHeader.REQUEST_AFFLICTIONS, DeliveryMethod.Unreliable, client); + } + + private RateLimitResult CheckRateLimit(Client client) + { + if (rateLimits.TryGetValue(client, out RateLimitInfo rateLimitInfo)) + { + if (rateLimitInfo.Expiry < DateTimeOffset.Now) + { + rateLimitInfo.Expiry = DateTimeOffset.Now.AddSeconds(5); + rateLimitInfo.Requests = 1; + } + else + { + if (rateLimitInfo.Requests > RateLimitInfo.MaxRequests) { return RateLimitResult.LimitReached; } + + rateLimitInfo.Requests++; + } + + rateLimits[client] = rateLimitInfo; + } + else + { + rateLimits.Add(client, new RateLimitInfo { Requests = 1, Expiry = DateTimeOffset.Now.AddSeconds(5) }); + } + + return RateLimitResult.OK; + } + + private IWriteMessage StartSending() + { + IWriteMessage msg = new WriteOnlyMessage(); + msg.Write((byte)ServerPacketHeader.MEDICAL); + return msg; + } + + private void ServerSend(INetSerializableStruct? netStruct, NetworkHeader header, DeliveryMethod deliveryMethod, Client? targetClient = null, Client? reponseClient = null) + { + if (targetClient is null) + { + foreach (Client c in GameMain.Server.ConnectedClients) + { + SendToClient(c); + } + + return; + } + + SendToClient(targetClient); + + void SendToClient(Client c) + { + MessageFlag flag = MessageFlag.Announce; + if (reponseClient != null && reponseClient == c) + { + flag = MessageFlag.Response; + } + + IWriteMessage msg = StartSending(); + msg.Write((byte)header); + msg.Write((byte)flag); + netStruct?.Write(msg); + GameMain.Server.ServerPeer.Send(msg, c.Connection, deliveryMethod); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Growable.cs index 16fe7f125..b4490c121 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Growable.cs @@ -1,13 +1,14 @@ -using System; -using System.ComponentModel; -using System.Linq; +using Barotrauma.Networking; +using System; using System.Xml.Linq; -using Barotrauma.Networking; namespace Barotrauma.Items.Components { internal partial class Growable { + private const int serverHealthUpdateDelay = 10; + private int serverHealthUpdateTimer; + partial void LoadVines(XElement element) { foreach (XElement subElement in element.Elements()) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs index 472cf14b9..c26c7cde2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs @@ -50,14 +50,14 @@ namespace Barotrauma.Items.Components newSteeringInput = new Vector2(msg.ReadSingle(), msg.ReadSingle()); } - if (!item.CanClientAccess(c)) return; + if (!item.CanClientAccess(c)) { return; } user = c.Character; AutoPilot = autoPilot; if (dockingButtonClicked) { - item.SendSignal("1", "toggle_docking"); + item.SendSignal(new Signal("1", sender: c.Character), "toggle_docking"); GameMain.Server.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ComponentState, item.GetComponentIndex(this), true }); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs index 23c145bbf..5f9b0385b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs @@ -46,6 +46,7 @@ namespace Barotrauma.Items.Components msg.Write(DeteriorateAlways); msg.Write(tinkeringDuration); msg.Write(tinkeringStrength); + msg.Write(tinkeringPowersDevices); msg.Write(CurrentFixer == null ? (ushort)0 : CurrentFixer.ID); msg.WriteRangedInteger((int)currentFixerAction, 0, 2); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Rope.cs index 16e3abf9a..e56702fc3 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Rope.cs @@ -7,6 +7,26 @@ namespace Barotrauma.Items.Components public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { msg.Write(Snapped); + + if (!Snapped) + { + msg.Write(target?.ID ?? Entity.NullEntityID); + if (source is Entity entity && !entity.Removed) + { + msg.Write(entity?.ID ?? Entity.NullEntityID); + msg.Write((byte)0); + } + else if (source is Limb limb && limb.character != null && !limb.character.Removed) + { + msg.Write(limb.character?.ID ?? Entity.NullEntityID); + msg.Write((byte)limb.character.AnimController.Limbs.IndexOf(limb)); + } + else + { + msg.Write(Entity.NullEntityID); + msg.Write((byte)0); + } + } } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ButtonTerminal.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ButtonTerminal.cs index 056410165..4cdc0ec1b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ButtonTerminal.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ButtonTerminal.cs @@ -8,7 +8,7 @@ namespace Barotrauma.Items.Components { int signalIndex = msg.ReadRangedInteger(0, Signals.Length - 1); if (!item.CanClientAccess(c)) { return; } - if (!SendSignal(signalIndex)) { 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 }); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/WifiComponent.cs new file mode 100644 index 000000000..333028165 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/WifiComponent.cs @@ -0,0 +1,12 @@ +using Barotrauma.Networking; + +namespace Barotrauma.Items.Components +{ + partial class WifiComponent + { + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + { + msg.WriteRangedInteger(Channel, MinChannel, MaxChannel); + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index 07117c718..918bfde23 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -98,22 +98,6 @@ namespace Barotrauma case NetEntityEvent.Type.AssignCampaignInteraction: msg.Write((byte)CampaignInteractionType); break; - case NetEntityEvent.Type.Treatment: - { - ItemComponent targetComponent = (ItemComponent)extraData[1]; - ActionType actionType = (ActionType)extraData[2]; - ushort targetID = (ushort)extraData[3]; - Limb targetLimb = (Limb)extraData[4]; - - Character targetCharacter = FindEntityByID(targetID) as Character; - byte targetLimbIndex = targetLimb != null && targetCharacter != null ? (byte)Array.IndexOf(targetCharacter.AnimController.Limbs, targetLimb) : (byte)255; - - msg.Write((byte)components.IndexOf(targetComponent)); - msg.WriteRangedInteger((int)actionType, 0, Enum.GetValues(typeof(ActionType)).Length - 1); - msg.Write(targetID); - msg.Write(targetLimbIndex); - } - break; case NetEntityEvent.Type.ApplyStatusEffect: { ActionType actionType = (ActionType)extraData[1]; @@ -268,6 +252,7 @@ namespace Barotrauma msg.Write(Position.X); msg.Write(Position.Y); + msg.WriteRangedSingle(body == null ? 0.0f : MathUtils.WrapAngleTwoPi(body.Rotation), 0.0f, MathHelper.TwoPi, 8); msg.Write(Submarine != null ? Submarine.ID : (ushort)0); } else diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs index dd99114fd..827171ade 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs @@ -27,8 +27,8 @@ namespace Barotrauma //don't create updates if all clients are very far from the hull float hullUpdateDistanceSqr = NetConfig.HullUpdateDistance * NetConfig.HullUpdateDistance; if (!GameMain.Server.ConnectedClients.Any(c => - c.Character != null && - Vector2.DistanceSquared(c.Character.WorldPosition, WorldPosition) < hullUpdateDistanceSqr)) + c.Character != null && + Vector2.DistanceSquared(c.Character.WorldPosition, WorldPosition) < hullUpdateDistanceSqr)) { return; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs index fd61f0c1c..754fbc499 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs @@ -39,7 +39,7 @@ namespace Barotrauma.Networking string orderOption = orderMessageInfo.OrderOption ?? (orderMessageInfo.OrderOptionIndex == null || orderMessageInfo.OrderOptionIndex < 0 || orderMessageInfo.OrderOptionIndex >= orderPrefab.Options.Length ? "" : orderPrefab.Options[orderMessageInfo.OrderOptionIndex.Value]); - orderMsg = new OrderChatMessage(orderPrefab, orderOption, orderMessageInfo.Priority, orderTargetPosition ?? orderTargetEntity as ISpatialEntity, orderTargetCharacter, c.Character) + orderMsg = new OrderChatMessage(orderPrefab, orderOption, orderMessageInfo.Priority, orderTargetPosition ?? orderTargetEntity as ISpatialEntity, orderTargetCharacter, c.Character, isNewOrder: orderMessageInfo.IsNewOrder) { WallSectionIndex = wallSectionIndex }; @@ -223,6 +223,11 @@ namespace Barotrauma.Networking { msg.Write(Sender.ID); } + msg.Write(customTextColor != null); + if (customTextColor != null) + { + msg.WriteColorR8G8B8A8(customTextColor.Value); + } msg.WritePadBits(); if (Type == ChatMessageType.ServerMessageBoxInGame) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs index 6e7a4fc84..86c7ac808 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs @@ -117,8 +117,14 @@ namespace Barotrauma.Networking { GameMain.Server.VoipServer.UnregisterQueue(VoipQueue); VoipQueue.Dispose(); - characterInfo?.Remove(); - characterInfo = null; + if (characterInfo != null) + { + if (characterInfo.Character == null || characterInfo.Character.Removed) + { + characterInfo?.Remove(); + characterInfo = null; + } + } } public void InitClientSync() diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 91beb3a79..815393bec 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -222,33 +222,6 @@ namespace Barotrauma.Networking if (shuttle != null) { GameMain.NetLobbyScreen.SelectedShuttle = shuttle; } } - List campaignSubs = new List(); - if (serverSettings.CampaignSubmarines != null && serverSettings.CampaignSubmarines.Length > 0) - { - string[] submarines = serverSettings.CampaignSubmarines.Split(ServerSettings.SubmarineSeparatorChar); - for (int i = 0; i < submarines.Length; i++) - { - SubmarineInfo subInfo = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == submarines[i]); - if (subInfo != null && subInfo.IsCampaignCompatible) - { - campaignSubs.Add(subInfo); - } - } - } - else - { - // Add vanilla submarines by default - for (int i = 0; i < SubmarineInfo.SavedSubmarines.Count(); i++) - { - SubmarineInfo subInfo = SubmarineInfo.SavedSubmarines.ElementAt(i); - if (subInfo.IsVanillaSubmarine() && subInfo.IsCampaignCompatible) - { - campaignSubs.Add(SubmarineInfo.SavedSubmarines.ElementAt(i)); - } - } - } - GameMain.NetLobbyScreen.CampaignSubmarines = campaignSubs; - started = true; GameAnalyticsManager.AddDesignEvent("GameServer:Start"); @@ -763,7 +736,15 @@ namespace Barotrauma.Networking } break; case ClientPacketHeader.REQUEST_STARTGAMEFINALIZE: - if (gameStarted && connectedClient != null) + if (connectedClient == null) + { + DebugConsole.AddWarning("Received a REQUEST_STARTGAMEFINALIZE message. Client not connected, ignoring the message."); + } + else if (!gameStarted) + { + DebugConsole.AddWarning("Received a REQUEST_STARTGAMEFINALIZE message. Game not started, ignoring the message."); + } + else { SendRoundStartFinalize(connectedClient); } @@ -783,7 +764,7 @@ namespace Barotrauma.Networking string seed = inc.ReadString(); string subName = inc.ReadString(); string subHash = inc.ReadString(); - CampaignSettings settings = new CampaignSettings(inc); + CampaignSettings settings = new CampaignSettings(inc); var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.Hash == subHash); @@ -806,6 +787,7 @@ namespace Barotrauma.Networking { ServerSettings.RadiationEnabled = settings.RadiationEnabled; ServerSettings.MaxMissionCount = settings.MaxMissionCount; + ServerSettings.SaveSettings(); MultiPlayerCampaign.StartNewCampaign(localSavePath, matchingSub.FilePath, seed, settings); } } @@ -845,6 +827,9 @@ namespace Barotrauma.Networking case ClientPacketHeader.CREW: ReadCrewMessage(inc, connectedClient); break; + case ClientPacketHeader.MEDICAL: + ReadMedicalMessage(inc, connectedClient); + break; case ClientPacketHeader.READY_CHECK: ReadyCheck.ServerRead(inc, connectedClient); break; @@ -885,6 +870,12 @@ namespace Barotrauma.Networking case ClientNetError.MISSING_ENTITY: UInt16 eventID = inc.ReadUInt16(); UInt16 entityID = inc.ReadUInt16(); + byte subCount = inc.ReadByte(); + List subNames = new List(); + for (int i = 0; i < subCount; i++) + { + subNames.Add(inc.ReadString()); + } Entity entity = Entity.FindEntityByID(entityID); if (entity == null) { @@ -892,16 +883,23 @@ namespace Barotrauma.Networking } else if (entity is Character character) { - errorStr = "Missing character " + character.Name + " (event id " + eventID.ToString() + ", entity id " + entityID.ToString() + ")."; - errorStrNoName = "Missing character " + character.SpeciesName + "(event id " + eventID.ToString() + ", entity id " + entityID.ToString() + ")."; + errorStr = $"Missing character {character.Name} (event id {eventID}, entity id {entityID})."; + errorStrNoName = $"Missing character {character.SpeciesName} (event id {eventID}, entity id {entityID})."; } else if (entity is Item item) { - errorStr = errorStrNoName = "Missing item " + item.Name + " (event id " + eventID.ToString() + ", entity id " + entityID.ToString() + ")."; + errorStr = errorStrNoName = $"Missing item {item.Name}, sub: {item.Submarine?.Info?.Name ?? "none"} (event id {eventID}, entity id {entityID})."; } else { - errorStr = errorStrNoName = "Missing entity " + entity.ToString() + " (event id " + eventID.ToString() + ", entity id " + entityID.ToString() + ")."; + errorStr = errorStrNoName = $"Missing entity {entity}, sub: {entity.Submarine?.Info?.Name ?? "none"} (event id {eventID}, entity id {entityID})."; + } + var serverSubNames = Submarine.Loaded.Select(s => s.Info.Name); + if (subCount != Submarine.Loaded.Count || !subNames.SequenceEqual(serverSubNames)) + { + string subErrorStr = $" Loaded submarines don't match (client: {string.Join(", ", subNames)}, server: {string.Join(", ", serverSubNames)})."; + errorStr += subErrorStr; + errorStrNoName += subErrorStr; } break; } @@ -1247,6 +1245,14 @@ namespace Barotrauma.Networking } } + private void ReadMedicalMessage(IReadMessage inc, Client sender) + { + if (GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign) + { + mpCampaign.MedicalClinic.ServerRead(inc, sender); + } + } + private void ReadReadyToSpawnMessage(IReadMessage inc, Client sender) { sender.SpectateOnly = inc.ReadBoolean() && (serverSettings.AllowSpectating || sender.Connection == OwnerConnection); @@ -1873,27 +1879,10 @@ namespace Barotrauma.Networking } outmsg.Write(GameMain.NetLobbyScreen.SelectedSub.Name); outmsg.Write(GameMain.NetLobbyScreen.SelectedSub.MD5Hash.ToString()); - outmsg.Write(serverSettings.UseRespawnShuttle); - outmsg.Write(GameMain.NetLobbyScreen.SelectedShuttle.Name); - outmsg.Write(GameMain.NetLobbyScreen.SelectedShuttle.MD5Hash.ToString()); - - List campaignSubIndices = new List(); - if (GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign) - { - IReadOnlyList subList = GameMain.NetLobbyScreen.GetSubList(); - for (int i = 0; i < subList.Count; i++) - { - if (GameMain.NetLobbyScreen.CampaignSubmarines.Contains(subList[i])) - { - campaignSubIndices.Add(i); - } - } - } - outmsg.Write((UInt16)campaignSubIndices.Count); - foreach (int campaignSubIndex in campaignSubIndices) - { - outmsg.Write((UInt16)campaignSubIndex); - } + outmsg.Write(serverSettings.UseRespawnShuttle || (gameStarted && respawnManager.UsingShuttle)); + var selectedShuttle = gameStarted && respawnManager.UsingShuttle ? respawnManager.RespawnShuttle.Info : GameMain.NetLobbyScreen.SelectedShuttle; + outmsg.Write(selectedShuttle.Name); + outmsg.Write(selectedShuttle.MD5Hash.ToString()); outmsg.Write(serverSettings.Voting.AllowSubVoting); outmsg.Write(serverSettings.Voting.AllowModeVoting); @@ -1954,7 +1943,7 @@ namespace Barotrauma.Networking int chatMessageBytes = outmsg.LengthBytes; WriteChatMessages(outmsg, c); - chatMessageBytes = outmsg.LengthBytes - outmsg.LengthBytes; + chatMessageBytes = outmsg.LengthBytes - chatMessageBytes; outmsg.Write((byte)ServerNetObject.END_OF_MESSAGE); @@ -1977,7 +1966,11 @@ namespace Barotrauma.Networking warningMsg += " Settings buffer size: " + settingsBuf.LengthBytes + " bytes\n"; } - if (GameSettings.VerboseLogging) { DebugConsole.AddWarning(warningMsg); } +#if DEBUG || UNSTABLE + DebugConsole.ThrowError(warningMsg); +#else + if (GameSettings.VerboseLogging) { DebugConsole.AddWarning(warningMsg); } +#endif GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame1:ClientWriteLobby" + outmsg.LengthBytes, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg); } @@ -1993,12 +1986,16 @@ namespace Barotrauma.Networking //these large initial messages until the client acknowledges receiving them c.LastRecvLobbyUpdate++; - SendVoteStatus(new List() { c }); } else { serverPeer.Send(outmsg, c.Connection, DeliveryMethod.Unreliable); } + + if (isInitialUpdate) + { + SendVoteStatus(new List() { c }); + } } private void WriteChatMessages(IWriteMessage outmsg, Client c) @@ -2064,7 +2061,7 @@ namespace Barotrauma.Networking msg.Write(selectedSub.Name); msg.Write(selectedSub.MD5Hash.Hash); - msg.Write(serverSettings.UseRespawnShuttle); + msg.Write(serverSettings.UseRespawnShuttle || (gameStarted && respawnManager.UsingShuttle)); msg.Write(selectedShuttle.Name); msg.Write(selectedShuttle.MD5Hash.Hash); @@ -2182,7 +2179,6 @@ namespace Barotrauma.Networking else { SendStartMessage(roundStartSeed, GameMain.NetLobbyScreen.LevelSeed, GameMain.GameSession, connectedClients, false); - GameMain.GameSession.StartRound(GameMain.NetLobbyScreen.LevelSeed, serverSettings.SelectedLevelDifficulty); Log("Game mode: " + selectedMode.Name, ServerLog.MessageType.ServerMessage); Log("Submarine: " + selectedSub.Name, ServerLog.MessageType.ServerMessage); @@ -2202,8 +2198,7 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Failure; } - MissionMode missionMode = GameMain.GameSession.GameMode as MissionMode; - bool missionAllowRespawn = missionMode == null || !missionMode.Missions.Any(m => !m.AllowRespawn); + bool missionAllowRespawn = !(GameMain.GameSession.GameMode is MissionMode missionMode) || !missionMode.Missions.Any(m => !m.AllowRespawn); bool isOutpost = campaign != null && campaign.NextLevel?.Type == LevelData.LevelType.Outpost; if (serverSettings.AllowRespawn && missionAllowRespawn) @@ -2281,7 +2276,7 @@ namespace Barotrauma.Networking characterInfos.Add(client.CharacterInfo); if (client.CharacterInfo.Job == null || client.CharacterInfo.Job.Prefab != client.AssignedJob.First) { - client.CharacterInfo.Job = new Job(client.AssignedJob.First, client.AssignedJob.Second); + client.CharacterInfo.Job = new Job(client.AssignedJob.First, Rand.RandSync.Unsynced, client.AssignedJob.Second); } } @@ -2498,7 +2493,7 @@ namespace Barotrauma.Networking msg.Write(serverSettings.LockAllDefaultWires); msg.Write(serverSettings.AllowRagdollButton); msg.Write(serverSettings.AllowLinkingWifiToChat); - msg.Write(serverSettings.UseRespawnShuttle); + msg.Write(serverSettings.UseRespawnShuttle || (gameStarted && respawnManager.UsingShuttle)); msg.Write((byte)serverSettings.LosMode); msg.Write(includesFinalize); msg.WritePadBits(); @@ -2510,8 +2505,9 @@ namespace Barotrauma.Networking msg.Write(serverSettings.SelectedLevelDifficulty); msg.Write(gameSession.SubmarineInfo.Name); msg.Write(gameSession.SubmarineInfo.MD5Hash.Hash); - msg.Write(GameMain.NetLobbyScreen.SelectedShuttle.Name); - msg.Write(GameMain.NetLobbyScreen.SelectedShuttle.MD5Hash.Hash); + var selectedShuttle = gameStarted && respawnManager.UsingShuttle ? respawnManager.RespawnShuttle.Info : GameMain.NetLobbyScreen.SelectedShuttle; + msg.Write(selectedShuttle.Name); + msg.Write(selectedShuttle.MD5Hash.Hash); msg.Write((byte)GameMain.GameSession.GameMode.Missions.Count()); foreach (Mission mission in GameMain.GameSession.GameMode.Missions) { @@ -2966,9 +2962,9 @@ namespace Barotrauma.Networking SendDirectChatMessage(msg, recipient); } - public void SendConsoleMessage(string txt, Client recipient) + public void SendConsoleMessage(string txt, Client recipient, Color? color = null) { - ChatMessage msg = ChatMessage.Create("", txt, ChatMessageType.Console, null); + ChatMessage msg = ChatMessage.Create("", txt, ChatMessageType.Console, sender: null, textColor: color); SendDirectChatMessage(msg, recipient); } @@ -3219,11 +3215,11 @@ namespace Barotrauma.Networking //too far to hear the msg -> don't send if (!client.Character.CanHearCharacter(message.Sender)) { continue; } } - SendDirectChatMessage(new OrderChatMessage(message.Order, message.OrderOption, message.OrderPriority, message.TargetEntity, message.TargetCharacter, message.Sender), client); + SendDirectChatMessage(new OrderChatMessage(message.Order, message.OrderOption, message.OrderPriority, message.TargetEntity, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder), client); } if (!string.IsNullOrWhiteSpace(message.Text)) { - AddChatMessage(new OrderChatMessage(message.Order, message.OrderOption, message.OrderPriority, message.Text, message.TargetEntity, message.TargetCharacter, message.Sender)); + AddChatMessage(new OrderChatMessage(message.Order, message.OrderOption, message.OrderPriority, message.Text, message.TargetEntity, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder)); } } @@ -3418,14 +3414,40 @@ namespace Barotrauma.Networking } } + public void IncrementStat(Character character, string achievementIdentifier, int amount) + { + achievementIdentifier = achievementIdentifier.ToLowerInvariant(); + foreach (Client client in connectedClients) + { + if (client.Character == character) + { + IncrementStat(client, achievementIdentifier, amount); + return; + } + } + } + public void GiveAchievement(Client client, string achievementIdentifier) { - if (client.GivenAchievements.Contains(achievementIdentifier)) return; + if (client.GivenAchievements.Contains(achievementIdentifier)) { return; } client.GivenAchievements.Add(achievementIdentifier); IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ServerPacketHeader.ACHIEVEMENT); msg.Write(achievementIdentifier); + msg.Write(0); + + serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); + } + + public void IncrementStat(Client client, string achievementIdentifier, int amount) + { + if (client.GivenAchievements.Contains(achievementIdentifier)) { return; } + + IWriteMessage msg = new WriteOnlyMessage(); + msg.Write((byte)ServerPacketHeader.ACHIEVEMENT); + msg.Write(achievementIdentifier); + msg.Write(amount); serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } @@ -3779,7 +3801,7 @@ namespace Barotrauma.Networking if (assignedPlayerCount[jobPrefab] >= jobPrefab.MaxNumber) { continue; } var variant = Rand.Range(0, jobPrefab.Variants, Rand.RandSync.Server); - unassignedBots[0].Job = new Job(jobPrefab, variant); + unassignedBots[0].Job = new Job(jobPrefab, Rand.RandSync.Server, variant); assignedPlayerCount[jobPrefab]++; unassignedBots.Remove(unassignedBots[0]); canAssign = true; @@ -3802,7 +3824,7 @@ namespace Barotrauma.Networking { var job = remainingJobs.GetRandom(); var variant = Rand.Range(0, job.Variants); - c.Job = new Job(job, variant); + c.Job = new Job(job, Rand.RandSync.Unsynced, variant); assignedPlayerCount[c.Job.Prefab]++; } } @@ -3891,16 +3913,6 @@ namespace Barotrauma.Networking if (GameMain.NetLobbyScreen.SelectedSub != null) { serverSettings.SelectedSubmarine = GameMain.NetLobbyScreen.SelectedSub.Name; } if (GameMain.NetLobbyScreen.SelectedShuttle != null) { serverSettings.SelectedShuttle = GameMain.NetLobbyScreen.SelectedShuttle.Name; } - if (GameMain.NetLobbyScreen.CampaignSubmarines != null) - { - string submarinesString = string.Empty; - for (int i = 0; i < GameMain.NetLobbyScreen.CampaignSubmarines.Count; i++) - { - submarinesString += GameMain.NetLobbyScreen.CampaignSubmarines[i].Name + ServerSettings.SubmarineSeparatorChar; - } - submarinesString.Trim(ServerSettings.SubmarineSeparatorChar); - serverSettings.CampaignSubmarines = submarinesString; - } serverSettings.SaveSettings(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index 520d5e4b4..f31a8b099 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -118,14 +118,18 @@ namespace Barotrauma.Networking return ShouldStartRespawnCountdown(characterToRespawnCount); } + private int GetMinCharactersToRespawn() + { + return Math.Max((int)(GameMain.Server.ConnectedClients.Count * GameMain.Server.ServerSettings.MinRespawnRatio), 1); + } + private bool ShouldStartRespawnCountdown(int characterToRespawnCount) { if (GameMain.Lua.game.overrideRespawnSub) { characterToRespawnCount = 0; } - int totalCharacterCount = GameMain.Server.ConnectedClients.Count; - return (float)characterToRespawnCount >= Math.Max((float)totalCharacterCount * GameMain.Server.ServerSettings.MinRespawnRatio, 1.0f); + return characterToRespawnCount >= GetMinCharactersToRespawn(); } partial void UpdateWaiting(float deltaTime) @@ -139,7 +143,7 @@ namespace Barotrauma.Networking } pendingRespawnCount = GetClientsToRespawn().Count(); - requiredRespawnCount = (int)Math.Max((float)GameMain.Server.ConnectedClients.Count * GameMain.Server.ServerSettings.MinRespawnRatio, 1.0f); + requiredRespawnCount = GetMinCharactersToRespawn(); if (pendingRespawnCount != prevPendingRespawnCount || requiredRespawnCount != prevRequiredRespawnCount) { @@ -372,7 +376,7 @@ namespace Barotrauma.Networking { if (campaign?.GetClientCharacterData(c) == null || c.CharacterInfo.Job == null) { - c.CharacterInfo.Job = new Job(c.AssignedJob.First, c.AssignedJob.Second); + c.CharacterInfo.Job = new Job(c.AssignedJob.First, Rand.RandSync.Unsynced, c.AssignedJob.Second); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index cb0a2582f..9f9e10a7c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -11,6 +11,21 @@ namespace Barotrauma.Networking { partial class ServerSettings { + partial class NetPropertyData + { + private object lastSyncedValue; + public UInt16 LastUpdateID { get; private set; } + + public void SyncValue() + { + if (!PropEquals(lastSyncedValue, Value)) + { + LastUpdateID = (UInt16)(GameMain.NetLobbyScreen.LastUpdateID); + lastSyncedValue = Value; + } + } + } + public static readonly string ClientPermissionsFile = "Data" + Path.DirectorySeparatorChar + "clientpermissions.xml"; public static readonly char SubmarineSeparatorChar = '|'; @@ -35,20 +50,25 @@ namespace Barotrauma.Networking LoadClientPermissions(); } - private void WriteNetProperties(IWriteMessage outMsg) + private void WriteNetProperties(IWriteMessage outMsg, Client c) { - outMsg.Write((UInt16)netProperties.Keys.Count); foreach (UInt32 key in netProperties.Keys) { - outMsg.Write(key); - netProperties[key].Write(outMsg); + var property = netProperties[key]; + property.SyncValue(); + if (property.LastUpdateID > c.LastRecvLobbyUpdate) + { + outMsg.Write(key); + netProperties[key].Write(outMsg); + } } + outMsg.Write((UInt32)0); } public void ServerAdminWrite(IWriteMessage outMsg, Client c) { c.LastSentServerSettingsUpdate = LastPropertyUpdateId; - WriteNetProperties(outMsg); + WriteNetProperties(outMsg, c); WriteMonsterEnabled(outMsg); BanList.ServerAdminWrite(outMsg, c); Whitelist.ServerAdminWrite(outMsg, c); @@ -79,8 +99,11 @@ namespace Barotrauma.Networking { WriteExtraCargo(outMsg); } - - WriteHiddenSubs(outMsg); + + if (requiredFlags.HasFlag(NetFlags.HiddenSubs)) + { + WriteHiddenSubs(outMsg); + } if (c.HasPermission(Networking.ClientPermissions.ManageSettings) && !NetIdUtils.IdMoreRecentOrMatches(c.LastRecvServerSettingsUpdate, LastPropertyUpdateId)) @@ -164,6 +187,7 @@ namespace Barotrauma.Networking { ReadHiddenSubs(incMsg); changed |= true; + UpdateFlag(NetFlags.HiddenSubs); } if (flags.HasFlag(NetFlags.Misc)) @@ -199,11 +223,6 @@ namespace Barotrauma.Networking AutoRestart = autoRestart; } - RadiationEnabled = incMsg.ReadBoolean(); - - int maxMissionCount = MaxMissionCount + incMsg.ReadByte() - 1; - MaxMissionCount = MathHelper.Clamp(maxMissionCount, CampaignSettings.MinMissionCountLimit, CampaignSettings.MaxMissionCountLimit); - changed |= true; UpdateFlag(NetFlags.Misc); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs index e795f0ace..6c211062f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs @@ -32,26 +32,6 @@ namespace Barotrauma set { selectedShuttle = value; lastUpdateID++; } } - [Obsolete("TODO: this list shouldn't exist, the client should just use the visible subs list instead")] - public List CampaignSubmarines - { - get - { - return campaignSubmarines; - } - set - { - campaignSubmarines = value; - lastUpdateID++; - if (GameMain.NetworkMember?.ServerSettings != null) - { - GameMain.NetworkMember.ServerSettings.ServerDetailsChanged = true; - } - } - } - - private List campaignSubmarines; - public GameModePreset[] GameModes { get; } private int selectedModeIndex; diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index ee5f619ca..207a2fa77 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.15.23.0 + 0.16.6.1 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml index d33456484..da4c9c36c 100644 --- a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml +++ b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml @@ -77,8 +77,8 @@ + - @@ -113,6 +113,7 @@ + @@ -121,7 +122,9 @@ + + @@ -144,6 +147,9 @@ + + + diff --git a/Barotrauma/BarotraumaShared/Data/karmasettings.xml b/Barotrauma/BarotraumaShared/Data/karmasettings.xml index 7467a8269..5a78125cb 100644 --- a/Barotrauma/BarotraumaShared/Data/karmasettings.xml +++ b/Barotrauma/BarotraumaShared/Data/karmasettings.xml @@ -6,8 +6,8 @@ karmadecaythreshold="50" karmaincrease="0.05" karmaincreasethreshold="50" - structurerepairkarmaincrease="0.01" - structuredamagekarmadecrease="0.05" + structurerepairkarmaincrease="0.005" + structuredamagekarmadecrease="0.025" itemrepairkarmaincrease="0.03" reactoroverheatkarmadecrease="0.5" reactormeltdownkarmadecrease="30" @@ -35,8 +35,8 @@ karmadecaythreshold="50" karmaincrease="0.04" karmaincreasethreshold="45" - structurerepairkarmaincrease="0.01" - structuredamagekarmadecrease="0.2" + structurerepairkarmaincrease="0.005" + structuredamagekarmadecrease="0.1" itemrepairkarmaincrease="0.03" reactoroverheatkarmadecrease="1.0" reactormeltdownkarmadecrease="35" diff --git a/Barotrauma/BarotraumaShared/README.txt b/Barotrauma/BarotraumaShared/README.txt index a71341163..fe1f66f2a 100644 --- a/Barotrauma/BarotraumaShared/README.txt +++ b/Barotrauma/BarotraumaShared/README.txt @@ -2,8 +2,8 @@ http://www.barotraumagame.com -© 2018-2020 FakeFish Ltd. All rights reserved. -© 2019-2020 Daedalic Entertainment GmbH. The Daedalic logo is a trademark of Daedalic Entertainment GmbH, Germany. All rights reserved. +© 2018-2022 FakeFish Ltd. All rights reserved. +© 2019-2022 Daedalic Entertainment GmbH. The Daedalic logo is a trademark of Daedalic Entertainment GmbH, Germany. All rights reserved. Privacy policy: http://privacypolicy.daedalic.com See the wiki for more detailed info and instructions: diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 0500a03c8..4b2dc74dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -150,7 +150,7 @@ namespace Barotrauma private CoroutineHandle disableTailCoroutine; - private readonly IEnumerable myBodies; + private readonly List myBodies; public LatchOntoAI LatchOntoAI { get; private set; } public SwarmBehavior SwarmBehavior { get; private set; } @@ -207,8 +207,10 @@ namespace Barotrauma } = new HashSet(); public bool IsTargetingPlayerTeam => IsTargetInPlayerTeam(SelectedAiTarget); - public bool IsBeingChasedBy(Character c) => c.AIController is EnemyAIController enemyAI && enemyAI.SelectedAiTarget?.Entity is Character && (enemyAI.State == AIState.Aggressive || enemyAI.State == AIState.Attack); - private bool IsBeingChased => SelectedAiTarget?.Entity is Character targetCharacter && IsBeingChasedBy(targetCharacter); + public static bool IsTargetBeingChasedBy(Character target, Character character) + => character?.AIController is EnemyAIController enemyAI && enemyAI.SelectedAiTarget?.Entity == target && (enemyAI.State == AIState.Attack || enemyAI.State == AIState.Aggressive); + public bool IsBeingChasedBy(Character c) => IsTargetBeingChasedBy(Character, c); + private bool IsBeingChased => IsBeingChasedBy(SelectedAiTarget?.Entity as Character); private bool IsTargetInPlayerTeam(AITarget target) => target?.Entity?.Submarine != null && target.Entity.Submarine.Info.IsPlayer || target?.Entity is Character targetCharacter && targetCharacter.IsOnPlayerTeam; @@ -306,7 +308,8 @@ namespace Barotrauma requiredHoleCount = (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderWidth) / Structure.WallSectionSize); - myBodies = Character.AnimController.Limbs.Select(l => l.body.FarseerBody); + myBodies = Character.AnimController.Limbs.Select(l => l.body.FarseerBody).ToList(); + myBodies.Add(Character.AnimController.Collider.FarseerBody); } private CharacterParams.AIParams _aiParams; @@ -339,7 +342,7 @@ namespace Barotrauma { targetingTag = "dead"; } - else if (AIParams.TryGetTarget(targetCharacter.CharacterHealth.GetActiveAfflictionTags(), out CharacterParams.TargetParams tp) && tp.Threshold > Character.GetDamageDoneByAttacker(targetCharacter)) + else if (AIParams.TryGetTarget(targetCharacter.CharacterHealth.GetActiveAfflictionTags(), out CharacterParams.TargetParams tp) && tp.Threshold >= Character.GetDamageDoneByAttacker(targetCharacter)) { targetingTag = tp.Tag; } @@ -530,8 +533,7 @@ namespace Barotrauma selectedTargetingParams = targetingParams; State = targetingParams.State; } - if (SelectedAiTarget?.Entity != null && - (LatchOntoAI == null || !LatchOntoAI.IsAttached || wallTarget != null) && + if ((LatchOntoAI == null || !LatchOntoAI.IsAttached || wallTarget != null) && (State == AIState.Attack || State == AIState.Aggressive || State == AIState.PassiveAggressive)) { UpdateWallTarget(requiredHoleCount); @@ -645,7 +647,7 @@ namespace Barotrauma } else { - run = isBeingChased ? true : squaredDistance < Math.Pow(halfReactDistance, 2); + run = isBeingChased || squaredDistance < Math.Pow(halfReactDistance, 2); State = AIState.Escape; avoidTimer = AIParams.AvoidTime * 0.5f * Rand.Range(0.75f, 1.25f); } @@ -673,15 +675,21 @@ namespace Barotrauma Character c = a.Character; if (c.IsDead || c.Removed) { return false; } if (!Character.IsFriendly(c)) { return true; } - // Only apply the threshold to friendly characters + if (!c.IsPlayer) { return false; } + // Only apply the threshold to players return a.Damage >= selectedTargetingParams.Threshold; } Character attacker = targetCharacter.LastAttackers.LastOrDefault(IsValid)?.Character; - if (attacker != null) + //if the attacker has the same targeting tag as the character we're protecting, we can't change the TargetState + //otherwise e.g. a pet that's set to follow humans would start attacking all humans (and other pets, since they're considered part of the same group) when a hostile human attacks it + //TODO: a way for pets to differentiate hostile and friendly humans? + if (attacker?.AiTarget != null && !targetCharacter.SpeciesName.Equals(GetTargetingTag(attacker.AiTarget), StringComparison.OrdinalIgnoreCase)) { // Attack the character that attacked the target we are protecting ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2); SelectTarget(attacker.AiTarget); + State = AIState.Attack; + UpdateWallTarget(requiredHoleCount); return; } } @@ -1316,7 +1324,7 @@ namespace Barotrauma Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 2); Body closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true); if (Submarine.LastPickedFraction != 1.0f && closestBody != null && - (!AIParams.TargetOuterWalls || !canAttackWalls && closestBody.UserData is Structure s && s.Submarine != null || !canAttackDoors && closestBody.UserData is Item i && i.Submarine != null && i.GetComponent() != null)) + ((!AIParams.TargetOuterWalls || !canAttackWalls) && closestBody.UserData is Structure s && s.Submarine != null || !canAttackDoors && closestBody.UserData is Item i && i.Submarine != null && i.GetComponent() != null)) { // Target is unreachable, there's a door or wall ahead State = AIState.Idle; @@ -1597,7 +1605,7 @@ namespace Barotrauma } else { - sweepTimer = Rand.Range(-1000, 1000) * selectedTargetingParams.SweepSpeed; + sweepTimer = Rand.Range(-1000f, 1000f) * selectedTargetingParams.SweepSpeed; } } break; @@ -1837,7 +1845,7 @@ namespace Barotrauma if (!attack.IsValidTarget(target)) { return false; } if (target is ISerializableEntity se && target is Character) { - if (attack.Conditionals.Any(c => !c.Matches(se))) { return false; } + if (attack.Conditionals.Any(c => !c.TargetSelf && !c.Matches(se))) { return false; } } if (attack.Conditionals.Any(c => c.TargetSelf && !c.Matches(Character))) { return false; } if (attack.Ranged) @@ -2182,10 +2190,22 @@ namespace Barotrauma float margin = MathHelper.PiOver4 * distanceFactor; if (angle < margin) { - var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel; - var pickedBody = Submarine.PickBody(weapon.SimPosition, target.SimPosition, myBodies, collisionCategories, allowInsideFixture: true); + var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel; + var pickedBody = Submarine.PickBody(weapon.SimPosition, Character.GetRelativeSimPosition(target), myBodies, collisionCategories, allowInsideFixture: true); if (pickedBody != null) { + if (target is MapEntity) + { + if (pickedBody.UserData is Submarine sub && sub == target.Submarine) + { + return true; + } + else if (target == pickedBody.UserData) + { + return true; + } + } + Character t = null; if (pickedBody.UserData is Character c) { @@ -2254,6 +2274,10 @@ namespace Barotrauma if (SelectedAiTarget == null || SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed) { State = AIState.Idle; + if (Character.SelectedCharacter != null) + { + Character.DeselectCharacter(); + } return; } if (SelectedAiTarget.Entity is Character || SelectedAiTarget.Entity is Item) @@ -2269,7 +2293,16 @@ namespace Barotrauma Vector2 attackSimPosition = Character.GetRelativeSimPosition(SelectedAiTarget.Entity); Vector2 limbDiff = attackSimPosition - mouthPos; float extent = Math.Max(mouthLimb.body.GetMaxExtent(), 2); - if (limbDiff.LengthSquared() < extent * extent) + bool tooFar = Character.InWater ? limbDiff.LengthSquared() > extent * extent : limbDiff.X > extent; + if (tooFar) + { + steeringManager.SteeringSeek(attackSimPosition - (mouthPos - SimPosition), 2); + if (Character.InWater) + { + SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15); + } + } + else { if (SelectedAiTarget.Entity is Character targetCharacter) { @@ -2285,14 +2318,12 @@ namespace Barotrauma { item.body.LinearVelocity *= 0.9f; item.body.LinearVelocity -= limbDiff * 0.25f; - bool wasBroken = item.Condition <= 0.0f; - - item.AddDamage(Character, item.WorldPosition, new Attack(0.0f, 0.0f, 0.0f, 0.0f, 0.1f), deltaTime); - + item.AddDamage(Character, item.WorldPosition, new Attack(0.0f, 0.0f, 0.0f, 0.0f, 0.02f * Character.Params.EatingSpeed), deltaTime); + Character.ApplyStatusEffects(ActionType.OnEating, deltaTime); if (item.Condition <= 0.0f) { - if (!wasBroken) { PetBehavior?.OnEat(item.GetTags(), 1.0f); } + if (!wasBroken) { PetBehavior?.OnEat(item); } Entity.Spawner.AddToRemoveQueue(item); } } @@ -2301,14 +2332,6 @@ namespace Barotrauma steeringManager.SteeringManual(deltaTime, Vector2.Normalize(limbDiff) * 3); Character.AnimController.Collider.ApplyForce(limbDiff * mouthLimb.Mass * 50.0f, mouthPos); } - else - { - steeringManager.SteeringSeek(attackSimPosition - (mouthPos - SimPosition), 2); - if (Character.AnimController.InWater) - { - SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15); - } - } } else { @@ -2364,6 +2387,24 @@ namespace Barotrauma } #region Targeting + public static bool IsLatchedTo(Character target, Character character) + { + if (target.AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null) + { + return enemyAI.LatchOntoAI.IsAttached && enemyAI.LatchOntoAI.TargetCharacter == character; + } + return false; + } + + public static bool IsLatchedToSomeoneElse(Character target, Character character) + { + if (target.AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null) + { + return enemyAI.LatchOntoAI.IsAttached && enemyAI.LatchOntoAI.TargetCharacter != null && enemyAI.LatchOntoAI.TargetCharacter != character; + } + return false; + } + private bool IsLatchedOnSub => LatchOntoAI != null && LatchOntoAI.IsAttachedToSub; //goes through all the AItargets, evaluates how preferable it is to attack the target, @@ -2376,7 +2417,8 @@ namespace Barotrauma selectedTargetMemory = null; targetingParams = null; bool isAnyTargetClose = false; - + bool isBeingChased = IsBeingChased; + float maxModifier = 5; foreach (AITarget aiTarget in AITarget.List) { if (aiTarget.InDetectable) { continue; } @@ -2499,11 +2541,12 @@ namespace Barotrauma // Ignore inner walls when outside (walltargets still work) continue; } - valueModifier = 1; if (!Character.AnimController.CanEnterSubmarine && IsWallDisabled(s)) { continue; } + // Prefer weaker walls (200 is the default for normal hull walls) + valueModifier = 200f / s.MaxHealth; for (int i = 0; i < s.Sections.Length; i++) { var section = s.Sections[i]; @@ -2515,12 +2558,12 @@ namespace Barotrauma { if (CanPassThroughHole(s, i)) { - valueModifier *= leadsInside ? (IsAggressiveBoarder ? 3 : 1) : 0; + valueModifier *= leadsInside ? (IsAggressiveBoarder ? maxModifier : 1) : 0; } - else if (IsAggressiveBoarder && leadsInside && canAttackWalls && AIParams.TargetOuterWalls) + else if (IsAggressiveBoarder && leadsInside && canAttackWalls) { - // Up to 25% priority increase for every gap in the wall when an aggressive boarder is outside - valueModifier *= 1 + section.gap.Open * 0.25f; + // Up to 100% priority increase for every gap in the wall when an aggressive boarder is outside + valueModifier *= 1 + section.gap.Open; } } else @@ -2558,6 +2601,7 @@ namespace Barotrauma // We are actually interested in breaking things -> reduce the priority when the wall is already broken // (Terminalcells) valueModifier *= 1 - section.gap.Open * 0.25f; + valueModifier = Math.Max(valueModifier, 0.1f); } } } @@ -2577,6 +2621,7 @@ namespace Barotrauma valueModifier *= 1 + section.gap.Open; } } + valueModifier = Math.Clamp(valueModifier, 0, maxModifier); } } if (door != null) @@ -2588,7 +2633,7 @@ namespace Barotrauma bool isOpen = door.CanBeTraversed; if (!isOpen) { - if (!canAttackDoors || isOutdoor && !AIParams.TargetOuterWalls) { continue; } + if (!canAttackDoors) { continue; } } else if (!Character.AnimController.CanEnterSubmarine) { @@ -2602,11 +2647,11 @@ namespace Barotrauma // Increase the priority if the character is outside and the door is from outside to inside if (door.CanBeTraversed) { - valueModifier = 3; + valueModifier = maxModifier; } else if (door.LinkedGap != null) { - valueModifier = 1 + door.LinkedGap.Open; + valueModifier = 1 + door.LinkedGap.Open * (maxModifier - 1); } } else @@ -2658,6 +2703,10 @@ namespace Barotrauma } } } + if (targetParams.State == AIState.Eat && Character.Params.Health.HealthRegenerationWhenEating > 0) + { + valueModifier *= MathHelper.Lerp(1f, 0.1f, Character.HealthPercentage / 100f); + } valueModifier *= targetParams.Priority; if (valueModifier == 0.0f) { continue; } if (targetingTag != "decoy") @@ -2701,12 +2750,55 @@ namespace Barotrauma if (SelectedAiTarget == aiTarget) { + if (Character.Submarine == null && aiTarget.Entity is ISpatialEntity spatialEntity && spatialEntity.Submarine != null) + { + if (targetingTag == "door" || targetingTag == "wall") + { + Vector2 rayStart = Character.SimPosition; + Vector2 rayEnd = aiTarget.SimPosition + spatialEntity.Submarine.SimPosition; + Body closestBody = Submarine.PickBody(rayStart, rayEnd, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel, allowInsideFixture: true); + if (closestBody != null && closestBody.UserData is ISpatialEntity hit) + { + Vector2 hitPos = hit.SimPosition; + if (closestBody.UserData is Submarine) + { + hitPos = Submarine.LastPickedPosition; + } + else if (hit.Submarine != null) + { + hitPos += hit.Submarine.SimPosition; + } + float subHalfWidth = spatialEntity.Submarine.Borders.Width / 2; + float subHalfHeight = spatialEntity.Submarine.Borders.Height / 2; + Vector2 diff = ConvertUnits.ToDisplayUnits(rayEnd - hitPos); + bool isOtherSideOfTheSub = Math.Abs(diff.X) > subHalfWidth || Math.Abs(diff.Y) > subHalfHeight; + if (isOtherSideOfTheSub) + { + IgnoreTarget(aiTarget); + ResetAITarget(); + continue; + } + } + } + } // Stick to the current target valueModifier *= 1.1f; } + if (!isBeingChased) + { + if (targetParams.State == AIState.Avoid || targetParams.State == AIState.PassiveAggressive || targetParams.State == AIState.Aggressive) + { + float reactDistance = targetParams.ReactDistance; + if (reactDistance > 0 && reactDistance < dist) + { + // The target is too far and should be ignored. + continue; + } + } + } //if the target is very close, the distance doesn't make much difference - // -> just ignore the distance and attack whatever has the highest priority + // -> just ignore the distance and target whatever has the highest priority dist = Math.Max(dist, 100.0f); AITargetMemory targetMemory = GetTargetMemory(aiTarget, addIfNotFound: true); if (Character.Submarine != null && !Character.Submarine.Info.IsRuin && Character.CurrentHull != null) @@ -2719,19 +2811,22 @@ namespace Barotrauma } } - if (targetParams.AttackPattern == AttackPattern.Circle) + if (Character.Submarine == null && aiTarget.Entity?.Submarine != null && targetCharacter == null) { - if (Character.Submarine == null && aiTarget.Entity?.Submarine != null && !isAnyTargetClose) + if (targetParams.AttackPattern == AttackPattern.Circle || targetParams.AttackPattern == AttackPattern.Sweep) { - if (Submarine.MainSubs.Contains(aiTarget.Entity.Submarine)) + if (!isAnyTargetClose) { - // Prioritize targets that are near the horizontal center of the sub, but only when none of the targets is reachable. - float horizontalDistanceToSubCenter = Math.Abs(aiTarget.WorldPosition.X - aiTarget.Entity.Submarine.WorldPosition.X); - dist *= MathHelper.Lerp(1f, 5f, MathUtils.InverseLerp(0, 10000, horizontalDistanceToSubCenter)); - } - else - { - dist *= 5; + if (Submarine.MainSubs.Contains(aiTarget.Entity.Submarine)) + { + // Prioritize targets that are near the horizontal center of the sub, but only when none of the targets is reachable. + float horizontalDistanceToSubCenter = Math.Abs(aiTarget.WorldPosition.X - aiTarget.Entity.Submarine.WorldPosition.X); + dist *= MathHelper.Lerp(1f, 5f, MathUtils.InverseLerp(0, 10000, horizontalDistanceToSubCenter)); + } + else if (targetParams.AttackPattern == AttackPattern.Circle) + { + dist *= 5; + } } } } @@ -2785,9 +2880,9 @@ namespace Barotrauma { if (Character.CurrentHull != null && targetCharacter.CurrentHull != Character.CurrentHull) { - if (targetParams.State == AIState.Follow || targetParams.State == AIState.Protect || targetParams.State == AIState.Observe) + if (targetParams.State == AIState.Follow || targetParams.State == AIState.Protect || targetParams.State == AIState.Observe || targetParams.State == AIState.Eat) { - // Ignore targets that cannot see + // Ignore targets that cannot be seen if (!VisibleHulls.Contains(targetCharacter.CurrentHull)) { continue; @@ -2898,9 +2993,9 @@ namespace Barotrauma if (HasValidPath(requireNonDirty: true)) { return; } wallHits.Clear(); Structure wall = null; + Vector2 rayStart = AttackingLimb != null ? AttackingLimb.SimPosition : SimPosition; if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Target)) { - Vector2 rayStart = SimPosition; Vector2 rayEnd = SelectedAiTarget.SimPosition; if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null) { @@ -2914,7 +3009,6 @@ namespace Barotrauma } if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Heading)) { - Vector2 rayStart = SimPosition; Vector2 rayEnd = rayStart + VectorExtensions.Forward(Character.AnimController.Collider.Rotation + MathHelper.PiOver2, avoidLookAheadDistance * 5); if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null) { @@ -2930,7 +3024,6 @@ namespace Barotrauma } if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Steering)) { - Vector2 rayStart = SimPosition; Vector2 rayEnd = rayStart + Steering * 5; if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null) { @@ -2983,6 +3076,7 @@ namespace Barotrauma // Blocked by a wall that shouldn't be targeted. The main intention here is to prevent monsters from entering the the tail and the nose pieces. if (!isTargetingDoor) { + IgnoreTarget(SelectedAiTarget); ResetAITarget(); } } @@ -2994,6 +3088,7 @@ namespace Barotrauma else { // Blocked by a disabled wall. + IgnoreTarget(SelectedAiTarget); ResetAITarget(); } } @@ -3044,8 +3139,17 @@ namespace Barotrauma if (!(hit.UserData is Structure w)) { return false; } if (w.Submarine == null) { return false; } if (w.Submarine != SelectedAiTarget.Entity.Submarine) { return false; } - if (Character.Submarine == null && w.prefab.Tags.Contains("inner")) { return false; } - if (!AIParams.TargetOuterWalls && !w.prefab.Tags.Contains("inner")) { return false; } + if (Character.Submarine == null) + { + if (w.prefab.Tags.Contains("inner")) + { + if (!Character.AnimController.CanEnterSubmarine) { return false; } + } + else if (!AIParams.TargetOuterWalls) + { + return false; + } + } wall = w; return true; } @@ -3082,7 +3186,8 @@ namespace Barotrauma { if (door.LinkedGap.Size > ConvertUnits.ToDisplayUnits(colliderWidth)) { - return SteerThroughGap(door.LinkedGap, door.LinkedGap.FlowTargetHull.WorldPosition, deltaTime, maxDistance: 100); + float maxDistance = Math.Max(ConvertUnits.ToDisplayUnits(colliderLength), 100); + return SteerThroughGap(door.LinkedGap, door.LinkedGap.FlowTargetHull.WorldPosition, deltaTime, maxDistance: maxDistance); } } } @@ -3279,7 +3384,7 @@ namespace Barotrauma { if (priority.HasValue) { - targetParams.Priority = priority.Value; + targetParams.Priority = Math.Max(targetParams.Priority, priority.Value); } targetParams.State = state; if (!modifiedParams.ContainsKey(tag)) @@ -3298,6 +3403,7 @@ namespace Barotrauma /// /// Temporarily changes the predefined state for a target. Eg. Idle -> Attack. + /// Note: does not change the current AIState! /// private void ChangeTargetState(Character target, AIState state, float? priority = null) { @@ -3319,14 +3425,14 @@ namespace Barotrauma // --> Target the submarine too. if (target.Submarine != null && Character.Submarine == null && (canAttackDoors || canAttackWalls)) { - ChangeParams("room", state, priority * 0.1f); + ChangeParams("room", state, priority / 2); if (canAttackWalls) { - ChangeParams("wall", state, priority * 0.1f); + ChangeParams("wall", state, priority / 2); } if (canAttackDoors) { - ChangeParams("door", state, priority * 0.1f); + ChangeParams("door", state, priority / 2); } } ChangeParams("provocative", state, priority, onlyExisting: true); @@ -3378,9 +3484,15 @@ namespace Barotrauma private bool CanPerceive(AITarget target, float dist = -1, float distSquared = -1, bool checkVisibility = false) { + if (target?.Entity == null) { return false; } bool insideSightRange; bool insideSoundRange; - checkVisibility = checkVisibility && Character.Submarine != null && target.Entity.Submarine == Character.Submarine; + if (checkVisibility) + { + // We only want to check the visibility when the target is in ruins/wreck/similiar place where sneaking should be possible. + // When the monsters attack the player sub, they wall hack so that they can be more aggressive. + checkVisibility = target.Entity.Submarine != null && target.Entity.Submarine == Character.Submarine && target.Entity.Submarine.TeamID == CharacterTeamType.None; + } if (dist > 0) { insideSightRange = IsInRange(dist, target.SightRange, Sight); @@ -3539,12 +3651,12 @@ namespace Barotrauma public override bool SteerThroughGap(Gap gap, Vector2 targetWorldPos, float deltaTime, float maxDistance = -1) { - wallTarget = null; - LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 2); - Character.AnimController.ReleaseStuckLimbs(); bool success = base.SteerThroughGap(gap, targetWorldPos, deltaTime, maxDistance); if (success) { + wallTarget = null; + LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 2); + Character.AnimController.ReleaseStuckLimbs(); SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 1); } IsSteeringThroughGap = success; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 01f4f5d90..7438c42bf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -20,7 +20,6 @@ namespace Barotrauma private float reactTimer; private float unreachableClearTimer; private bool shouldCrouch; - public bool IsInsideCave { get; private set; } /// /// Resets each frame /// @@ -58,14 +57,14 @@ namespace Barotrauma private float obstacleRaycastTimer; private readonly float enemyCheckInterval = 0.2f; - private readonly float enemySpotDistanceOutside = 1500; + private readonly float enemySpotDistanceOutside = 800; private readonly float enemySpotDistanceInside = 1000; private float enemycheckTimer; /// - /// How far other characters can hear reports done by this character (e.g. reports for fires, intruders). Defaults to infinity. + /// How far other characters can hear reports done by this character (e.g. reports for fires, intruders). /// - public float ReportRange { get; set; } = float.PositiveInfinity; + public float ReportRange { get; set; } private float _aimSpeed = 1; public float AimSpeed @@ -167,6 +166,7 @@ namespace Barotrauma objectiveManager = new AIObjectiveManager(c); reactTimer = GetReactionTime(); SortTimer = Rand.Range(0f, sortObjectiveInterval); + ReportRange = Character.IsOnPlayerTeam ? float.PositiveInfinity : 1000; } public override void Update(float deltaTime) @@ -306,7 +306,7 @@ namespace Barotrauma UseIndoorSteeringOutside = false; } - if (Character.Submarine == null || !IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID) && !Character.IsEscorted) + if (Character.Submarine == null || Character.IsOnPlayerTeam && !Character.IsEscorted && !IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID)) { // Spot enemies while staying outside or inside an enemy ship. // does not apply for escorted characters, such as prisoners or terrorists who have their own behavior @@ -327,9 +327,13 @@ namespace Barotrauma float dist = toTarget.LengthSquared(); float maxDistance = Character.Submarine == null ? enemySpotDistanceOutside : enemySpotDistanceInside; if (dist > maxDistance * maxDistance) { continue; } - Vector2 forward = VectorExtensions.Forward(Character.AnimController.Collider.Rotation); - forward.X *= Character.AnimController.Dir; - if (Vector2.Dot(toTarget, forward) < 0.2f) { continue; } + if (EnemyAIController.IsLatchedToSomeoneElse(c, Character)) { continue; } + var head = Character.AnimController.GetLimb(LimbType.Head); + if (head == null) { continue; } + float rotation = head.body.TransformedRotation; + Vector2 forward = VectorExtensions.Forward(rotation); + float angle = MathHelper.ToDegrees(VectorExtensions.Angle(toTarget, forward)); + if (angle > 70) { continue; } if (!Character.CanSeeCharacter(c)) { continue; } if (dist < closestDistance || closestEnemy == null) { @@ -344,8 +348,6 @@ namespace Barotrauma } } } - - IsInsideCave = Character.CurrentHull == null && Level.Loaded?.Caves.FirstOrDefault(c => c.Area.Contains(Character.WorldPosition)) is Level.Cave; if (UseIndoorSteeringOutside || Character.CurrentHull?.Submarine != null || hasValidPath || IsCloseEnoughToTarget(steeringBuffer)) { @@ -565,7 +567,7 @@ namespace Barotrauma Character.AnimController.HeadInWater || Character.Submarine == null || (Character.Submarine.TeamID != Character.TeamID && !Character.IsEscorted) || - ObjectiveManager.CurrentOrders.Any(o => o.Objective.KeepDivingGearOn) || + ObjectiveManager.CurrentOrders.Any(o => o.Objective.KeepDivingGearOnAlsoWhenInactive) || ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn) || Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10; bool IsOrderedToWait() => Character.IsOnPlayerTeam && ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character; @@ -630,6 +632,7 @@ namespace Barotrauma { divingSuit.Drop(Character); HandleRelocation(divingSuit); + ReequipUnequipped(); } else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingSuit) { @@ -657,6 +660,7 @@ namespace Barotrauma { divingSuit.Drop(Character); HandleRelocation(divingSuit); + ReequipUnequipped(); } } } @@ -675,6 +679,7 @@ namespace Barotrauma { mask.Drop(Character); HandleRelocation(mask); + ReequipUnequipped(); } else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask) { @@ -699,6 +704,7 @@ namespace Barotrauma { mask.Drop(Character); HandleRelocation(mask); + ReequipUnequipped(); } } } @@ -832,6 +838,8 @@ namespace Barotrauma if (container == null) { return 0; } if (!container.HasAccess(character)) { return 0; } if (!container.Inventory.CanBePut(containableItem)) { return 0; } + var rootContainer = container.Item.GetRootContainer(); + if (rootContainer?.GetComponent() != null || rootContainer?.GetComponent() != null) { return 0; } if (container.ShouldBeContained(containableItem, out bool isRestrictionsDefined)) { if (isRestrictionsDefined) @@ -876,7 +884,7 @@ namespace Barotrauma foreach (Character target in Character.CharacterList) { if (target.CurrentHull != hull || !target.Enabled) { continue; } - if (AIObjectiveFightIntruders.IsValidTarget(target, Character)) + if (AIObjectiveFightIntruders.IsValidTarget(target, Character, false)) { if (!target.IsArrested && AddTargets(Character, target) && newOrder == null) { @@ -1096,7 +1104,7 @@ namespace Barotrauma // excluding poisons etc float realDamage = attackResult.Damage - healAmount; // including poisons etc - float totalDamage = realDamage - healAmount; + float totalDamage = realDamage; if (attackResult.Afflictions != null) { foreach (Affliction affliction in attackResult.Afflictions) @@ -1135,7 +1143,15 @@ namespace Barotrauma // Don't react to attackers that are outside of the sub (e.g. AoE attacks) return; } + bool isAttackerInfected = false; bool isAttackerFightingEnemy = false; + float minorDamageThreshold = 1; + float majorDamageThreshold = 20; + if (attacker.TeamID == Character.TeamID) + { + minorDamageThreshold = 10; + majorDamageThreshold = 40; + } if (IsFriendly(attacker)) { if (attacker.AnimController.Anim == Barotrauma.AnimController.Animation.CPR && attacker.SelectedCharacter == Character) @@ -1144,63 +1160,58 @@ namespace Barotrauma // Should not cancel any existing ai objectives (so that if the character attacked you and then helped, we still would want to retaliate). return; } - float cumulativeDamage = Character.GetDamageDoneByAttacker(attacker); + float cumulativeDamage = realDamage + Character.GetDamageDoneByAttacker(attacker); bool isAccidental = attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && Character.CombatAction == null; if (isAccidental) { - if (!Character.IsSecurity && cumulativeDamage > 1) + if (!Character.IsSecurity && cumulativeDamage > minorDamageThreshold) { AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker); } } else { + isAttackerInfected = attacker.CharacterHealth.GetAfflictionStrength("alieninfection") > 0; // Inform other NPCs - if (cumulativeDamage > 1 || totalDamage >= 10) + if (isAttackerInfected || cumulativeDamage > minorDamageThreshold || totalDamage > minorDamageThreshold) { - InformOtherNPCs(cumulativeDamage); + if (GameMain.IsMultiplayer || !attacker.IsPlayer || Character.TeamID != attacker.TeamID) + { + InformOtherNPCs(cumulativeDamage); + } } if (Character.IsBot) { - if (ObjectiveManager.CurrentObjective is AIObjectiveFightIntruders) { return; } - if (attacker.IsPlayer) + var combatMode = DetermineCombatMode(Character, cumulativeDamage); + if (attacker.IsPlayer && !Character.IsInstigator && !ObjectiveManager.IsCurrentObjective()) { - if (Character.IsSecurity) + switch (combatMode) { - if (attacker.TeamID != Character.TeamID && cumulativeDamage > 1 || cumulativeDamage > 10) - { - Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityarrest"), null, 0.50f, "attackedbyfriendlysecurityarrest", minDurationBetweenSimilar: 30.0f); - } - else - { - Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityresponse"), null, 0.50f, "attackedbyfriendlysecurityresponse", minDurationBetweenSimilar: 30.0f); - } - } - else if (!Character.IsInstigator && cumulativeDamage > 1) - { - Character.Speak(TextManager.Get("DialogAttackedByFriendly"), null, 0.50f, "attackedbyfriendly", minDurationBetweenSimilar: 30.0f); - } - } - if (cumulativeDamage > 1 && attacker.TeamID != Character.TeamID) - { - // If the attacker is using a low damage and high frequency weapon like a repair tool, we shouldn't use any delay. - AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage), attacker, delay: realDamage > 1 ? GetReactionTime() : 0); - } - else - { - // Don't react to minor (accidental) dmg done by characters that are in the same team - if (cumulativeDamage < 10) - { - if (!Character.IsSecurity && cumulativeDamage > 1) - { - AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker); - } - } - else - { - AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage, dmgThreshold: 50), attacker, GetReactionTime() * 2); + case AIObjectiveCombat.CombatMode.Defensive: + case AIObjectiveCombat.CombatMode.Retreat: + if (Character.IsSecurity) + { + Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityresponse"), null, 0.5f, "attackedbyfriendlysecurityresponse", minDurationBetweenSimilar: 10.0f); + } + else + { + Character.Speak(TextManager.Get("DialogAttackedByFriendly"), null, 0.5f, "attackedbyfriendly", minDurationBetweenSimilar: 10.0f); + } + break; + case AIObjectiveCombat.CombatMode.Offensive: + case AIObjectiveCombat.CombatMode.Arrest: + Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityarrest"), null, 0.5f, "attackedbyfriendlysecurityarrest", minDurationBetweenSimilar: 10.0f); + break; + case AIObjectiveCombat.CombatMode.None: + if (Character.IsSecurity && realDamage > 1) + { + Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityresponse"), null, 0.5f, "attackedbyfriendlysecurityresponse", minDurationBetweenSimilar: 10.0f); + } + break; } } + // If the attacker is using a low damage and high frequency weapon like a repair tool, we shouldn't use any delay. + AddCombatObjective(combatMode, attacker, delay: realDamage > 1 ? GetReactionTime() : 0); } if (!isAttackerFightingEnemy) { @@ -1213,15 +1224,15 @@ namespace Barotrauma if (Character.Submarine != null && Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine)) { // Non-friendly - InformOtherNPCs(Character.GetDamageDoneByAttacker(attacker)); + InformOtherNPCs(); } if (Character.IsBot) { - AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage: realDamage), attacker); + AddCombatObjective(DetermineCombatMode(Character), attacker); } } - void InformOtherNPCs(float cumulativeDamage) + void InformOtherNPCs(float cumulativeDamage = 0) { foreach (Character otherCharacter in Character.CharacterList) { @@ -1237,25 +1248,25 @@ namespace Barotrauma { //if the other character did not witness the attack, and the character is not within report range (or capable of reporting) //don't react to the attack - if (Character.IsDead || Character.IsUnconscious || !CheckReportRange(Character, otherCharacter, ReportRange)) + if (Character.IsDead || Character.IsUnconscious || otherCharacter.TeamID != Character.TeamID || !CheckReportRange(Character, otherCharacter, ReportRange)) { continue; } } - var combatMode = DetermineCombatMode(otherCharacter, cumulativeDamage, isWitnessing, dmgThreshold: attacker.TeamID == Character.TeamID ? 50 : 10); + var combatMode = DetermineCombatMode(otherCharacter, cumulativeDamage, isWitnessing); float delay = isWitnessing ? GetReactionTime() : Rand.Range(2.0f, 5.0f, Rand.RandSync.Unsynced); otherHumanAI.AddCombatObjective(combatMode, attacker, delay); } } - AIObjectiveCombat.CombatMode DetermineCombatMode(Character c, float cumulativeDamage, bool isWitnessing = false, float dmgThreshold = 10, bool allowOffensive = true) + AIObjectiveCombat.CombatMode DetermineCombatMode(Character c, float cumulativeDamage = 0, bool isWitnessing = false) { if (!IsFriendly(attacker)) { if (Character.Submarine == null) { - // Outside -> don't react. - return AIObjectiveCombat.CombatMode.None; + // Outside + return attacker.Submarine == null ? AIObjectiveCombat.CombatMode.Defensive : AIObjectiveCombat.CombatMode.Retreat; } if (!Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine)) { @@ -1268,6 +1279,15 @@ namespace Barotrauma } else { + if (isAttackerInfected) + { + cumulativeDamage = 100; + } + if (GameMain.IsSingleplayer && attacker.IsPlayer && Character.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 (Character.Submarine == null || !Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine)) { // Outside or attacked from an unconnected submarine -> don't react. @@ -1279,17 +1299,17 @@ namespace Barotrauma isAttackerFightingEnemy = true; return AIObjectiveCombat.CombatMode.None; } - else if (isWitnessing && Character.CombatAction != null && !c.IsSecurity) + if (isWitnessing && Character.CombatAction != null && !c.IsSecurity) { return Character.CombatAction.WitnessReaction; } - else if (attacker.IsPlayer && FindInstigator() is Character instigator) + if (attacker.IsPlayer && FindInstigator() is Character instigator) { - // The guards don't react when the player there's an instigator around + // The guards don't react to player's aggressions when there's an instigator around isAttackerFightingEnemy = true; return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : (instigator.CombatAction != null ? instigator.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat); } - else if (attacker.TeamID == CharacterTeamType.FriendlyNPC && attacker.AIController != null && !(attacker.AIController.IsMentallyUnstable || attacker.AIController.IsMentallyUnstable)) + if (attacker.TeamID == CharacterTeamType.FriendlyNPC && !(attacker.AIController.IsMentallyUnstable || attacker.AIController.IsMentallyUnstable)) { if (c.IsSecurity) { @@ -1307,25 +1327,25 @@ namespace Barotrauma // Already targeting the attacker -> treat as a more serious threat. cumulativeDamage *= 2; } - if (attackResult.Afflictions != null && attackResult.Afflictions.Any(a => a is AfflictionHusk)) - { - cumulativeDamage = 100; - } - if (cumulativeDamage > dmgThreshold) + if (cumulativeDamage > majorDamageThreshold) { if (c.IsSecurity) { - return c.IsSecurity && allowOffensive ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Arrest; + return AIObjectiveCombat.CombatMode.Offensive; } else { return c == Character ? AIObjectiveCombat.CombatMode.Defensive : AIObjectiveCombat.CombatMode.Retreat; } } - else + else if (cumulativeDamage > minorDamageThreshold) { return c.IsSecurity ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Retreat; } + else + { + return AIObjectiveCombat.CombatMode.None; + } } Character FindInstigator() @@ -1758,7 +1778,7 @@ namespace Barotrauma foreach (var enemy in Character.CharacterList) { if (enemy.CurrentHull != hull) { continue; } - if (AIObjectiveFightIntruders.IsValidTarget(enemy, character)) + if (AIObjectiveFightIntruders.IsValidTarget(enemy, character, false)) { AddTargets(character, enemy); } @@ -1838,7 +1858,7 @@ namespace Barotrauma bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective(); bool ignoreWater = HasDivingSuit(character); bool ignoreOxygen = ignoreWater || HasDivingMask(character); - bool ignoreEnemies = ObjectiveManager.IsCurrentOrder() || ObjectiveManager.Objectives.Any(o => o is AIObjectiveFightIntruders); + bool ignoreEnemies = ObjectiveManager.IsCurrentOrder() || ObjectiveManager.IsCurrentObjective(); float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies); if (isCurrentHull) { @@ -2059,10 +2079,12 @@ namespace Barotrauma public static bool IsItemTargetedBySomeone(ItemComponent target, CharacterTeamType team, out Character operatingCharacter) { operatingCharacter = null; + if (target?.Item == null) { return false; } float highestPriority = -1.0f; float highestPriorityModifier = -1.0f; foreach (Character c in Character.CharacterList) { + if (c == null) { continue; } if (c.Removed) { continue; } if (c.TeamID != team) { continue; } if (c.IsIncapacitated) { continue; } @@ -2071,12 +2093,12 @@ namespace Barotrauma operatingCharacter = c; return true; } - if (c.AIController is HumanAIController humanAI) + if (c.AIController is HumanAIController humanAI && humanAI.ObjectiveManager is AIObjectiveManager objectiveManager) { - foreach (var objective in humanAI.ObjectiveManager.Objectives) + foreach (var objective in objectiveManager.Objectives) { if (!(objective is AIObjectiveOperateItem operateObjective)) { continue; } - if (operateObjective.Component.Item != target.Item) { continue; } + if (operateObjective.Component?.Item != target.Item) { continue; } if (operateObjective.Priority < highestPriority) { continue; } if (operateObjective.PriorityModifier < highestPriorityModifier) { continue; } operatingCharacter = c; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index ca242f790..2e651a325 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -9,10 +9,10 @@ namespace Barotrauma { class IndoorsSteeringManager : SteeringManager { - private PathFinder pathFinder; + private readonly PathFinder pathFinder; private SteeringPath currentPath; - private bool canOpenDoors; + private readonly bool canOpenDoors; public bool CanBreakDoors { get; set; } private bool ShouldBreakDoor(Door door) => @@ -20,7 +20,7 @@ namespace Barotrauma !door.Item.Indestructible && !door.Item.InvulnerableToDamage && (door.Item.Submarine == null || door.Item.Submarine.TeamID != character.TeamID); - private Character character; + private readonly Character character; private Vector2 currentTarget; @@ -51,19 +51,13 @@ namespace Barotrauma private set; } - /// - /// Returns true if the current or the next node is in ladders. - /// - public bool InLadders => - currentPath != null && currentPath.CurrentNode != null && - (currentPath.CurrentNode.Ladders != null && currentPath.CurrentNode.Ladders.Item.IsInteractable(character) || - (currentPath.NextNode != null && currentPath.NextNode.Ladders != null && currentPath.NextNode.Ladders.Item.IsInteractable(character))); - /// /// Returns true if any node in the path is in stairs /// public bool InStairs => currentPath != null && currentPath.Nodes.Any(n => n.Stairs != null); + public bool IsCurrentNodeLadder => currentPath?.CurrentNode?.Ladders != null && currentPath.CurrentNode.Ladders.Item.IsInteractable(character); + public bool IsNextNodeLadder => GetNextLadder() != null; public bool IsNextLadderSameAsCurrent @@ -83,8 +77,10 @@ namespace Barotrauma public IndoorsSteeringManager(ISteerable host, bool canOpenDoors, bool canBreakDoors) : base(host) { - pathFinder = new PathFinder(WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Path), true); - pathFinder.GetNodePenalty = GetNodePenalty; + pathFinder = new PathFinder(WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Path), true) + { + GetNodePenalty = GetNodePenalty + }; this.canOpenDoors = canOpenDoors; this.CanBreakDoors = canBreakDoors; @@ -99,14 +95,24 @@ namespace Barotrauma base.Update(speed); float step = 1.0f / 60.0f; checkDoorsTimer -= step; - buttonPressTimer -= step; + if (lastDoor.door == null || !lastDoor.shouldBeOpen || lastDoor.door.IsOpen) + { + buttonPressTimer = 0; + } + else + { + buttonPressTimer -= step; + } findPathTimer -= step; } public void SetPath(SteeringPath path) { currentPath = path; - if (path.Nodes.Any()) currentTarget = path.Nodes[path.Nodes.Count - 1].SimPosition; + if (path.Nodes.Any()) + { + currentTarget = path.Nodes[path.Nodes.Count - 1].SimPosition; + } findPathTimer = Math.Min(findPathTimer, 1.0f); IsPathDirty = false; } @@ -124,15 +130,9 @@ namespace Barotrauma public void SteeringSeek(Vector2 target, float weight, float minGapWidth = 0, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null, bool checkVisiblity = true) { - if (buttonPressTimer > 0 && lastDoor.door != null && lastDoor.state && !lastDoor.door.IsOpen) - { - // We have pressed the button and are waiting for the door to open -> Hold still until we can press the button again. - Reset(); - } - else - { - steering += CalculateSteeringSeek(target, weight, minGapWidth, startNodeFilter, endNodeFilter, nodeFilter, checkVisiblity); - } + // Have to use a variable here or resetting doesn't work. + Vector2 addition = CalculateSteeringSeek(target, weight, minGapWidth, startNodeFilter, endNodeFilter, nodeFilter, checkVisiblity); + steering += addition; } /// @@ -328,7 +328,13 @@ namespace Barotrauma { CheckDoorsInPath(); doorsChecked = true; - } + } + if (buttonPressTimer > 0 && lastDoor.door != null && lastDoor.shouldBeOpen && !lastDoor.door.IsOpen) + { + // We have pressed the button and are waiting for the door to open -> Hold still until we can press the button again. + Reset(); + return Vector2.Zero; + } Vector2 pos = host.WorldPosition; bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater; // Only humanoids can climb ladders @@ -378,7 +384,7 @@ namespace Barotrauma //at the same height as the waypoint if (Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y) < (collider.height / 2 + collider.radius) * 1.25f) { - float heightFromFloor = character.AnimController.GetColliderBottom().Y - character.AnimController.FloorY; + float heightFromFloor = character.AnimController.GetHeightFromFloor(); if (heightFromFloor <= 0.0f) { diff.Y = Math.Max(diff.Y, 100); @@ -451,12 +457,20 @@ namespace Barotrauma // Cannot use the head position, because not all characters have head or it can be below the total height of the character float characterHeight = Math.Max(colliderSize.Y + character.AnimController.ColliderHeightFromFloor, minHeight); float horizontalDistance = Math.Abs(collider.SimPosition.X - currentPath.CurrentNode.SimPosition.X); - bool isAboveFeet = currentPath.CurrentNode.SimPosition.Y > colliderBottom.Y; - bool isNotTooHigh = currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y + characterHeight; + bool isTargetTooHigh = currentPath.CurrentNode.SimPosition.Y > colliderBottom.Y + characterHeight; + bool isTargetTooLow = currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y; var door = currentPath.CurrentNode.ConnectedDoor; float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 5, 0, 1)); + if (currentPath.CurrentNode.Stairs != null && currentPath.NextNode?.Stairs == null) + { + margin = 1; + if (currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y + character.AnimController.ColliderHeightFromFloor * 0.25f) + { + isTargetTooLow = true; + } + } float targetDistance = Math.Max(colliderSize.X / 2 * margin, minWidth / 2); - if (horizontalDistance < targetDistance && isAboveFeet && isNotTooHigh && (door == null || door.CanBeTraversed)) + if (horizontalDistance < targetDistance && !isTargetTooHigh && !isTargetTooLow && (door == null || door.CanBeTraversed)) { NextNode(!doorsChecked); } @@ -504,6 +518,16 @@ namespace Barotrauma canAccessButtons = true; } } + foreach (var linked in door.Item.linkedTo) + { + if (!(linked is Item linkedItem)) { continue; } + var button = linkedItem.GetComponent(); + if (button == null) { continue; } + if (button.HasAccess(character) && (buttonFilter == null || buttonFilter(button))) + { + canAccessButtons = true; + } + } return canAccessButtons || door.IsOpen || ShouldBreakDoor(door); } } @@ -516,7 +540,7 @@ namespace Barotrauma return ConvertUnits.ToDisplayUnits(Math.Max(colliderSize.X, colliderSize.Y)); } - private (Door door, bool state) lastDoor; + private (Door door, bool shouldBeOpen) lastDoor; private float GetDoorCheckTime() { if (steering.LengthSquared() > 0) @@ -539,7 +563,6 @@ namespace Barotrauma WayPoint nextWaypoint = null; Door door = null; bool shouldBeOpen = false; - if (currentPath.Nodes.Count == 1) { door = currentPath.Nodes.First().ConnectedDoor; @@ -645,7 +668,7 @@ namespace Barotrauma }); if (canAccess) { - bool pressButton = buttonPressTimer <= 0 || lastDoor.door != door || lastDoor.state != shouldBeOpen; + bool pressButton = buttonPressTimer <= 0 || lastDoor.door != door || lastDoor.shouldBeOpen != shouldBeOpen; if (door.HasIntegratedButtons) { if (pressButton && character.CanSeeTarget(door.Item)) @@ -653,7 +676,7 @@ namespace Barotrauma if (door.Item.TryInteract(character, forceSelectKey: true)) { lastDoor = (door, shouldBeOpen); - buttonPressTimer = buttonPressCooldown; + buttonPressTimer = shouldBeOpen ? buttonPressCooldown : 0; } else { @@ -671,7 +694,7 @@ namespace Barotrauma if (closestButton.Item.TryInteract(character, forceSelectKey: true)) { lastDoor = (door, shouldBeOpen); - buttonPressTimer = buttonPressCooldown; + buttonPressTimer = shouldBeOpen ? buttonPressCooldown : 0; } else { @@ -697,7 +720,6 @@ namespace Barotrauma // The button is on the wrong side of the door or a wall currentPath.Unreachable = true; } - lastDoor = (null, false); return; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index 905cffa20..2a76c67aa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -16,7 +16,6 @@ namespace Barotrauma public virtual bool IgnoreUnsafeHulls => false; public virtual bool AbandonWhenCannotCompleteSubjectives => true; public virtual bool AllowSubObjectiveSorting => false; - public virtual bool ForceOrderPriority => true; public virtual bool PrioritizeIfSubObjectivesActive => false; /// @@ -31,6 +30,8 @@ namespace Barotrauma public virtual bool ConcurrentObjectives => false; public virtual bool KeepDivingGearOn => false; + public virtual bool KeepDivingGearOnAlsoWhenInactive => false; + /// /// There's a separate property for diving suit and mask: KeepDivingGearOn. /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs index 42950891c..007aee4a5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs @@ -85,11 +85,12 @@ namespace Barotrauma bool equip = item.GetComponent() != null || item.AllowedSlots.Any(s => s != InvSlotType.Any) && item.AllowedSlots.None(s => - s == InvSlotType.Card || - s == InvSlotType.Head || - s == InvSlotType.Headset || - s == InvSlotType.InnerClothes || - s == InvSlotType.OuterClothes); + s == InvSlotType.Card || + s == InvSlotType.Head || + s == InvSlotType.Headset || + s == InvSlotType.InnerClothes || + s == InvSlotType.OuterClothes || + s == InvSlotType.HealthInterface); TryAddSubObjective(ref decontainObjective, () => new AIObjectiveDecontainItem(character, item, objectiveManager, targetContainer: suitableContainer.GetComponent()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs index f3197b975..b84bf63ba 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs @@ -11,7 +11,7 @@ namespace Barotrauma public override string Identifier { get; set; } = "cleanup items"; public override bool KeepDivingGearOn => true; public override bool AllowAutomaticItemUnequipping => false; - public override bool ForceOrderPriority => false; + protected override bool ForceOrderPriority => false; public readonly List prioritizedItems = new List(); @@ -52,7 +52,7 @@ namespace Barotrauma // The validity changes when a character picks the item up. if (!IsValidTarget(target, character, checkInventory: true)) { return Objectives.ContainsKey(target) && IsItemInsideValidSubmarine(target, character); } if (target.CurrentHull.FireSources.Count > 0) { return false; } - // Don't repair items in rooms that have enemies inside. + // Don't clean up items in rooms that have enemies inside. if (Character.CharacterList.Any(c => c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; } return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index de085d3c4..3de6f2a28 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -30,7 +30,7 @@ namespace Barotrauma private float holdFireTimer; private bool hasAimed; private bool isLethalWeapon; - private bool AllowCoolDown => !IsOffensiveOrArrest || Mode != initialMode; + private bool AllowCoolDown => !IsOffensiveOrArrest || Mode != initialMode || character.TeamID == Enemy.TeamID; public Character Enemy { get; private set; } public bool HoldPosition { get; set; } @@ -117,7 +117,10 @@ namespace Barotrauma private float AimSpeed => HumanAIController.AimSpeed; private float AimAccuracy => HumanAIController.AimAccuracy; - private bool EnemyIsClose() => Enemy != null && character.CurrentHull != null && character.CurrentHull == Enemy.CurrentHull || Vector2.DistanceSquared(character.Position, Enemy.Position) < 500; + private bool IsEnemyCloserThan(float margin) => + Enemy != null && Enemy.CurrentHull != null && + character.InWater && Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition) < margin * margin || + HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull) && Math.Abs(character.WorldPosition.X - Enemy.WorldPosition.X) < margin; public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1, float coolDown = 10.0f) : base(character, objectiveManager, priorityModifier) @@ -143,13 +146,20 @@ namespace Barotrauma { Mode = CombatMode.Retreat; } - spreadTimer = Rand.Range(-10, 10); + spreadTimer = Rand.Range(-10f, 10f); + SetAimTimer(Rand.Range(1f, 1.5f) / AimSpeed); HumanAIController.SortTimer = 0; } protected override float GetPriority() { - if (character.TeamID == CharacterTeamType.FriendlyNPC && Enemy != null) + if (Enemy == null) + { + Priority = 0; + Abandon = true; + return Priority; + } + if (character.TeamID == CharacterTeamType.FriendlyNPC) { if (Enemy.Submarine == null || (Enemy.Submarine.TeamID != character.TeamID && Enemy.Submarine != character.Submarine)) { @@ -160,6 +170,13 @@ namespace Barotrauma } float damageFactor = MathUtils.InverseLerp(0.0f, 5.0f, character.GetDamageDoneByAttacker(Enemy) / 100.0f); Priority = TargetEliminated ? 0 : Math.Min((95 + damageFactor) * PriorityModifier, 100); + if (Priority > 0) + { + if (EnemyAIController.IsLatchedToSomeoneElse(Enemy, character)) + { + Priority = 0; + } + } return Priority; } @@ -366,7 +383,7 @@ namespace Barotrauma } } } - bool isAllowedToSeekWeapons = !EnemyIsClose() && character.TeamID != CharacterTeamType.FriendlyNPC && IsOffensiveOrArrest; + bool isAllowedToSeekWeapons = character.CurrentHull != null && !IsEnemyCloserThan(300) && character.IsOnPlayerTeam && IsOffensiveOrArrest; if (!isAllowedToSeekWeapons) { if (WeaponComponent == null) @@ -418,9 +435,16 @@ namespace Barotrauma onCompleted: () => RemoveSubObjective(ref seekWeaponObjective), onAbandon: () => { - SpeakNoWeapons(); RemoveSubObjective(ref seekWeaponObjective); - Mode = CombatMode.Retreat; + if (Weapon == null) + { + SpeakNoWeapons(); + Mode = CombatMode.Retreat; + } + else + { + Mode = CombatMode.Defensive; + } }); } } @@ -478,13 +502,25 @@ namespace Barotrauma weaponComponent = null; float bestPriority = 0; float lethalDmg = -1; - bool enemyIsClose = EnemyIsClose(); + bool isAllowedToSeekWeapons = !IsEnemyCloserThan(300); + bool prioritizeMelee = IsEnemyCloserThan(50) || EnemyAIController.IsLatchedTo(Enemy, character); foreach (var weapon in weaponList) { float priority = weapon.CombatPriority; + if (prioritizeMelee) + { + if (weapon is MeleeWeapon) + { + priority *= 5; + } + else + { + priority /= 2; + } + } if (!weapon.IsLoaded(character)) { - if (weapon is RangedWeapon && enemyIsClose) + if (weapon is RangedWeapon && !isAllowedToSeekWeapons) { // Close to the enemy. Ignore weapons that don't have any ammunition (-> Don't seek ammo). continue; @@ -693,7 +729,7 @@ namespace Barotrauma var slots = Weapon.AllowedSlots.Where(s => IsHandSlotType(s)); if (character.Inventory.TryPutItem(Weapon, character, slots)) { - aimTimer = Rand.Range(0.2f, 0.4f) / AimSpeed; + SetAimTimer(Rand.Range(0.2f, 0.4f) / AimSpeed); } else { @@ -1014,7 +1050,7 @@ namespace Barotrauma } if (!canSeeTarget) { - aimTimer = Rand.Range(0.2f, 0.4f) / AimSpeed; + SetAimTimer(Rand.Range(0.2f, 0.4f) / AimSpeed); return; } if (Weapon.RequireAimToUse) @@ -1074,7 +1110,7 @@ namespace Barotrauma else if (!character.IsFacing(Enemy.WorldPosition)) { // Don't do the facing check if we are close to the target, because it easily causes the character to get stuck here when it flips around. - aimTimer = Rand.Range(1f, 1.5f) / AimSpeed; + SetAimTimer(Rand.Range(1f, 1.5f) / AimSpeed); } } else @@ -1177,7 +1213,7 @@ namespace Barotrauma } private void SpeakNoWeapons() => Speak("dialogcombatnoweapons", delay: 0, minDuration: 30); - private void AskHelp() => Speak("dialogcombatretreating", delay: Rand.Range(0, 1), minDuration: 20); + private void AskHelp() => Speak("dialogcombatretreating", delay: Rand.Range(0f, 1f), minDuration: 20); private void Speak(string textIdentifier, float delay, float minDuration) { @@ -1191,18 +1227,6 @@ namespace Barotrauma } } - //private float CalculateEnemyStrength() - //{ - // float enemyStrength = 0; - // AttackContext currentContext = character.GetAttackContext(); - // foreach (Limb limb in Enemy.AnimController.Limbs) - // { - // if (limb.attack == null) continue; - // if (!limb.attack.IsValidContext(currentContext)) { continue; } - // if (!limb.attack.IsValidTarget(AttackTarget.Character)) { continue; } - // enemyStrength += limb.attack.GetTotalDamage(false); - // } - // return enemyStrength; - //} + private void SetAimTimer(float newTimer) => aimTimer = Math.Max(aimTimer, newTimer); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index f39b105f7..26d2e8757 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -120,7 +120,7 @@ namespace Barotrauma } if (character.CanInteractWith(container.Item, checkLinked: false)) { - if (RemoveExisting || (RemoveExistingWhenNecessary && !container.Inventory.CanBePut(item))) + if (RemoveExisting || (RemoveExistingWhenNecessary && !container.Inventory.CanBePut(ItemToContain))) { HumanAIController.UnequipContainedItems(container.Item, predicate: RemoveExistingPredicate, unequipMax: RemoveMax); } @@ -159,7 +159,8 @@ namespace Barotrauma { TargetName = container.Item.Name, AbortCondition = obj => - container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI(character) || + container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI(character) || + (container.Item.GetRootContainer()?.OwnInventory?.Locked ?? false) || ItemToContain == null || ItemToContain.Removed || !ItemToContain.IsOwnedBy(character) || container.Item.GetRootInventoryOwner() is Character c && c != character, SpeakIfFails = !objectiveManager.IsCurrentOrder() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs index 1c8b16955..dbcfad9d2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs @@ -40,6 +40,7 @@ namespace Barotrauma public Func RemoveExistingPredicate { get; set; } public int? RemoveExistingMax { get; set; } public string AbandonGetItemDialogueIdentifier { get; set; } + public Func AbandonGetItemDialogueCondition { get; set; } public AIObjectiveDecontainItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, ItemContainer sourceContainer = null, ItemContainer targetContainer = null, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) @@ -106,6 +107,7 @@ namespace Barotrauma TryAddSubObjective(ref getItemObjective, constructor: () => new AIObjectiveGetItem(character, targetItem, objectiveManager, Equip) { + CannotFindDialogueCondition = AbandonGetItemDialogueCondition, CannotFindDialogueIdentifierOverride = AbandonGetItemDialogueIdentifier, SpeakIfFails = AbandonGetItemDialogueIdentifier != null, TakeWholeStack = this.TakeWholeStack diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs index 4de4c0a43..ce20e9de1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs @@ -12,10 +12,12 @@ namespace Barotrauma protected override float TargetUpdateTimeMultiplier => 0.2f; + public bool TargetCharactersInOtherSubs { get; set; } + public AIObjectiveFightIntruders(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { } - protected override bool Filter(Character target) => IsValidTarget(target, character); + protected override bool Filter(Character target) => IsValidTarget(target, character, TargetCharactersInOtherSubs); protected override IEnumerable GetList() => Character.CharacterList; @@ -26,6 +28,7 @@ namespace Barotrauma if (totalEnemies == 0) { return 0; } if (character.IsSecurity) { return 100; } if (objectiveManager.IsOrder(this)) { return 100; } + // If there's any security officers onboard, leave fighting for them. return HumanAIController.IsTrueForAnyCrewMember(c => c.Character.IsSecurity && !c.Character.IsIncapacitated && c.Character.Submarine == character.Submarine) ? 0 : 100; } @@ -53,7 +56,7 @@ namespace Barotrauma protected override void OnObjectiveCompleted(AIObjective objective, Character target) => HumanAIController.RemoveTargets(character, target); - public static bool IsValidTarget(Character target, Character character) + public static bool IsValidTarget(Character target, Character character, bool targetCharactersInOtherSubs) { if (target == null || target.Removed) { return false; } if (target.IsDead) { return false; } @@ -64,8 +67,10 @@ namespace Barotrauma if (target.CurrentHull == null) { return false; } if (HumanAIController.IsFriendly(character, target)) { return false; } if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; } + if (!targetCharactersInOtherSubs && character.Submarine.TeamID != target.Submarine.TeamID) { return false; } if (target.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { return false; } if (target.IsArrested) { return false; } + if (EnemyAIController.IsLatchedToSomeoneElse(target, character)) { return false; } return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs index 5afa3665f..cd3abcc19 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs @@ -108,7 +108,7 @@ namespace Barotrauma AllowToFindDivingGear = false, AllowDangerousPressure = true, ConditionLevel = MIN_OXYGEN, - RemoveExisting = true + RemoveExistingWhenNecessary = true }; }, onAbandon: () => diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index bb988f3c5..dc2a12da2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -76,6 +76,10 @@ namespace Barotrauma // -> ignore find safety unless we need to find a diving gear Priority = 0; } + else if (objectiveManager.Objectives.Any(o => o is AIObjectiveCombat && o.Priority > 0)) + { + Priority = 0; + } Priority = MathHelper.Clamp(Priority, 0, 100); if (divingGearObjective != null && !divingGearObjective.IsCompleted && divingGearObjective.CanBeCompleted) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index af2c65a9d..256913c4e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -162,7 +162,10 @@ namespace Barotrauma CloseEnough = reach, DialogueIdentifier = Leak.FlowTargetHull != null ? "dialogcannotreachleak" : null, TargetName = Leak.FlowTargetHull?.DisplayName, - CheckVisibility = false + 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() }, onAbandon: () => { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index 6613d85c0..a0d14f7dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -59,6 +59,7 @@ namespace Barotrauma public bool CheckPathForEachItem { get; set; } public bool SpeakIfFails { get; set; } public string CannotFindDialogueIdentifierOverride { get; set; } + public Func CannotFindDialogueCondition { get; set; } private int _itemCount = 1; public int ItemCount @@ -400,6 +401,11 @@ namespace Barotrauma { if (!ownerItem.IsInteractable(character)) { continue; } if (!(ownerItem.GetComponent()?.HasRequiredItems(character, addMessage: false) ?? true)) { continue; } + //the item is inside an item inside an item (e.g. fuel tank in a welding tool in a cabinet -> reduce priority to prefer items that aren't inside a tool) + if (ownerItem != item.Container) + { + itemPriority *= 0.1f; + } } Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition; float yDist = Math.Abs(character.WorldPosition.Y - itemPos.Y); @@ -560,22 +566,18 @@ namespace Barotrauma DebugConsole.NewMessage($"{character.Name}: Get item failed to reach {moveToTarget}", Color.Yellow); #endif } - if (SpeakIfFails) - { - SpeakCannotFind(); - } + SpeakCannotFind(); } private void SpeakCannotFind() { - if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective) - { - string msg = TextManager.Get(CannotFindDialogueIdentifierOverride, returnNull: true) ?? TextManager.Get("dialogcannotfinditem", returnNull: true); - if (msg != null) - { - character.Speak(msg, identifier: "dialogcannotfinditem", minDurationBetweenSimilar: 20.0f); - } - } + if (!SpeakIfFails) { return; } + if (!character.IsOnPlayerTeam) { return; } + if (objectiveManager.CurrentOrder != objectiveManager.CurrentObjective) { return; } + if (CannotFindDialogueCondition != null && !CannotFindDialogueCondition()) { return; } + string msg = TextManager.Get(CannotFindDialogueIdentifierOverride, returnNull: true) ?? TextManager.Get("dialogcannotfinditem", returnNull: true); + if (msg == null) { return; } + character.Speak(msg, identifier: "dialogcannotfinditem", minDurationBetweenSimilar: 20.0f); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index 72dcbaf98..dd250f619 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -43,7 +43,6 @@ namespace Barotrauma private readonly float minDistance = 50; private readonly float seekGapsInterval = 1; private float seekGapsTimer; - private bool cannotFollow; /// /// Display units @@ -52,6 +51,11 @@ namespace Barotrauma { get { + if (IsFollowOrderObjective && Target is Character targetCharacter && (targetCharacter.CurrentHull == null) != (character.CurrentHull == null)) + { + // Keep close when the target is going inside/outside + return minDistance; + } float dist = _closeEnough * CloseEnoughMultiplier; float extraMultiplier = Math.Clamp(CloseEnoughMultiplier * 0.6f, 1, 3); if (character.AnimController.InWater) @@ -73,6 +77,9 @@ namespace Barotrauma // 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; } @@ -96,6 +103,8 @@ namespace Barotrauma public float? OverridePriority = null; + public Func SpeakCannotReachCondition { get; set; } + protected override float GetPriority() { bool isOrder = objectiveManager.IsOrder(this); @@ -166,14 +175,14 @@ namespace Barotrauma DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target}", Color.Yellow); } #endif - if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective && DialogueIdentifier != null && SpeakIfFails) - { - string msg = TargetName == null ? TextManager.Get(DialogueIdentifier, true) : TextManager.GetWithVariable(DialogueIdentifier, "[name]", TargetName, formatCapitals: !(Target is Character)); - if (msg != null) - { - character.Speak(msg, identifier: DialogueIdentifier, minDurationBetweenSimilar: 20.0f); - } - } + if (!character.IsOnPlayerTeam) { return; } + if (objectiveManager.CurrentOrder != objectiveManager.CurrentObjective) { return; } + if (DialogueIdentifier == null) { return; } + if (!SpeakIfFails) { return; } + if (SpeakCannotReachCondition != null && !SpeakCannotReachCondition()) { return; } + string msg = TargetName == null ? TextManager.Get(DialogueIdentifier, true) : TextManager.GetWithVariable(DialogueIdentifier, "[name]", TargetName, formatCapitals: !(Target is Character)); + if (msg == null) { return; } + character.Speak(msg, identifier: DialogueIdentifier, minDurationBetweenSimilar: 20.0f); } public void ForceAct(float deltaTime) => Act(deltaTime); @@ -286,28 +295,16 @@ namespace Barotrauma { TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager), onAbandon: () => Abandon = true, - onCompleted: () => - { - cannotFollow = false; - RemoveSubObjective(ref findDivingGear); - }); + onCompleted: () => RemoveSubObjective(ref findDivingGear)); } else { TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager), onAbandon: () => Abandon = true, - onCompleted: () => - { - cannotFollow = false; - RemoveSubObjective(ref findDivingGear); - }); + onCompleted: () => RemoveSubObjective(ref findDivingGear)); } return; } - else - { - cannotFollow = false; - } } if (repeat) { @@ -635,21 +632,29 @@ namespace Barotrauma { get { - if (SteeringManager == PathSteering && PathSteering.CurrentPath?.CurrentNode?.Ladders != null) + if (character.IsClimbing) { - //don't consider the character to be close enough to the target while climbing ladders, - //UNLESS the last node in the path has been reached - //otherwise characters can let go of the ladders too soon once they're close enough to the target - if (PathSteering.CurrentPath.NextNode != null) { return false; } + if (SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.CurrentPath.Finished && PathSteering.IsCurrentNodeLadder) + { + if (Target.WorldPosition.Y > character.WorldPosition.Y) + { + // The target is still above us + return false; + } + if (!character.AnimController.IsAboveFloor) + { + // Going through a hatch + return false; + } + } } if (!AlwaysUseEuclideanDistance && !character.AnimController.InWater) { - float yDiff = Math.Abs(Target.WorldPosition.Y - character.WorldPosition.Y); - if (yDiff > CloseEnough) { return false; } - float xDiff = Math.Abs(Target.WorldPosition.X - character.WorldPosition.X); - return xDiff <= CloseEnough; + float yDist = Math.Abs(Target.WorldPosition.Y - character.WorldPosition.Y); + if (yDist > CloseEnough) { return false; } + float xDist = Math.Abs(Target.WorldPosition.X - character.WorldPosition.X); + return xDist <= CloseEnough; } - Vector2 sourcePos = UseDistanceRelativeToAimSourcePos ? character.AnimController.AimSourceWorldPos : character.WorldPosition; return Vector2.DistanceSquared(Target.WorldPosition, sourcePos) < CloseEnough * CloseEnough; } @@ -727,7 +732,6 @@ namespace Barotrauma findDivingGear = null; seekGapsTimer = 0; TargetGap = null; - cannotFollow = false; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs index 8a6b8cea7..9ec761e4c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; namespace Barotrauma { @@ -20,6 +21,8 @@ namespace Barotrauma private Item Container { get; } private ItemContainer ItemContainer { get; } private ImmutableArray TargetContainerTags { get; } + private ImmutableHashSet ValidContainableItemIdentifiers { get; } + private static Dictionary> AllValidContainableItemIdentifiers { get; } = new Dictionary>(); private int itemIndex = 0; private AIObjectiveDecontainItem decontainObjective; @@ -47,6 +50,111 @@ namespace Barotrauma abandonGetItemDialogueIdentifier = optionSpecificDialogueIdentifier; } } + ValidContainableItemIdentifiers = GetValidContainableItemIdentifiers(); + if (ValidContainableItemIdentifiers.None()) + { +#if DEBUG + DebugConsole.ShowError($"No valid containable item identifiers found for the Load Item objective targeting {Container}"); +#endif + Abandon = true; + return; + } + } + + private enum CheckStatus { Unfinished, Finished } + + private ImmutableHashSet GetValidContainableItemIdentifiers() + { + if (AllValidContainableItemIdentifiers.TryGetValue(Container.Prefab, out var existingIdentifiers)) + { + return existingIdentifiers; + } + // Status effects are often used to alter item condition so using the Containable Item Identifiers directly can lead to unwanted results + // For example, placing welding fuel tanks inside oxygen tank shelves + bool useDefaultContainableItemIdentifiers = true; + var potentialContainablePrefabs = MapEntityPrefab.List + .Where(mep => mep is ItemPrefab ip && ItemContainer.ContainableItemIdentifiers.Any(i => i == ip.Identifier || ip.Tags.Contains(i))) + .Cast(); + var validContainableItemIdentifiers = new HashSet(); + foreach (var component in Container.Components) + { + if (CheckComponent() == CheckStatus.Finished) + { + break; + } + CheckStatus CheckComponent() + { + if (component.statusEffectLists != null) + { + foreach (var (_, statusEffects) in component.statusEffectLists) + { + if (CheckStatusEffects(statusEffects) == CheckStatus.Finished) + { + return CheckStatus.Finished; + } + } + } + if (component is ItemContainer itemContainer && itemContainer.ContainableItems != null) + { + foreach (var item in itemContainer.ContainableItems) + { + if (CheckStatusEffects(item.statusEffects) == CheckStatus.Finished) + { + return CheckStatus.Finished; + } + } + } + return CheckStatus.Unfinished; + CheckStatus CheckStatusEffects(IEnumerable statusEffects) + { + if (statusEffects == null) { return CheckStatus.Unfinished; } + foreach (var statusEffect in statusEffects) + { + if ((statusEffect.TargetIdentifiers == null || statusEffect.TargetIdentifiers.None()) && !statusEffect.HasConditions) { continue; } + switch (TargetItemCondition) + { + case AIObjectiveLoadItems.ItemCondition.Empty: + if (!statusEffect.ReducesItemCondition()) { continue; } + break; + case AIObjectiveLoadItems.ItemCondition.Full: + if (!statusEffect.IncreasesItemCondition()) { continue; } + break; + default: + continue; + } + useDefaultContainableItemIdentifiers = false; + if (statusEffect.TargetIdentifiers != null) + { + foreach (string target in statusEffect.TargetIdentifiers) + { + foreach (var prefab in potentialContainablePrefabs) + { + if (CheckPrefab(prefab, () => prefab.Tags.Contains(target)) == CheckStatus.Finished) { return CheckStatus.Finished; } + } + } + } + foreach (var prefab in potentialContainablePrefabs) + { + if (CheckPrefab(prefab, () => statusEffect.MatchesTagConditionals(prefab)) == CheckStatus.Finished) { return CheckStatus.Finished; } + } + CheckStatus CheckPrefab(ItemPrefab prefab, Func isValid) + { + if (validContainableItemIdentifiers.Contains(prefab.Identifier)) { return CheckStatus.Unfinished; } + if (!isValid()) { return CheckStatus.Unfinished; } + validContainableItemIdentifiers.Add(prefab.Identifier); + if (potentialContainablePrefabs.Any(p => !validContainableItemIdentifiers.Contains(p.Identifier))) { return CheckStatus.Unfinished; } + return CheckStatus.Finished; + } + } + return CheckStatus.Unfinished; + } + } + } + var identifiers = useDefaultContainableItemIdentifiers ? + potentialContainablePrefabs.Select(p => p.Identifier).ToImmutableHashSet() : + validContainableItemIdentifiers.ToImmutableHashSet(); + AllValidContainableItemIdentifiers.Add(Container.Prefab, identifiers); + return identifiers; } protected override float GetPriority() @@ -116,7 +224,7 @@ namespace Barotrauma base.Update(deltaTime); if (targetItem == null) { - if (character.FindItem(ref itemIndex, out Item item, identifiers: ItemContainer.ContainableItemIdentifiers, ignoreBroken: false, customPredicate: IsValidContainable, customPriorityFunction: GetConditionBasedPriority)) + if (character.FindItem(ref itemIndex, out Item item, identifiers: ValidContainableItemIdentifiers, ignoreBroken: false, customPredicate: IsValidContainable, customPriorityFunction: GetPriority)) { if (item == null) { @@ -125,17 +233,19 @@ namespace Barotrauma } targetItem = item; } - // Prefer items closer to full condition when target condition is Empty, and vice versa - float GetConditionBasedPriority(Item item) + float GetPriority(Item item) { try { - return TargetItemCondition switch + // Prefer items closer to full condition when target condition is Empty, and vice versa + float conditionBasedPriority = TargetItemCondition switch { AIObjectiveLoadItems.ItemCondition.Full => MathUtils.InverseLerp(100.0f, 0.0f, item.ConditionPercentage), AIObjectiveLoadItems.ItemCondition.Empty => MathUtils.InverseLerp(0.0f, 100.0f, item.ConditionPercentage), _ => throw new NotImplementedException() }; + // Prefer items that have the same identifier as one of the already contained items + return ItemContainer.ContainsItemsWithSameIdentifier(item) ? conditionBasedPriority : conditionBasedPriority / 2; } catch (NotImplementedException) { @@ -161,10 +271,11 @@ namespace Barotrauma TryAddSubObjective(ref decontainObjective, constructor: () => new AIObjectiveDecontainItem(character, targetItem, objectiveManager, targetContainer: ItemContainer, priorityModifier: PriorityModifier) { + AbandonGetItemDialogueCondition = () => IsValidContainable(targetItem), AbandonGetItemDialogueIdentifier = abandonGetItemDialogueIdentifier, Equip = true, RemoveExistingWhenNecessary = true, - RemoveExistingPredicate = (i) => AIObjectiveLoadItems.ItemMatchesTargetCondition(i, TargetItemCondition), + RemoveExistingPredicate = (i) => !ValidContainableItemIdentifiers.Contains(i.Prefab.Identifier) || AIObjectiveLoadItems.ItemMatchesTargetCondition(i, TargetItemCondition), RemoveExistingMax = 1 }, onCompleted: () => @@ -189,6 +300,7 @@ namespace Barotrauma { if (item == null) { return false; } if (item.Removed) { return false; } + if (!ValidContainableItemIdentifiers.Contains(item.Prefab.Identifier)) { return false; } if (ignoredItems.Contains(item)) { return false; } if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; } var rootInventoryOwner = item.GetRootInventoryOwner(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs index 21720599b..55b32ce12 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs @@ -46,6 +46,7 @@ namespace Barotrauma public override bool AllowSubObjectiveSorting => true; public virtual bool InverseTargetEvaluation => false; protected virtual bool ResetWhenClearingIgnoreList => true; + protected virtual bool ForceOrderPriority => true; public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace.CleanupStackTrace()); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index 71871dfff..ef356ff70 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -417,7 +417,7 @@ namespace Barotrauma if (orderGiver == null) { return null; } newObjective = new AIObjectiveGoTo(orderGiver, character, this, repeat: true, priorityModifier: priorityModifier) { - CloseEnough = Rand.Range(80, 100), + CloseEnough = Rand.Range(80f, 100f), CloseEnoughMultiplier = Math.Min(1 + HumanAIController.CountCrew(c => c.ObjectiveManager.HasOrder(o => o.Target == orderGiver), onlyBots: true) * Rand.Range(0.8f, 1f), 4), ExtraDistanceOutsideSub = 100, ExtraDistanceWhileSwimming = 100, @@ -431,7 +431,7 @@ namespace Barotrauma case "wait": newObjective = new AIObjectiveGoTo(order.TargetSpatialEntity ?? character, character, this, repeat: true, priorityModifier: priorityModifier) { - AllowGoingOutside = character.Submarine == null || (order.TargetSpatialEntity != null && character.Submarine != order.TargetSpatialEntity.Submarine) + AllowGoingOutside = true }; break; case "return": @@ -477,6 +477,12 @@ namespace Barotrauma case "fightintruders": newObjective = new AIObjectiveFightIntruders(character, this, priorityModifier); break; + case "assaultenemy": + newObjective = new AIObjectiveFightIntruders(character, this, priorityModifier) + { + TargetCharactersInOtherSubs = true + }; + break; case "steer": var steering = (order?.TargetEntity as Item)?.GetComponent(); if (steering != null) { steering.PosToMaintain = steering.Item.Submarine?.WorldPosition; } @@ -652,7 +658,12 @@ namespace Barotrauma public bool IsOrder(AIObjective objective) { - return objective == ForcedOrder || CurrentOrders.Any(o => o.Objective == objective); + if (objective == ForcedOrder) { return true; } + foreach (var order in CurrentOrders) + { + if (order.Objective == objective) { return true; } + } + return false; } public bool HasOrders() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs index c4b2e1d1c..9bea9ed11 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs @@ -11,6 +11,7 @@ namespace Barotrauma public override string Identifier { get; set; } = "prepare"; public override string DebugTag => $"{Identifier}"; public override bool KeepDivingGearOn => true; + public override bool KeepDivingGearOnAlsoWhenInactive => true; public override bool PrioritizeIfSubObjectivesActive => true; private AIObjectiveGetItem getSingleItemObjective; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs index 720cbd730..d0c4045e8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs @@ -33,6 +33,7 @@ namespace Barotrauma if (pump.Item.Submarine == null) { return false; } if (pump.Item.CurrentHull == null) { return false; } if (pump.Item.Submarine.TeamID != character.TeamID) { return false; } + if (pump.IsAutoControlled) { return false; } if (pump.Item.ConditionPercentage <= 0) { return false; } if (pump.Item.CurrentHull.FireSources.Count > 0) { return false; } if (character.Submarine != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index 1c7786d6b..3b0b1499f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -489,7 +489,7 @@ namespace Barotrauma return Priority; } - public static IEnumerable GetSortedAfflictions(Character character) => CharacterHealth.SortAfflictionsBySeverity(character.CharacterHealth.GetAllAfflictions()); + public static IEnumerable GetSortedAfflictions(Character character, bool excludeBuffs = true) => CharacterHealth.SortAfflictionsBySeverity(character.CharacterHealth.GetAllAfflictions(), excludeBuffs); public static IEnumerable GetTreatableAfflictions(Character character) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs index 78d80ac98..31f2b4d75 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs @@ -7,11 +7,11 @@ namespace Barotrauma class AIObjectiveReturn : AIObjective { public override string Identifier { get; set; } = "return"; - private AIObjectiveGoTo moveInsideObjective, moveInCaveObjective, moveOutsideObjective; - private bool usingEscapeBehavior; - private bool isSteeringThroughGap; public Submarine ReturnTarget { get; } + private AIObjectiveGoTo moveInsideObjective, moveOutsideObjective; + private bool usingEscapeBehavior, isSteeringThroughGap; + public AIObjectiveReturn(Character character, Character orderGiver, AIObjectiveManager objectiveManager, float priorityModifier = 1.0f) : base(character, objectiveManager, priorityModifier) { ReturnTarget = GetReturnTarget(Submarine.MainSubs) ?? GetReturnTarget(Submarine.Loaded); @@ -112,7 +112,6 @@ namespace Barotrauma } if (targetHull != null) { - RemoveSubObjective(ref moveInCaveObjective); RemoveSubObjective(ref moveOutsideObjective); TryAddSubObjective(ref moveInsideObjective, constructor: () => new AIObjectiveGoTo(targetHull, character, objectiveManager) @@ -137,91 +136,41 @@ namespace Barotrauma IsCompleted = true; } } - else if (!isSteeringThroughGap && moveInCaveObjective == null && moveOutsideObjective == null) + else if (!isSteeringThroughGap && moveOutsideObjective == null) { - if (HumanAIController.IsInsideCave) + Hull targetHull = null; + float targetDistanceSquared = float.MaxValue; + bool targetIsAirlock = false; + foreach (var hull in ReturnTarget.GetHulls(false)) { - WayPoint closestOutsideWaypoint = null; - float closestDistance = float.MaxValue; - foreach (var w in WayPoint.WayPointList) + bool hullIsAirlock = hull.IsTaggedAirlock(); + if(hullIsAirlock || (!targetIsAirlock && hull.LeadsOutside(character))) { - if (w.Tunnel != null && w.Tunnel.Type == Level.TunnelType.Cave) { continue; } - if (w.linkedTo.None(l => l is WayPoint linkedWaypoint && linkedWaypoint.Tunnel?.Type == Level.TunnelType.Cave)) { continue; } - float distance = Vector2.DistanceSquared(character.WorldPosition, w.WorldPosition); - if (closestOutsideWaypoint == null || distance < closestDistance) + float distanceSquared = Vector2.DistanceSquared(character.WorldPosition, hull.WorldPosition); + if (targetHull == null || distanceSquared < targetDistanceSquared) { - closestOutsideWaypoint = w; - closestDistance = distance; + targetHull = hull; + targetDistanceSquared = distanceSquared; + targetIsAirlock = hullIsAirlock; } } - if (closestOutsideWaypoint != null) - { - RemoveSubObjective(ref moveInsideObjective); - RemoveSubObjective(ref moveOutsideObjective); - TryAddSubObjective(ref moveInCaveObjective, - constructor: () => new AIObjectiveGoTo(closestOutsideWaypoint, character, objectiveManager) - { - endNodeFilter = n => n.Waypoint == closestOutsideWaypoint, - AllowGoingOutside = true - }, - onCompleted: () => RemoveSubObjective(ref moveInCaveObjective), - onAbandon: () => Abandon = true); - } - else - { -#if DEBUG - DebugConsole.ThrowError("Error with a Return objective: no suitable main or side path node target found for 'moveOutsideObjective'"); -#endif - } + } + if (targetHull != null) + { + RemoveSubObjective(ref moveInsideObjective); + TryAddSubObjective(ref moveOutsideObjective, + constructor: () => new AIObjectiveGoTo(targetHull, character, objectiveManager) + { + AllowGoingOutside = true + }, + onCompleted: () => RemoveSubObjective(ref moveOutsideObjective), + onAbandon: () => Abandon = true); } else { - Hull targetHull = null; - float targetDistanceSquared = float.MaxValue; - bool targetIsAirlock = false; - foreach (var hull in ReturnTarget.GetHulls(false)) - { - bool hullIsAirlock = hull.IsTaggedAirlock(); - if(hullIsAirlock || (!targetIsAirlock && hull.LeadsOutside(character))) - { - float distanceSquared = Vector2.DistanceSquared(character.WorldPosition, hull.WorldPosition); - if (targetHull == null || distanceSquared < targetDistanceSquared) - { - targetHull = hull; - targetDistanceSquared = distanceSquared; - targetIsAirlock = hullIsAirlock; - } - } - } - if (targetHull != null) - { - RemoveSubObjective(ref moveInsideObjective); - RemoveSubObjective(ref moveInCaveObjective); - TryAddSubObjective(ref moveOutsideObjective, - constructor: () => new AIObjectiveGoTo(targetHull, character, objectiveManager) - { - AllowGoingOutside = true - }, - onCompleted: () => RemoveSubObjective(ref moveOutsideObjective), - onAbandon: () => Abandon = true); - } - else - { #if DEBUG - DebugConsole.ThrowError("Error with a Return objective: no suitable target for 'moveOutsideObjective'"); + DebugConsole.ThrowError("Error with a Return objective: no suitable target for 'moveOutsideObjective'"); #endif - } - } - } - else - { - if (HumanAIController.IsInsideCave) - { - RemoveSubObjective(ref moveOutsideObjective); - } - else - { - RemoveSubObjective(ref moveInCaveObjective); } } usingEscapeBehavior = shouldUseEscapeBehavior; @@ -249,7 +198,6 @@ namespace Barotrauma { base.Reset(); moveInsideObjective = null; - moveInCaveObjective = null; moveOutsideObjective = null; usingEscapeBehavior = false; isSteeringThroughGap = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index a460c39c0..4bc797d54 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -155,6 +155,9 @@ namespace Barotrauma public OrderCategory? Category { get; private set; } //legacy support + /// + /// If defined, the order can only be quick-assigned to characters with these jobs. Or if it's a report, the icon will only be displayed to characters with these jobs. + /// public readonly string[] AppropriateJobs; public readonly string[] Options; public readonly string[] HiddenOptions; @@ -177,6 +180,10 @@ namespace Barotrauma public bool IsPrefab { get; private set; } public readonly bool MustManuallyAssign; public readonly bool AutoDismiss; + /// + /// If defined, the order will be quick-assigned to characters with these jobs before characters with other jobs. + /// + public string[] PreferredJobs { get; } public readonly OrderTarget TargetPosition; @@ -221,6 +228,9 @@ namespace Barotrauma /// public int AssignmentPriority { get; } + public bool ColoredWhenControllingGiver { get; } + public bool DisplayGiverInTooltip { get; } + public static void Init() { Prefabs = new Dictionary(); @@ -324,6 +334,7 @@ namespace Barotrauma ControllerTags = orderElement.GetAttributeStringArray("controllertags", new string[0]); TargetAllCharacters = orderElement.GetAttributeBool("targetallcharacters", false); AppropriateJobs = orderElement.GetAttributeStringArray("appropriatejobs", new string[0]); + PreferredJobs = orderElement.GetAttributeStringArray("preferredjobs", new string[0]); Options = orderElement.GetAttributeStringArray("options", new string[0]); HiddenOptions = orderElement.GetAttributeStringArray("hiddenoptions", new string[0]); AllOptions = Options.Concat(HiddenOptions).ToArray(); @@ -374,7 +385,7 @@ namespace Barotrauma } if (OptionNames.Count != Options.Length) { - DebugConsole.ThrowError("Error in Order " + Name + " - the number of option names doesn't match the number of options."); + DebugConsole.AddWarning("Error in Order " + Name + " - the number of option names doesn't match the number of options."); OptionNames.Clear(); Options.ForEach(o => OptionNames.Add(o, o)); } @@ -404,8 +415,10 @@ namespace Barotrauma MustManuallyAssign = orderElement.GetAttributeBool("mustmanuallyassign", false); IsIgnoreOrder = Identifier == "ignorethis" || Identifier == "unignorethis"; DrawIconWhenContained = orderElement.GetAttributeBool("displayiconwhencontained", false); - AutoDismiss = orderElement.GetAttributeBool("autodismiss", Category == OrderCategory.Movement); + AutoDismiss = orderElement.GetAttributeBool("autodismiss", Category == OrderCategory.Operate || Category == OrderCategory.Movement); AssignmentPriority = Math.Clamp(orderElement.GetAttributeInt("assignmentpriority", 100), 0, 100); + ColoredWhenControllingGiver = orderElement.GetAttributeBool("coloredwhencontrollinggiver", false); + DisplayGiverInTooltip = orderElement.GetAttributeBool("displaygiverintooltip", false); } /// @@ -430,6 +443,7 @@ namespace Barotrauma ControllerTags = prefab.ControllerTags; TargetAllCharacters = prefab.TargetAllCharacters; AppropriateJobs = prefab.AppropriateJobs; + PreferredJobs = prefab.PreferredJobs; FadeOutTime = prefab.FadeOutTime; MustSetTarget = prefab.MustSetTarget; CanBeGeneralized = prefab.CanBeGeneralized; @@ -441,6 +455,9 @@ namespace Barotrauma Hidden = prefab.Hidden; IgnoreAtOutpost = prefab.IgnoreAtOutpost; AssignmentPriority = prefab.AssignmentPriority; + AutoDismiss = prefab.AutoDismiss; + DisplayGiverInTooltip = prefab.DisplayGiverInTooltip; + ColoredWhenControllingGiver = prefab.ColoredWhenControllingGiver; OrderGiver = orderGiver; TargetEntity = targetEntity; @@ -481,34 +498,39 @@ namespace Barotrauma WallSectionIndex = sectionIndex; TargetType = OrderTargetType.WallSection; } - - public bool HasAppropriateJob(Character character) - { - if (character.Info == null || character.Info.Job == null) { return false; } - if (character.Info.Job.Prefab.AppropriateOrders.Any(appropriateOrderId => Identifier == appropriateOrderId)) { return true; } - if (!JobPrefab.Prefabs.Any(jp => jp.AppropriateOrders.Contains(Identifier)) && - (AppropriateJobs == null || AppropriateJobs.Length == 0)) + private bool HasSpecifiedJob(Character character, string[] jobs) + { + if (jobs == null || jobs.Length == 0) { return false; } + string jobIdentifier = character?.Info?.Job?.Prefab?.Identifier; + if (string.IsNullOrEmpty(jobIdentifier)) { return false; } + for (int i = 0; i < jobs.Length; i++) { - return true; - } - for (int i = 0; i < AppropriateJobs.Length; i++) - { - if (character.Info.Job.Prefab.Identifier.Equals(AppropriateJobs[i], StringComparison.OrdinalIgnoreCase)) { return true; } + if (jobIdentifier.Equals(jobs[i], StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } - public string GetChatMessage(string targetCharacterName, string targetRoomName, bool givingOrderToSelf, string orderOption = "", int? priority = null) + public bool HasAppropriateJob(Character character) => HasSpecifiedJob(character, AppropriateJobs); + + public bool HasPreferredJob(Character character) => HasSpecifiedJob(character, PreferredJobs); + + public string GetChatMessage(string targetCharacterName, string targetRoomName, bool givingOrderToSelf, string orderOption = "", bool isNewOrder = true) { - priority ??= CharacterInfo.HighestManualOrderPriority; - // If the order has a lesser priority, it means we are rearranging character orders - if (!TargetAllCharacters && priority != CharacterInfo.HighestManualOrderPriority && Identifier != "dismissed") + if (!TargetAllCharacters && !isNewOrder && Identifier != "dismissed") { - return TextManager.GetWithVariable("rearrangedorders", "[name]", targetCharacterName ?? string.Empty, returnNull: true) ?? string.Empty; + // Use special dialogue when we're rearranging character orders + if (!givingOrderToSelf) + { + return TextManager.GetWithVariable("rearrangedorders", "[name]", targetCharacterName ?? string.Empty, returnNull: true) ?? string.Empty; + } + else + { + // Say nothing when rearranging the orders of the character you're controlling + return string.Empty; + } } - string messageTag = $"{(givingOrderToSelf && !TargetAllCharacters ? "OrderDialogSelf" : "OrderDialog")}"; - messageTag += $".{Identifier}"; + string messageTag = $"{(givingOrderToSelf && !TargetAllCharacters ? "OrderDialogSelf" : "OrderDialog")}.{Identifier}"; if (!string.IsNullOrEmpty(orderOption)) { if (Identifier != "dismissed") diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs index 540e4135d..3a8186c25 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs @@ -333,7 +333,6 @@ namespace Barotrauma //if searching for a path inside the sub, make sure the waypoint is visible if (checkVisibility && isCharacter) { - if (node.Waypoint.isObstructed) { return false; } var body = Submarine.PickBody(rayStart, node.TempPosition, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs); if (body != null) @@ -350,6 +349,7 @@ namespace Barotrauma { if (nodeFilter != null && !nodeFilter(node)) { return false; } if (startNodeFilter != null && !startNodeFilter(node)) { return false; } + if (node.Waypoint.isObstructed) { return false; } // Always check the visibility for the start node if (!IsWaypointVisible(node, start)) { return false; } if (node.IsBlocked()) { return false; } @@ -364,6 +364,7 @@ namespace Barotrauma { if (nodeFilter != null && !nodeFilter(node)) { return false; } if (endNodeFilter != null && !endNodeFilter(node)) { return false; } + if (node.Waypoint.isObstructed) { return false; } // Only check the visibility for the end node when allowed (fix leaks) if (!IsWaypointVisible(node, end, checkVisibility: checkVisibility)) { return false; } if (node.IsBlocked()) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs index 07104f091..079b59da6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs @@ -134,6 +134,7 @@ namespace Barotrauma aggregate += Items[i].Commonness; if (aggregate >= r && Items[i].Prefab != null) { + GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":PetProducedItem:" + pet.AiController.Character.SpeciesName + ":" + Items[i].Prefab.Identifier); Entity.Spawner.AddToSpawnQueue(Items[i].Prefab, pet.AiController.Character.WorldPosition); break; } @@ -200,6 +201,8 @@ namespace Barotrauma break; } } + + GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":PetSpawned:" + aiController.Character.SpeciesName); } public StatusIndicatorType GetCurrentStatusIndicatorType() @@ -210,23 +213,44 @@ namespace Barotrauma return StatusIndicatorType.None; } - public bool OnEat(IEnumerable tags, float amount) + public bool OnEat(Item item) + { + bool success = OnEat(item.GetTags()); + if (success) + { + GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":PetEat:" + AiController.Character.SpeciesName + ":" + item.prefab.Identifier); + } + return success; + } + + public bool OnEat(Character character) + { + if (character == null || !character.IsDead) { return false; } + bool success = OnEat("dead"); + if (success) + { + GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":PetEat:" + AiController.Character.SpeciesName + ":" + character.SpeciesName); + } + return success; + } + + private bool OnEat(IEnumerable tags) { foreach (string tag in tags) { - if (OnEat(tag, amount)) { return true; } + if (OnEat(tag)) { return true; } } return false; } - public bool OnEat(string tag, float amount) + private bool OnEat(string tag) { for (int i = 0; i < foods.Count; i++) { if (tag.Equals(foods[i].Tag, System.StringComparison.OrdinalIgnoreCase)) { - Hunger += foods[i].Hunger * amount; - Happiness += foods[i].Happiness * amount; + Hunger += foods[i].Hunger; + Happiness += foods[i].Happiness; #if CLIENT AiController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.5f); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs index cb99e69b9..8aca8933a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs @@ -55,7 +55,7 @@ namespace Barotrauma CommandingCharacter.Speak(SuggestedOrderPrefab.GetChatMessage(OrderedCharacter.Name, "", false), minDurationBetweenSimilar: 5); } CurrentOrder = new Order(SuggestedOrderPrefab, TargetItem, TargetItemComponent, CommandingCharacter); - OrderedCharacter.SetOrder(CurrentOrder, Option, priority: 3, CommandingCharacter, CommandingCharacter != OrderedCharacter); + OrderedCharacter.SetOrder(CurrentOrder, Option, priority: CharacterInfo.HighestManualOrderPriority, CommandingCharacter, CommandingCharacter != OrderedCharacter); OrderedCharacter.Speak(TextManager.Get("DialogAffirmative"), delay: 1.0f, minDurationBetweenSimilar: 5); } TimeSinceLastAttempt = 0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs index fb2aa21b9..c227317ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs @@ -22,7 +22,13 @@ namespace Barotrauma public override void CalculateImportanceSpecific() { - if (TargetItemComponent is Turret turret && !turret.HasPowerToShoot()) { return; } + if (TargetItemComponent is Turret turret && !turret.HasPowerToShoot()) + { + //operate (= recharge the turrets) with low priority if they're out of power + //if something else (issues with reactor or the electrical grid) is preventing them from being charged, fixing those issues should take priority + Importance = ShipCommandManager.MinimumIssueThreshold * 1.05f; + return; + } targetingImportances.Clear(); foreach (Character character in shipCommandManager.EnemyCharacters) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs index 9ffb51694..2cc21e90e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs @@ -51,7 +51,7 @@ namespace Barotrauma private const float RamTimerMax = 17.5f; public readonly List ShipIssueWorkers = new List(); - private const float MinimumIssueThreshold = 10f; + public const float MinimumIssueThreshold = 10f; private const float IssueDevotionBuffer = 5f; private float decisionTimer = 6f; @@ -75,7 +75,7 @@ namespace Barotrauma public void Update(float deltaTime) { - if (!Active) { return; } + if (!Active || character.IsArrested) { return; } decisionTimer -= deltaTime; if (decisionTimer <= 0.0f) { @@ -344,7 +344,6 @@ namespace Barotrauma ShipIssueWorkers.Clear(); - // could have support for multiple reactors, todo m61 if (CommandedSubmarine.GetItems(false).Find(i => i.HasTag("reactor") && !i.NonInteractable)?.GetComponent() is Reactor reactor) { ShipIssueWorkers.Add(new ShipIssueWorkerPowerUpReactor(this, Order.GetPrefab("operatereactor"), reactor.Item, reactor, "powerup")); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs index dc6e181ae..3fe39c57b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs @@ -254,7 +254,7 @@ namespace Barotrauma private void SpawnInitialCells() { - int brainRoomCells = Rand.Range(MinCellsPerBrainRoom, MaxCellsPerRoom); + int brainRoomCells = Rand.Range(MinCellsPerBrainRoom, MaxCellsPerRoom + 1); if (brain.CurrentHull?.WaterPercentage >= MinWaterLevel) { for (int i = 0; i < brainRoomCells; i++) @@ -262,12 +262,12 @@ namespace Barotrauma if (!TrySpawnCell(out _, brain.CurrentHull)) { break; } } } - int cellsInside = Rand.Range(MinCellsInside, MaxCellsInside); + int cellsInside = Rand.Range(MinCellsInside, MaxCellsInside + 1); for (int i = 0; i < cellsInside; i++) { if (!TrySpawnCell(out _)) { break; } } - int cellsOutside = Rand.Range(MinCellsOutside, MaxCellsOutside); + int cellsOutside = Rand.Range(MinCellsOutside, MaxCellsOutside + 1); // If we failed to spawn some of the cells in the brainroom/inside, spawn some extra cells outside. cellsOutside = Math.Clamp(cellsOutside + brainRoomCells + cellsInside - protectiveCells.Count, cellsOutside, MaxCellsOutside); for (int i = 0; i < cellsOutside; i++) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index f9a0b6c2e..65597452e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -269,6 +269,11 @@ namespace Barotrauma } } + public float GetHeightFromFloor() => GetColliderBottom().Y - FloorY; + + // We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative. + public bool IsAboveFloor => GetHeightFromFloor() > -0.1f; + public void UpdateUseItem(bool allowMovement, Vector2 handWorldPos) { useItemTimer = 0.5f; @@ -332,7 +337,7 @@ namespace Barotrauma aimingMelee = aimMelee; if (character.Stun > 0.0f || character.IsIncapacitated) { - aim = false; + aim = false; } //calculate the handle positions diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index e829475c3..074817047 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -346,7 +346,12 @@ namespace Barotrauma Vector2 limbDiff = attackSimPosition - mouthPos; float extent = Math.Max(mouthLimb.body.GetMaxExtent(), 1); - if (limbDiff.LengthSquared() < extent * extent) + bool tooFar = character.InWater ? limbDiff.LengthSquared() > extent * extent : limbDiff.X > extent; + if (tooFar) + { + character.SelectedCharacter = null; + } + else { //pull the target character to the position of the mouth //(+ make the force fluctuate to waggle the character a bit) @@ -382,55 +387,55 @@ namespace Barotrauma mouthLimb.body.ApplyLinearImpulse(Vector2.UnitY * force * 2, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); mouthLimb.body.ApplyTorque(-force * 50); } - var jaw = GetLimb(LimbType.Jaw); - if (jaw != null) - { - jaw.body.ApplyTorque(-(float)Math.Sin(eatTimer * 150) * jaw.Mass * 25); - } - character.ApplyStatusEffects(ActionType.OnEating, deltaTime); - - float particleFrequency = MathHelper.Clamp(eatSpeed / 2, 0.02f, 0.5f); - if (Rand.Value() < particleFrequency / 6) + if (Character.CanEat && target.IsDead) { - target.AnimController.MainLimb.AddDamage(target.SimPosition, dmg, 0, 0, false); - } - if (Rand.Value() < particleFrequency) - { - target.AnimController.MainLimb.AddDamage(target.SimPosition, 0, dmg, 0, false); - } - if (eatTimer % 1.0f < 0.5f && (eatTimer - deltaTime * eatSpeed) % 1.0f > 0.5f) - { - static bool CanBeSevered(LimbJoint j) => !j.IsSevered && j.CanBeSevered && j.LimbA != null && !j.LimbA.IsSevered && j.LimbB != null && !j.LimbB.IsSevered; - //keep severing joints until there is only one limb left - var nonSeveredJoints = target.AnimController.LimbJoints.Where(CanBeSevered); - if (nonSeveredJoints.None()) + var jaw = GetLimb(LimbType.Jaw); + if (jaw != null) { - //small monsters don't eat the contents of the character's inventory - if (Mass < target.AnimController.Mass) - { - target.Inventory?.AllItemsMod.ForEach(it => it?.Drop(dropper: null)); - } - - //only one limb left, the character is now full eaten - Entity.Spawner?.AddToRemoveQueue(target); - - if (Character.AIController is EnemyAIController enemyAi) - { - enemyAi.PetBehavior?.OnEat("dead", 1.0f); - } - - character.SelectedCharacter = null; + jaw.body.ApplyTorque(-(float)Math.Sin(eatTimer * 150) * jaw.Mass * 25); } - else //sever a random joint + + character.ApplyStatusEffects(ActionType.OnEating, deltaTime); + + float particleFrequency = MathHelper.Clamp(eatSpeed / 2, 0.02f, 0.5f); + if (Rand.Value() < particleFrequency / 6) { - target.AnimController.SeverLimbJoint(nonSeveredJoints.GetRandom()); + target.AnimController.MainLimb.AddDamage(target.SimPosition, dmg, 0, 0, false); } - } - } - else - { - character.SelectedCharacter = null; + if (Rand.Value() < particleFrequency) + { + target.AnimController.MainLimb.AddDamage(target.SimPosition, 0, dmg, 0, false); + } + if (eatTimer % 1.0f < 0.5f && (eatTimer - deltaTime * eatSpeed) % 1.0f > 0.5f) + { + static bool CanBeSevered(LimbJoint j) => !j.IsSevered && j.CanBeSevered && j.LimbA != null && !j.LimbA.IsSevered && j.LimbB != null && !j.LimbB.IsSevered; + //keep severing joints until there is only one limb left + var nonSeveredJoints = target.AnimController.LimbJoints.Where(CanBeSevered); + if (nonSeveredJoints.None()) + { + //small monsters don't eat the contents of the character's inventory + if (Mass < target.AnimController.Mass) + { + target.Inventory?.AllItemsMod.ForEach(it => it?.Drop(dropper: null)); + } + + //only one limb left, the character is now full eaten + Entity.Spawner?.AddToRemoveQueue(target); + + if (Character.AIController is EnemyAIController enemyAi) + { + enemyAi.PetBehavior?.OnEat(target); + } + + character.SelectedCharacter = null; + } + else //sever a random joint + { + target.AnimController.SeverLimbJoint(nonSeveredJoints.GetRandom()); + } + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 1fa746db9..52048369d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -533,6 +533,8 @@ namespace Barotrauma bool onSlope = Math.Abs(movement.X) > 0.01f && Math.Abs(floorNormal.X) > 0.1f && Math.Sign(floorNormal.X) != Math.Sign(movement.X); + bool movingHorizontally = !MathUtils.NearlyEqual(TargetMovement.X, 0.0f); + if (Stairs != null || onSlope) { torso.PullJointWorldAnchorB = new Vector2( @@ -562,10 +564,8 @@ namespace Barotrauma if (!torso.Disabled) { - if (TorsoPosition.HasValue) - { - y += TorsoPosition.Value; - } + if (TorsoPosition.HasValue) { y += TorsoPosition.Value; } + if (Crouching && !movingHorizontally) { y -= HumanCrouchParams.MoveDownAmountWhenStationary; } torso.PullJointWorldAnchorB = MathUtils.SmoothStep(torso.SimPosition, new Vector2(footMid + movement.X * TorsoLeanAmount, y), getUpForce); @@ -574,10 +574,8 @@ namespace Barotrauma if (!head.Disabled) { y = colliderPos.Y + stepLift * CurrentGroundedParams.StepLiftHeadMultiplier; - if (HeadPosition.HasValue) - { - y += HeadPosition.Value; - } + if (HeadPosition.HasValue) { y += HeadPosition.Value; } + if (Crouching && !movingHorizontally) { y -= HumanCrouchParams.MoveDownAmountWhenStationary; } head.PullJointWorldAnchorB = MathUtils.SmoothStep(head.SimPosition, new Vector2(footMid + movement.X * HeadLeanAmount, y), getUpForce * 1.2f); @@ -593,12 +591,15 @@ namespace Barotrauma { float torsoAngle = TorsoAngle.Value; float herpesStrength = character.CharacterHealth.GetAfflictionStrength("spaceherpes"); + if (Crouching && !movingHorizontally) { torsoAngle -= HumanCrouchParams.ExtraTorsoAngleWhenStationary; } torsoAngle -= herpesStrength / 150.0f; torso.body.SmoothRotate(torsoAngle * Dir, CurrentGroundedParams.TorsoTorque); } if (HeadAngle.HasValue) { - head.body.SmoothRotate(HeadAngle.Value * Dir, CurrentGroundedParams.HeadTorque); + float headAngle = HeadAngle.Value; + if (Crouching && !movingHorizontally) { headAngle -= HumanCrouchParams.ExtraHeadAngleWhenStationary; } + head.body.SmoothRotate(headAngle * Dir, CurrentGroundedParams.HeadTorque); } if (!onGround) @@ -616,8 +617,7 @@ namespace Barotrauma Vector2 waistPos = waist != null ? waist.SimPosition : torso.SimPosition; - //moving horizontally - if (TargetMovement.X != 0.0f) + if (movingHorizontally) { //progress the walking animation WalkPos -= MathHelper.ToRadians(CurrentAnimationParams.CycleSpeed) * walkCycleMultiplier * movement.X; @@ -808,7 +808,8 @@ namespace Barotrauma if (head == null) { return; } if (torso == null) { return; } - if (currentHull != null) + //check both hulls: the hull whose coordinate space the ragdoll is in, and the hull whose bounds the character's origin actually is inside + if (currentHull != null && character.CurrentHull != null) { float surfacePos = currentHull.Surface; float surfaceThreshold = ConvertUnits.ToDisplayUnits(Collider.SimPosition.Y + 1.0f); @@ -816,7 +817,7 @@ namespace Barotrauma //and use its water surface instead of the current hull's if (currentHull.Rect.Y - currentHull.Surface < 5.0f) { - GetSurfacePos(CurrentHull, ref surfacePos); + GetSurfacePos(currentHull, ref surfacePos); void GetSurfacePos(Hull hull, ref float prevSurfacePos) { if (prevSurfacePos > surfaceThreshold) { return; } @@ -834,7 +835,7 @@ namespace Barotrauma foreach (var linkedTo in gap.linkedTo) { - if (linkedTo is Hull otherHull && otherHull != hull) + if (linkedTo is Hull otherHull && otherHull != hull && otherHull != currentHull) { prevSurfacePos = Math.Max(surfacePos, otherHull.Surface); GetSurfacePos(otherHull, ref prevSurfacePos); @@ -888,7 +889,6 @@ namespace Barotrauma { Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition); Vector2 diff = (mousePos - torso.SimPosition) * Dir; - TargetMovement = new Vector2(0.0f, -0.1f); float newRotation = MathUtils.VectorToAngle(diff); Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index b36776f97..d6719aa63 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -305,7 +305,27 @@ namespace Barotrauma public abstract float? TorsoPosition { get; } public abstract float? TorsoAngle { get; } - public float ImpactTolerance => RagdollParams.ImpactTolerance; + float? impactTolerance; + public float ImpactTolerance + { + get + { + if (impactTolerance == null) + { + impactTolerance = RagdollParams.ImpactTolerance; + if (character.Params.VariantFile != null) + { + float? tolerance = character.Params.VariantFile.Root.GetChildElement("ragdoll")?.GetAttributeFloat("impacttolerance", impactTolerance.Value); + if (tolerance.HasValue) + { + impactTolerance = tolerance; + } + } + } + return impactTolerance.Value; + } + } + public bool Draggable => RagdollParams.Draggable; public bool CanEnterSubmarine => RagdollParams.CanEnterSubmarine; @@ -764,8 +784,8 @@ namespace Barotrauma if (limbDiff.LengthSquared() < 0.0001f) { limbDiff = Rand.Vector(1.0f); } limbDiff = Vector2.Normalize(limbDiff); float mass = limbJoint.BodyA.Mass + limbJoint.BodyB.Mass; - limbJoint.LimbA.body.ApplyLinearImpulse(limbDiff * mass, (limbJoint.LimbA.SimPosition + limbJoint.LimbB.SimPosition) / 2.0f); - limbJoint.LimbB.body.ApplyLinearImpulse(-limbDiff * mass, (limbJoint.LimbA.SimPosition + limbJoint.LimbB.SimPosition) / 2.0f); + limbJoint.LimbA.body.ApplyLinearImpulse(limbDiff * Math.Min(mass, limbJoint.BodyA.Mass * 500), (limbJoint.LimbA.SimPosition + limbJoint.LimbB.SimPosition) / 2.0f); + limbJoint.LimbB.body.ApplyLinearImpulse(-limbDiff * Math.Min(mass, limbJoint.BodyB.Mass * 500), (limbJoint.LimbA.SimPosition + limbJoint.LimbB.SimPosition) / 2.0f); connectedLimbs.Clear(); checkedJoints.Clear(); @@ -1180,13 +1200,13 @@ namespace Barotrauma { headInWater = false; inWater = false; + RefreshFloorY(ignoreStairs: Stairs == null); if (currentHull.WaterVolume > currentHull.Volume * 0.95f) { inWater = true; } else { - RefreshFloorY(ignoreStairs: Stairs == null); float waterSurface = ConvertUnits.ToSimUnits(currentHull.Surface); if (targetMovement.Y < 0.0f) { @@ -1841,7 +1861,7 @@ namespace Barotrauma float sin = (float)Math.Sin(mouthLimb.Rotation); Vector2 bodySize = mouthLimb.body.GetSize(); Vector2 offset = new Vector2(mouthLimb.MouthPos.X * bodySize.X / 2, mouthLimb.MouthPos.Y * bodySize.Y / 2); - return mouthLimb.SimPosition + new Vector2(offset.X * cos - offset.Y * sin, offset.X * sin + offset.Y * cos) * mouthLimb.Scale * RagdollParams.LimbScale; + return mouthLimb.SimPosition + new Vector2(offset.X * cos - offset.Y * sin, offset.X * sin + offset.Y * cos); } public Vector2 GetColliderBottom() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index e07f73f0c..18b72752a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -101,11 +101,21 @@ namespace Barotrauma [Serialize(false, true, description: "Should the AI try to steer away from the target when aiming with this attack? Best combined with PassiveAggressive behavior."), Editable] public bool Retreat { get; private set; } + private float _range; [Serialize(0.0f, true, description: "The min distance from the attack limb to the target before the AI tries to attack."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f)] - public float Range { get; set; } + public float Range + { + get => _range * RangeMultiplier; + set => _range = value; + } + private float _damageRange; [Serialize(0.0f, true, description: "The min distance from the attack limb to the target to do damage. In distance-based hit detection, the hit will be registered as soon as the target is within the damage range, unless the attack duration has expired."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f)] - public float DamageRange { get; set; } + public float DamageRange + { + get => _damageRange * RangeMultiplier; + set => _damageRange = value; + } [Serialize(0.25f, true, description: "An approximation of the attack duration. Effectively defines the time window in which the hit can be registered. If set to too low value, it's possible that the attack won't hit the target in time."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, DecimalCount = 2)] public float Duration { get; private set; } @@ -130,6 +140,9 @@ namespace Barotrauma set => _structureDamage = value; } + [Serialize(true, true), Editable] + public bool EmitStructureDamageParticles { get; private set; } + private float _itemDamage; [Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] public float ItemDamage @@ -142,10 +155,20 @@ namespace Barotrauma public float Penetration { get; private set; } /// - /// Currently only used with variants. Used for multiplying all the damage. + /// Used for multiplying all the damage. /// public float DamageMultiplier { get; set; } = 1; + /// + /// Used for multiplying all the ranges. + /// + public float RangeMultiplier { get; set; } = 1; + + /// + /// Used for multiplying the physics forces. + /// + public float ImpactMultiplier { get; set; } = 1; + [Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] public float LevelWallDamage { get; set; } @@ -311,11 +334,11 @@ namespace Barotrauma return totalDamage * DamageMultiplier; } - public Attack(float damage, float bleedingDamage, float burnDamage, float structureDamage, float itemDamage, float range = 0.0f, float penetration = 0f) + public Attack(float damage, float bleedingDamage, float burnDamage, float structureDamage, float itemDamage, float range = 0.0f) { - if (damage > 0.0f) Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(damage), null); - if (bleedingDamage > 0.0f) Afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamage), null); - if (burnDamage > 0.0f) Afflictions.Add(AfflictionPrefab.Burn.Instantiate(burnDamage), null); + if (damage > 0.0f) { Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(damage), null); } + if (bleedingDamage > 0.0f) { Afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamage), null); } + if (burnDamage > 0.0f) { Afflictions.Add(AfflictionPrefab.Burn.Instantiate(burnDamage), null); } Range = range; DamageRange = range; @@ -438,7 +461,7 @@ namespace Barotrauma ReloadAfflictions(element); } - public AttackResult DoDamage(Character attacker, IDamageable target, Vector2 worldPosition, float deltaTime, bool playSound = true, PhysicsBody sourceBody = null) + public AttackResult DoDamage(Character attacker, IDamageable target, Vector2 worldPosition, float deltaTime, bool playSound = true, PhysicsBody sourceBody = null, Limb sourceLimb = null) { Character targetCharacter = target as Character; if (OnlyHumans) @@ -463,10 +486,10 @@ namespace Barotrauma foreach (StatusEffect effect in statusEffects) { effect.sourceBody = sourceBody; - // 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 if (effect.HasTargetType(StatusEffect.TargetType.This)) { - effect.Apply(effectType, deltaTime, attacker, attacker, worldPosition); + // 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 (targetCharacter != null) { @@ -503,7 +526,7 @@ namespace Barotrauma } readonly List targets = new List(); - public AttackResult DoDamageToLimb(Character attacker, Limb targetLimb, Vector2 worldPosition, float deltaTime, bool playSound = true, PhysicsBody sourceBody = null) + public AttackResult DoDamageToLimb(Character attacker, Limb targetLimb, Vector2 worldPosition, float deltaTime, bool playSound = true, PhysicsBody sourceBody = null, Limb sourceLimb = null) { if (targetLimb == null) { @@ -530,7 +553,7 @@ namespace Barotrauma effect.sourceBody = sourceBody; if (effect.HasTargetType(StatusEffect.TargetType.This)) { - effect.Apply(effectType, deltaTime, attacker, attacker); + effect.Apply(effectType, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity); } if (effect.HasTargetType(StatusEffect.TargetType.Character)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 324c29a6a..47874398f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -261,9 +261,16 @@ namespace Barotrauma public AttackResult LastDamage; + public Dictionary ItemSelectedDurations + { + get { return itemSelectedDurations; } + } + private readonly Dictionary itemSelectedDurations = new Dictionary(); + private double itemSelectedTime; + public float InvisibleTimer; - private CharacterPrefab prefab; + private readonly CharacterPrefab prefab; public readonly CharacterParams Params; public string SpeciesName => Params?.SpeciesName ?? "null"; @@ -496,7 +503,7 @@ namespace Barotrauma get { return cursorPosition; } set { - if (!MathUtils.IsValid(value)) return; + if (!MathUtils.IsValid(value)) { return; } cursorPosition = value; } } @@ -682,8 +689,8 @@ namespace Barotrauma get { return CharacterHealth.BloodlossAmount; } set { - if (!MathUtils.IsValid(value)) return; - CharacterHealth.BloodlossAmount = MathHelper.Clamp(value, 0.0f, 100.0f); + if (!MathUtils.IsValid(value)) { return; } + CharacterHealth.BloodlossAmount = value; } } @@ -700,7 +707,7 @@ namespace Barotrauma { get { - if (!CanSpeak || IsUnconscious || Stun > 0.0f || IsDead) { return 100.0f; } + if (!CanSpeak || IsUnconscious || IsKnockedDown) { return 100.0f; } return speechImpediment; } set @@ -737,9 +744,7 @@ namespace Barotrauma get => _selectedConstruction; set { -#if CLIENT var prevSelectedConstruction = _selectedConstruction; -#endif _selectedConstruction = value; #if CLIENT HintManager.OnSetSelectedConstruction(this, prevSelectedConstruction, _selectedConstruction); @@ -755,6 +760,19 @@ namespace Barotrauma } } #endif + if (prevSelectedConstruction == null && _selectedConstruction != null) + { + itemSelectedTime = Timing.TotalTime; + } + else if (prevSelectedConstruction != null && _selectedConstruction == null && itemSelectedTime > 0) + { + if (!itemSelectedDurations.ContainsKey(prevSelectedConstruction.Prefab)) + { + itemSelectedDurations.Add(prevSelectedConstruction.Prefab, 0); + } + itemSelectedDurations[prevSelectedConstruction.Prefab] += Timing.TotalTime - itemSelectedTime; + itemSelectedTime = 0; + } } } @@ -835,7 +853,7 @@ namespace Barotrauma } else { - return IsKnockedDown || LockHands || IsBot && TeamID != CharacterTeamType.FriendlyNPC; + return IsKnockedDown || LockHands || IsBot && IsOnPlayerTeam; } } set { canInventoryBeAccessed = value; } @@ -1234,6 +1252,10 @@ namespace Barotrauma Info.HairElement?.Elements("sprite").ForEach(s => head.OtherWearables.Add(new WearableSprite(s, WearableType.Hair))); #if CLIENT + if (info.Head?.HairWithHatElement != null) + { + head.HairWithHatSprite = new WearableSprite(info.Head?.HairWithHatElement.Element("sprite"), WearableType.Hair); + } head.EnableHuskSprite = Params.Husk; head.LoadHerpesSprite(); head.UpdateWearableTypesToHide(); @@ -1833,7 +1855,7 @@ namespace Barotrauma if (!attack.IsValidTarget(attackTarget)) { return false; } if (attackTarget is ISerializableEntity se && attackTarget is Character) { - if (attack.Conditionals.Any(c => !c.Matches(se))) { return false; } + if (attack.Conditionals.Any(c => !c.TargetSelf && !c.Matches(se))) { return false; } } } if (attack.Conditionals.Any(c => c.TargetSelf && !c.Matches(this))) { return false; } @@ -2273,17 +2295,18 @@ namespace Barotrauma } } - if (SelectedConstruction?.GetComponent()?.TargetItem == item || - HeldItems.Any(it => it.GetComponent()?.TargetItem == item)) - { - return true; - } - if (item.InteractDistance == 0.0f && !item.Prefab.Triggers.Any()) { return false; } Pickable pickableComponent = item.GetComponent(); if (pickableComponent != null && pickableComponent.Picker != this && pickableComponent.Picker != null && !pickableComponent.Picker.IsDead) { return false; } + if (SelectedConstruction?.GetComponent()?.TargetItem == item) { return true; } + //optimization: don't use HeldItems because it allocates memory and this method is executed very frequently + var heldItem1 = Inventory?.GetItemInLimbSlot(InvSlotType.RightHand); + if (heldItem1?.GetComponent()?.TargetItem == item) { return true; } + var heldItem2 = Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand); + if (heldItem2?.GetComponent()?.TargetItem == item) { return true; } + Vector2 characterDirection = Vector2.Transform(Vector2.UnitY, Matrix.CreateRotationZ(AnimController.Collider.Rotation)); Vector2 upperBodyPosition = Position + (characterDirection * 20.0f); @@ -3228,7 +3251,7 @@ namespace Barotrauma if (orderGiver != null) { - var abilityOrderedCharacter = new AbilityCharacter(this); + var abilityOrderedCharacter = new AbilityOrderedCharacter(this); orderGiver.CheckTalents(AbilityEffectType.OnGiveOrder, abilityOrderedCharacter); if (orderGiver.LastOrderedCharacter != this) @@ -3483,7 +3506,7 @@ namespace Barotrauma Limb limbHit = targetLimb; - float attackImpulse = attack.TargetImpulse + attack.TargetForce * deltaTime; + float attackImpulse = attack.TargetImpulse + attack.TargetForce * attack.ImpactMultiplier * deltaTime; AbilityAttackData attackData = new AbilityAttackData(attack, this); if (attacker != null) @@ -3521,7 +3544,7 @@ namespace Barotrauma } if (limbHit == null) { return new AttackResult(); } - Vector2 forceWorld = attack.TargetImpulseWorld + attack.TargetForceWorld; + Vector2 forceWorld = attack.TargetImpulseWorld + attack.TargetForceWorld * attack.ImpactMultiplier; if (attacker != null) { forceWorld.X *= attacker.AnimController.Dir; @@ -3550,12 +3573,12 @@ namespace Barotrauma } #endif // Don't allow beheading for monster attacks, because it happens too frequently (crawlers/tigerthreshers etc attacking each other -> they will most often target to the head) - TrySeverLimbJoints(limbHit, attack.SeverLimbsProbability, attackResult.Damage, allowBeheading: attacker == null || attacker.IsHuman || attacker.IsPlayer); + TrySeverLimbJoints(limbHit, attack.SeverLimbsProbability, attackResult.Damage, allowBeheading: attacker == null || attacker.IsHuman || attacker.IsPlayer, attacker: attacker); return attackResult; } - public void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability, float damage, bool allowBeheading) + public void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability, float damage, bool allowBeheading, Character attacker = null) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } #if DEBUG @@ -3576,8 +3599,12 @@ namespace Barotrauma foreach (LimbJoint joint in AnimController.LimbJoints) { if (!joint.CanBeSevered) { continue; } - // Limb A is where we usually create the joints from. Let's not allow severing when the "parent" limb is hit, or the head can pop off when we hit the torso, for example. - if (joint.LimbB != targetLimb) { continue; } + // Limb A is where we start creating the joint and LimbB is where the joint ends. + // Normally the joints have been created starting from the body, in which case we'd want to use LimbB e.g. to severe a hand when it's hit. + // But heads are a different case, because many characters have been created so that the head is first and then comes the rest of the body. + // If this is the case, we'll have to use LimbA to decapitate the creature when it's hit on the head. Otherwise decapitation could happen only when we hit the body, not the head. + var referenceLimb = targetLimb.type == LimbType.Head && targetLimb.Params.ID == 0 ? joint.LimbA : joint.LimbB; + if (referenceLimb != targetLimb) { continue; } float probability = severLimbsProbability; if (!IsDead) { @@ -3593,9 +3620,20 @@ namespace Barotrauma if (severed) { Limb otherLimb = joint.LimbA == targetLimb ? joint.LimbB : joint.LimbA; - otherLimb.body.ApplyLinearImpulse(targetLimb.LinearVelocity * targetLimb.Mass, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.5f); + otherLimb.body.ApplyLinearImpulse(targetLimb.LinearVelocity * targetLimb.Mass, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.5f); + if (attacker != null) + { + foreach (var statusEffect in statusEffects) + { + if (statusEffect.type == ActionType.OnSevered) { statusEffect.SetUser(attacker); } + } + foreach (var statusEffect in targetLimb.StatusEffects) + { + if (statusEffect.type == ActionType.OnSevered) { statusEffect.SetUser(attacker); } + } + } ApplyStatusEffects(ActionType.OnSevered, 1.0f); - targetLimb.ApplyStatusEffects(ActionType.OnSevered, 1.0f); + targetLimb.ApplyStatusEffects(ActionType.OnSevered, 1.0f); } } if (wasSevered && targetLimb.character.AIController is EnemyAIController enemyAI) @@ -3724,10 +3762,6 @@ namespace Barotrauma if (!wasDead) { TryAdjustAttackerSkill(attacker, CharacterHealth.Vitality - prevVitality); - if (IsDead) - { - attacker?.RecordKill(this); - } } }; if (attackResult.Damage > 0) @@ -3818,40 +3852,49 @@ namespace Barotrauma targets.AddRange(statusEffect.GetNearbyTargets(WorldPosition, targets)); statusEffect.Apply(actionType, deltaTime, this, targets); } - else + else if (statusEffect.targetLimbs != null) { - statusEffect.Apply(actionType, deltaTime, this, this); - if (statusEffect.targetLimbs != null) + foreach (var limbType in statusEffect.targetLimbs) { - foreach (var limbType in statusEffect.targetLimbs) + if (statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs)) { - if (statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs)) + // Target all matching limbs + foreach (var limb in AnimController.Limbs) { - // Target all matching limbs - foreach (var limb in AnimController.Limbs) + if (limb.IsSevered) { continue; } + if (limb.type == limbType) { - if (limb.IsSevered) { continue; } - if (limb.type == limbType) - { - statusEffect.Apply(actionType, deltaTime, this, limb); - } + statusEffect.sourceBody = limb.body; + statusEffect.Apply(actionType, deltaTime, this, limb); } } - else if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb)) + } + else if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb)) + { + // Target just the first matching limb + Limb limb = AnimController.GetLimb(limbType); + if (limb != null) { - // Target just the first matching limb - Limb limb = AnimController.GetLimb(limbType); + statusEffect.sourceBody = limb.body; statusEffect.Apply(actionType, deltaTime, this, limb); } - else if (statusEffect.HasTargetType(StatusEffect.TargetType.LastLimb)) + } + else if (statusEffect.HasTargetType(StatusEffect.TargetType.LastLimb)) + { + // Target just the last matching limb + Limb limb = AnimController.Limbs.LastOrDefault(l => l.type == limbType && !l.IsSevered && !l.Hidden); + if (limb != null) { - // Target just the last matching limb - Limb limb = AnimController.Limbs.LastOrDefault(l => l.type == limbType && !l.IsSevered && !l.Hidden); + statusEffect.sourceBody = limb.body; statusEffect.Apply(actionType, deltaTime, this, limb); } } } } + if (statusEffect.HasTargetType(StatusEffect.TargetType.This) || statusEffect.HasTargetType(StatusEffect.TargetType.Character)) + { + statusEffect.Apply(actionType, deltaTime, this, this); + } } if (actionType != ActionType.OnDamaged && actionType != ActionType.OnSevered) { @@ -3941,31 +3984,49 @@ namespace Barotrauma AnimController.Frozen = false; - if (GameAnalyticsManager.SendUserStatistics) - { - string characterType = "Unknown"; - - if (this == Controlled) - characterType = "Player"; - else if (IsRemotePlayer) - characterType = "RemotePlayer"; - else if (AIController is EnemyAIController) - characterType = "Enemy"; - else if (AIController is HumanAIController) - characterType = "AICrew"; - - string causeOfDeathStr = causeOfDeathAffliction == null ? - causeOfDeath.ToString() : causeOfDeathAffliction.Prefab.Name.Replace(" ", ""); - GameAnalyticsManager.AddDesignEvent("Kill:" + characterType + ":" + SpeciesName + ":" + causeOfDeathStr); - } - CauseOfDeath = new CauseOfDeath( causeOfDeath, causeOfDeathAffliction?.Prefab, - causeOfDeathAffliction?.Source ?? LastAttacker, LastDamageSource); + causeOfDeathAffliction?.Source, LastDamageSource); + + if (GameAnalyticsManager.SendUserStatistics) + { + string causeOfDeathStr = causeOfDeathAffliction == null ? + causeOfDeath.ToString() : causeOfDeathAffliction.Prefab.Identifier.Replace(" ", ""); + + string characterType = GetCharacterType(this); + GameAnalyticsManager.AddDesignEvent("Kill:" + characterType + ":" + causeOfDeathStr); + if (CauseOfDeath.Killer != null) + { + GameAnalyticsManager.AddDesignEvent("Kill:" + characterType + ":Killer:" + GetCharacterType(CauseOfDeath.Killer)); + } + if (CauseOfDeath.DamageSource != null) + { + string damageSourceStr = CauseOfDeath.DamageSource.ToString(); + if (CauseOfDeath.DamageSource is Item damageSourceItem) { damageSourceStr = damageSourceItem.ToString(); } + GameAnalyticsManager.AddDesignEvent("Kill:" + characterType + ":DamageSource:" + damageSourceStr); + } + + static string GetCharacterType(Character character) + { + if (character.IsPlayer) + return "Player"; + else if (character.AIController is EnemyAIController) + return "Enemy" + character.SpeciesName; + else if (character.AIController is HumanAIController && character.TeamID == CharacterTeamType.Team2) + return "EnemyHuman"; + else if (character.Info != null && character.TeamID == CharacterTeamType.Team1) + return "AICrew"; + else if (character.Info != null && character.TeamID == CharacterTeamType.FriendlyNPC) + return "FriendlyNPC"; + return "Unknown"; + } + } + OnDeath?.Invoke(this, CauseOfDeath); - var abilityKiller = new AbilityCharacter(CauseOfDeath.Killer); - CheckTalents(AbilityEffectType.OnDieToCharacter, abilityKiller); + var abilityCharacterKiller = new AbilityCharacterKiller(CauseOfDeath.Killer); + CheckTalents(AbilityEffectType.OnDieToCharacter, abilityCharacterKiller); + CauseOfDeath.Killer?.RecordKill(this); if (GameMain.GameSession != null && Screen.Selected == GameMain.GameScreen) { @@ -3974,7 +4035,7 @@ namespace Barotrauma KillProjSpecific(causeOfDeath, causeOfDeathAffliction, log); - if (info != null) + if (info != null) { info.CauseOfDeath = CauseOfDeath; info.MissionsCompletedSinceDeath = 0; @@ -4089,7 +4150,7 @@ namespace Barotrauma info?.Remove(); #if CLIENT - GameMain.GameSession?.CrewManager?.KillCharacter(this); + GameMain.GameSession?.CrewManager?.KillCharacter(this, resetCrewListIndex: false); #endif CharacterList.Remove(this); @@ -4104,6 +4165,8 @@ namespace Barotrauma } } + itemSelectedDurations.Clear(); + DisposeProjSpecific(); aiTarget?.Remove(); @@ -4476,18 +4539,19 @@ namespace Barotrauma if (info == null) { return false; } 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 }); #endif CharacterTalent characterTalent = new CharacterTalent(talentPrefab, this); - characterTalent.ActivateTalent(addingFirstTime); characterTalents.Add(characterTalent); + characterTalent.ActivateTalent(addingFirstTime); characterTalent.AddedThisRound = addingFirstTime; if (addingFirstTime) { - OnTalentGiven(talentPrefab.Identifier); + OnTalentGiven(talentPrefab); + GameAnalyticsManager.AddDesignEvent("TalentUnlocked:" + (info.Job?.Prefab.Identifier ?? "None") + ":" + talentPrefab.Identifier, + GameMain.GameSession?.Campaign?.TotalPlayTime ?? 0.0); } return true; } @@ -4497,6 +4561,24 @@ namespace Barotrauma return info.UnlockedTalents.Contains(identifier); } + public bool HasUnlockedAllTalents() + { + if (TalentTree.JobTalentTrees.TryGetValue(Info.Job.Prefab.Identifier, out TalentTree talentTree)) + { + foreach (TalentSubTree talentSubTree in talentTree.TalentSubTrees) + { + foreach (TalentOption talentOption in talentSubTree.TalentOptionStages) + { + if (talentOption.Talents.None(t => HasTalent(t.Identifier))) + { + return false; + } + } + } + } + return true; + } + public static IEnumerable GetFriendlyCrew(Character character) { if (character is null) @@ -4556,7 +4638,7 @@ namespace Barotrauma } partial void OnMoneyChanged(int prevAmount, int newAmount); - partial void OnTalentGiven(string talentIdentifier); + partial void OnTalentGiven(TalentPrefab talentPrefab); /// /// This dictionary is used for stats that are required very frequently. Not very performant, but easier to develop with for now. @@ -4728,4 +4810,49 @@ namespace Barotrauma public Character Killer { get; set; } } + class AbilityAttackData : AbilityObject, IAbilityCharacter + { + public float DamageMultiplier { get; set; } = 1f; + public float AddedPenetration { get; set; } = 0f; + public List Afflictions { get; set; } + public bool ShouldImplode { get; set; } = false; + public Attack SourceAttack { get; } + public Character Character { get; set; } + public Character Attacker { get; set; } + + public AbilityAttackData(Attack sourceAttack, Character character) + { + SourceAttack = sourceAttack; + Character = character; + } + } + + class AbilityAttackResult : AbilityObject, IAbilityAttackResult + { + public AttackResult AttackResult { get; set; } + + public AbilityAttackResult(AttackResult attackResult) + { + AttackResult = attackResult; + } + } + + class AbilityCharacterKiller : AbilityObject, IAbilityCharacter + { + public AbilityCharacterKiller(Character character) + { + Character = character; + } + public Character Character { get; set; } + } + + class AbilityOrderedCharacter : AbilityObject, IAbilityCharacter + { + public AbilityOrderedCharacter(Character character) + { + Character = character; + } + public Character Character { get; set; } + } + } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index e2772ba1c..85d189f96 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -43,6 +43,7 @@ namespace Barotrauma public int FaceAttachmentIndex { get; set; } = -1; public XElement HairElement { get; set; } + public XElement HairWithHatElement { get; set; } public XElement BeardElement { get; set; } public XElement MoustacheElement { get; set; } public XElement FaceAttachment { get; set; } @@ -361,7 +362,7 @@ namespace Barotrauma public CharacterTeamType TeamID; - private readonly NPCPersonalityTrait personalityTrait; + private NPCPersonalityTrait personalityTrait; public const int MaxCurrentOrders = 3; public static int HighestManualOrderPriority => MaxCurrentOrders; @@ -568,7 +569,7 @@ namespace Barotrauma HasGenders = CharacterConfigElement.GetAttributeBool("genders", false); HasRaces = CharacterConfigElement.GetAttributeBool("races", false); SetGenderAndRace(randSync); - Job = (jobPrefab == null) ? Job.Random(Rand.RandSync.Unsynced) : new Job(jobPrefab, variant); + Job = (jobPrefab == null) ? Job.Random(Rand.RandSync.Unsynced) : new Job(jobPrefab, randSync, variant); HairColors = CharacterConfigElement.GetAttributeTupleArray("haircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray(); FacialHairColors = CharacterConfigElement.GetAttributeTupleArray("facialhaircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray(); SkinColors = CharacterConfigElement.GetAttributeTupleArray("skincolors", new (Color, float)[] { (new Color(255, 215, 200, 255), 100f) }).ToImmutableArray(); @@ -584,11 +585,10 @@ namespace Barotrauma } else { - name = ""; Name = GetRandomName(randSync); } OriginalName = !string.IsNullOrEmpty(originalName) ? originalName : Name; - personalityTrait = NPCPersonalityTrait.GetRandom(name + HeadSpriteId); + SetPersonalityTrait(); Salary = CalculateSalary(); if (ragdollFileName != null) { @@ -597,6 +597,11 @@ namespace Barotrauma LoadHeadAttachments(); } + private void SetPersonalityTrait() + { + personalityTrait = NPCPersonalityTrait.GetRandom(Name + HeadSpriteId); + } + public string GetRandomName(Rand.RandSync randSync) { string name = ""; @@ -1121,6 +1126,20 @@ namespace Barotrauma Head.HairElement = GetRandomElement(hairs); Head.HairIndex = hairs.IndexOf(Head.HairElement); } + if (Head.HairElement != null) + { + int thisHairIndex = hairs.IndexOf(head.HairElement); + int hairWithHatIndex = head.HairElement.GetAttributeInt("replacewhenwearinghat", thisHairIndex); + if (thisHairIndex != hairWithHatIndex && hairWithHatIndex > -1 && hairWithHatIndex < hairs.Count) + { + head.HairWithHatElement = hairs[hairWithHatIndex]; + } + else + { + head.HairWithHatElement = null; + } + } + if (IsValidIndex(Head.BeardIndex, beards)) { Head.BeardElement = beards[Head.BeardIndex]; @@ -1261,7 +1280,7 @@ namespace Barotrauma { int prevAmount = ExperiencePoints; - var experienceGainMultiplier = new AbilityValue(1f); + var experienceGainMultiplier = new AbilityExperienceGainMultiplier(1f); if (isMissionExperience) { Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplier); @@ -1523,7 +1542,7 @@ namespace Barotrauma orderTargetElement.Add(new XAttribute("hullid", (uint)ot.Hull.ID)); position -= ot.Hull.WorldPosition; } - orderTargetElement.Add(new XAttribute("position", $"{position.X},{position.Y}")); + orderTargetElement.Add(new XAttribute("position", XMLExtensions.Vector2ToString(position))); orderElement.Add(orderTargetElement); break; case Order.OrderTargetType.WallSection when targetAvailableInNextLevel && order.TargetEntity is Structure s && order.WallSectionIndex.HasValue: @@ -1858,18 +1877,27 @@ namespace Barotrauma } } - class AbilitySkillGain : AbilityObject, IAbilityValue, IAbilityString, IAbilityCharacter + class AbilitySkillGain : AbilityObject, IAbilityValue, IAbilitySkillIdentifier, IAbilityCharacter { - public AbilitySkillGain(float value, string abilityString, Character character, bool gainedFromAbility) + public AbilitySkillGain(float skillAmount, string skillIdentifier, Character character, bool gainedFromAbility) { - Value = value; - String = abilityString; + Value = skillAmount; + SkillIdentifier = skillIdentifier; Character = character; GainedFromAbility = gainedFromAbility; } public Character Character { get; set; } public float Value { get; set; } - public string String { get; set; } + public string SkillIdentifier { get; set; } public bool GainedFromAbility { get; } } + + class AbilityExperienceGainMultiplier : AbilityObject, IAbilityValue + { + public AbilityExperienceGainMultiplier(float experienceGainMultiplier) + { + Value = experienceGainMultiplier; + } + public float Value { get; set; } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index cb0d4507a..ae345d463 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -67,6 +67,9 @@ namespace Barotrauma public Affliction(AfflictionPrefab prefab, float strength) { +#if CLIENT + prefab?.ReloadSoundsIfNeeded(); +#endif Prefab = prefab; PendingAdditionStrength = Prefab.GrainBurst; _strength = strength; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionBleeding.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionBleeding.cs index 2059661f5..f6da0bfd1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionBleeding.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionBleeding.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Barotrauma +namespace Barotrauma { class AfflictionBleeding : Affliction { @@ -15,6 +11,10 @@ namespace Barotrauma { base.Update(characterHealth, targetLimb, deltaTime); characterHealth.BloodlossAmount += Strength * (1.0f / 60.0f) * deltaTime; + if (Source != null) + { + characterHealth.BloodlossAffliction.Source = Source; + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index d1d728d12..27dcecb23 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -52,10 +52,6 @@ namespace Barotrauma { if (state == value) { return; } state = value; - if (character != null && character == Character.Controlled) - { - UpdateMessages(); - } } } @@ -81,6 +77,9 @@ namespace Barotrauma base.Update(characterHealth, targetLimb, deltaTime); character = characterHealth.Character; if (character == null) { return; } + + UpdateMessages(); + if (!subscribedToDeathEvent) { character.OnDeath += CharacterDead; @@ -107,7 +106,7 @@ namespace Barotrauma { if (State != InfectionState.Active && stun) { - character.SetStun(Rand.Range(2, 4)); + character.SetStun(Rand.Range(2f, 3f)); } State = InfectionState.Active; ActivateHusk(); @@ -247,7 +246,7 @@ namespace Barotrauma if (huskPrefab.ControlHusk || GameMain.Lua.game.enableControlHusk) { #if SERVER - var client = GameMain.Server?.ConnectedClients.FirstOrDefault(c => c.CharacterInfo.Character == character); + 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/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 970570afb..feb46b689 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -363,6 +363,9 @@ namespace Barotrauma public readonly string Name, Description; public readonly string TranslationOverride; public readonly bool IsBuff; + public readonly bool HealableInMedicalClinic; + public readonly float HealCostMultiplier; + public readonly int BaseHealCost; public readonly string CauseOfDeathDescription, SelfCauseOfDeathDescription; @@ -656,6 +659,13 @@ namespace Barotrauma Description = TextManager.Get("AfflictionDescription." + translationId, true) ?? element.GetAttributeString("description", ""); IsBuff = element.GetAttributeBool("isbuff", false); + HealableInMedicalClinic = element.GetAttributeBool("healableinmedicalclinic", + !IsBuff && + !AfflictionType.Equals("geneticmaterialbuff", StringComparison.OrdinalIgnoreCase) && + !AfflictionType.Equals("geneticmaterialdebuff", StringComparison.OrdinalIgnoreCase)); + HealCostMultiplier = element.GetAttributeFloat(nameof(HealCostMultiplier).ToLowerInvariant(), 1f); + BaseHealCost = element.GetAttributeInt(nameof(BaseHealCost).ToLowerInvariant(), 0); + if (element.Attribute("nameidentifier") != null) { Name = TextManager.Get(element.GetAttributeString("nameidentifier", string.Empty), returnNull: true) ?? Name; @@ -677,7 +687,7 @@ namespace Barotrauma MaxStrength = element.GetAttributeFloat("maxstrength", 100.0f); GrainBurst = element.GetAttributeFloat(nameof(GrainBurst).ToLowerInvariant(), 0.0f); - ShowInHealthScannerThreshold = element.GetAttributeFloat("showinhealthscannerthreshold", Math.Max(ActivationThreshold, 0.05f)); + ShowInHealthScannerThreshold = element.GetAttributeFloat("showinhealthscannerthreshold", Math.Max(ActivationThreshold, AfflictionType == "talentbuff" ? float.MaxValue : 0.05f)); TreatmentThreshold = element.GetAttributeFloat("treatmentthreshold", Math.Max(ActivationThreshold, 5.0f)); DamageOverlayAlpha = element.GetAttributeFloat("damageoverlayalpha", 0.0f); @@ -751,6 +761,32 @@ namespace Barotrauma } } +#if CLIENT + public void ReloadSoundsIfNeeded() + { + foreach (var effect in effects) + { + foreach (var statusEffect in effect.StatusEffects) + { + foreach (var sound in statusEffect.Sounds) + { + if (sound.Sound == null) { Submarine.ReloadRoundSound(sound); } + } + } + } + foreach (var periodicEffect in periodicEffects) + { + foreach (var statusEffect in periodicEffect.StatusEffects) + { + foreach (var sound in statusEffect.Sounds) + { + if (sound.Sound == null) { Submarine.ReloadRoundSound(sound); } + } + } + } + } +#endif + public override string ToString() { return "AfflictionPrefab (" + Name + ")"; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 7dc1cba09..ac0c9c969 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -112,6 +112,7 @@ namespace Barotrauma private Affliction oxygenLowAffliction; private Affliction pressureAffliction; private Affliction stunAffliction; + public Affliction BloodlossAffliction { get => bloodlossAffliction; } public bool IsUnconscious { @@ -182,7 +183,7 @@ namespace Barotrauma public float BloodlossAmount { get { return bloodlossAffliction.Strength; } - set { bloodlossAffliction.Strength = MathHelper.Clamp(value, 0.0f, 100.0f); } + set { bloodlossAffliction.Strength = MathHelper.Clamp(value, 0, bloodlossAffliction.Prefab.MaxStrength); } } public float Stun @@ -202,7 +203,7 @@ namespace Barotrauma get { return pressureAffliction; } } - public Character Character { get; private set; } + public readonly Character Character; public CharacterHealth(Character character) { @@ -325,7 +326,11 @@ namespace Barotrauma if (kvp.Key == affliction) { int limbHealthIndex = limbHealths.IndexOf(kvp.Value); - return Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == limbHealthIndex); + foreach (Limb limb in Character.AnimController.Limbs) + { + if (limb.HealthIndex == limbHealthIndex) { return limb; } + } + return null; } } return null; @@ -681,7 +686,7 @@ namespace Barotrauma newStrength = Math.Min(existingAffliction.Prefab.MaxStrength, newStrength); if (existingAffliction == stunAffliction) { Character.SetStun(newStrength, true, true); } existingAffliction.Strength = newStrength; - existingAffliction.Source = newAffliction.Source; + if (newAffliction.Source != null) { existingAffliction.Source = newAffliction.Source; } CalculateVitality(); if (Vitality <= MinVitality) { @@ -767,7 +772,6 @@ namespace Barotrauma Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.MovementSpeed)); - // maybe a bit of a hacky way to do this. should inquire if there is a better way. M61T if (Character.InWater) { Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.SwimmingSpeed)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs index 7e11a575b..7fd0524ed 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs @@ -35,7 +35,7 @@ namespace Barotrauma public Skill PrimarySkill { get; } - public Job(JobPrefab jobPrefab, int variant = 0) + public Job(JobPrefab jobPrefab, Rand.RandSync randSync = Rand.RandSync.Unsynced, int variant = 0) { prefab = jobPrefab; Variant = variant; @@ -43,7 +43,7 @@ namespace Barotrauma skills = new Dictionary(); foreach (SkillPrefab skillPrefab in prefab.Skills) { - var skill = new Skill(skillPrefab); + var skill = new Skill(skillPrefab, randSync); skills.Add(skillPrefab.Identifier, skill); if (skillPrefab.IsPrimarySkill) { PrimarySkill = skill; } } @@ -79,7 +79,7 @@ namespace Barotrauma { var prefab = JobPrefab.Random(randSync); var variant = Rand.Range(0, prefab.Variants, randSync); - return new Job(prefab, variant); + return new Job(prefab, randSync, variant); } public float GetSkillLevel(string skillIdentifier) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs index b0dc3c7be..2baae954b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs @@ -18,7 +18,7 @@ namespace Barotrauma public void IncreaseSkill(float value, bool increasePastMax) { - level = MathHelper.Clamp(level + value, 0.0f, increasePastMax ? SkillSettings.Current.MaximumOlympianSkill : MaximumSkill); + level = MathHelper.Clamp(level + value, 0.0f, increasePastMax ? SkillSettings.Current.MaximumSkillWithTalents : MaximumSkill); } private Sprite icon; @@ -36,10 +36,10 @@ namespace Barotrauma public readonly float PriceMultiplier = 1.0f; - public Skill(SkillPrefab prefab) + public Skill(SkillPrefab prefab, Rand.RandSync randSync) { Identifier = prefab.Identifier; - level = Rand.Range(prefab.LevelRange.Start, prefab.LevelRange.End, Rand.RandSync.Server); + level = Rand.Range(prefab.LevelRange.Start, prefab.LevelRange.End, randSync); icon = GetIcon(); PriceMultiplier = prefab.PriceMultiplier; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index 6d7a8f929..978410599 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -556,6 +556,7 @@ namespace Barotrauma // TODO: We might need this or solve the cases where a limb is severed while holding on to an item //if (character.Params.CanInteract) { return false; } if (this == character.AnimController.MainLimb) { return false; } + bool canBeSevered = Params.CanBeSeveredAlive; if (character.AnimController.CanWalk) { switch (type) @@ -571,7 +572,7 @@ namespace Barotrauma return false; } } - return true; + return canBeSevered; } } @@ -583,6 +584,8 @@ namespace Barotrauma private readonly List statusEffects = new List(); + public IEnumerable StatusEffects { get { return statusEffects; } } + public Limb(Ragdoll ragdoll, Character character, LimbParams limbParams) { this.ragdoll = ragdoll; @@ -647,6 +650,8 @@ namespace Barotrauma if (attackElement != null) { attack.DamageMultiplier = attackElement.GetAttributeFloat("damagemultiplier", 1f); + attack.RangeMultiplier = attackElement.GetAttributeFloat("rangemultiplier", 1f); + attack.ImpactMultiplier = attackElement.GetAttributeFloat("impactmultiplier", 1f); } } break; @@ -756,12 +761,13 @@ namespace Barotrauma } if (attacker != null) { - var abilityAffliction = new AbilityAfflictionCharacter(newAffliction, character); - attacker.CheckTalents(AbilityEffectType.OnAddDamageAffliction, abilityAffliction); + var abilityAfflictionCharacter = new AbilityAfflictionCharacter(newAffliction, character); + attacker.CheckTalents(AbilityEffectType.OnAddDamageAffliction, abilityAfflictionCharacter); } if (applyAffliction) { afflictionsCopy.Add(newAffliction); + newAffliction.Source ??= attacker; } appliedDamageModifiers.AddRange(tempModifiers); } @@ -1065,7 +1071,7 @@ namespace Barotrauma #endif if (damageTarget is Character targetCharacter && targetLimb != null) { - attackResult = attack.DoDamageToLimb(character, targetLimb, WorldPosition, 1.0f, playSound, body); + attackResult = attack.DoDamageToLimb(character, targetLimb, WorldPosition, 1.0f, playSound, body, this); } else { @@ -1075,7 +1081,7 @@ namespace Barotrauma } else { - attackResult = attack.DoDamage(character, damageTarget, WorldPosition, 1.0f, playSound, body); + attackResult = attack.DoDamage(character, damageTarget, WorldPosition, 1.0f, playSound, body, this); } } /*if (structureBody != null && attack.StickChance > Rand.Range(0.0f, 1.0f, Rand.RandSync.Server)) @@ -1309,4 +1315,16 @@ namespace Barotrauma partial void LoadParamsProjSpecific(); } + + class AbilityAfflictionCharacter : AbilityObject, IAbilityAffliction, IAbilityCharacter + { + public AbilityAfflictionCharacter(Affliction affliction, Character character) + { + Affliction = affliction; + Character = character; + } + public Character Character { get; set; } + public Affliction Affliction { get; set; } + } + } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs index ff46272dc..7b004f09d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs @@ -14,9 +14,9 @@ namespace Barotrauma NotDefined = 0, Walk = 1, Run = 2, - Crouch = 3, - SwimSlow = 4, - SwimFast = 5 + SwimSlow = 3, + SwimFast = 4, + Crouch = 5 } abstract class GroundedMovementParams : AnimationParams diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs index 4192bd331..43ed47198 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs @@ -26,6 +26,15 @@ namespace Barotrauma class HumanCrouchParams : HumanGroundedParams { + [Serialize(0.0f, true, description: "How much lower the character's head and torso move when stationary."), Editable(MinValueFloat = 0, MaxValueFloat = 2, DecimalCount = 2)] + public float MoveDownAmountWhenStationary { get; set; } + + [Serialize(0.0f, true), Editable(-360f, 360f)] + public float ExtraHeadAngleWhenStationary { get; set; } + + [Serialize(0.0f, true), Editable(-360f, 360f)] + public float ExtraTorsoAngleWhenStationary { get; set; } + public static HumanCrouchParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character, AnimationType.Crouch); public static HumanCrouchParams GetAnimParams(Character character, string fileName = null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs index ff4f961f9..3d2c05297 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs @@ -612,7 +612,7 @@ namespace Barotrauma [Serialize(0f, true, description: "Width of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] public float Width { get; set; } - [Serialize(10f, true, description: "The more the density the heavier the limb is."), Editable(MinValueFloat = 0, MaxValueFloat = 100, DecimalCount = 2)] + [Serialize(10f, true, description: "The more the density the heavier the limb is."), Editable(MinValueFloat = 0.01f, MaxValueFloat = 100, DecimalCount = 2)] public float Density { get; set; } [Serialize(false, true), Editable] @@ -648,6 +648,9 @@ namespace Barotrauma [Serialize(1f, true, description:"How much damage must be done by the attack in order to be able to cut off the limb. Note that it's evaluated after the damage modifiers."), Editable(DecimalCount = 0, MinValueFloat = 0, MaxValueFloat = 1000)] public float MinSeveranceDamage { get; set; } + [Serialize(true, true, description: "Disable if you don't want to allow severing this joint while the creature is alive. Note: Does nothing if the 'Severance Probability Modifier' in the joint settings is 0 (default). Also note that the setting doesn't override certain limitations, e.g. severing the main limb, or legs of a walking creature is not allowed."), Editable] + public bool CanBeSeveredAlive { get; set; } + //how long it takes for severed limbs to fade out [Serialize(10f, true, "How long it takes for the severed limb to fade out"), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 1)] public float SeveredFadeOutTime { get; set; } = 10.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs index 43085c608..f023b51f7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs @@ -42,7 +42,7 @@ namespace Barotrauma } private float skillIncreasePerRepairedStructureDamage; - [Serialize(0.005f, true)] + [Serialize(0.0025f, true)] public float SkillIncreasePerRepairedStructureDamage { get { return skillIncreasePerRepairedStructureDamage * GetCurrentSkillGainMultiplier(); } @@ -96,8 +96,8 @@ namespace Barotrauma set; } - [Serialize(500.0f, true)] - public float MaximumOlympianSkill + [Serialize(200.0f, true)] + public float MaximumSkillWithTalents { get; set; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs index 93b267cd3..b45a81b25 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs @@ -1,5 +1,6 @@ using Barotrauma.Items.Components; using System; +using System.Collections.Generic; using System.Linq; using System.Xml.Linq; @@ -7,15 +8,19 @@ namespace Barotrauma.Abilities { class AbilityConditionAttackData : AbilityConditionData { + [Flags] private enum WeaponType { Any = 0, Melee = 1, Ranged = 2, - HandheldRanged = 3, - Turret = 4 + HandheldRanged = 4, + Turret = 8, + NoWeapon = 16 }; + private static readonly List WeaponTypeValues = Enum.GetValues(typeof(WeaponType)).Cast().ToList(); + private readonly string itemIdentifier; private readonly string[] tags; private readonly WeaponType weapontype; @@ -65,27 +70,39 @@ namespace Barotrauma.Abilities if (weapontype != WeaponType.Any) { - switch (weapontype) + foreach (WeaponType wt in WeaponTypeValues) { - // it is possible that an item that has both a melee and a projectile component will return true - // even when not used as a melee/ranged weapon respectively - // attackdata should contain data regarding whether the attack is melee or not - case WeaponType.Melee: - return item?.GetComponent() != null; - case WeaponType.Ranged: - return item?.GetComponent() != null; - case WeaponType.HandheldRanged: - { - var projectile = item?.GetComponent(); - return projectile?.Launcher?.GetComponent() != null; - } - case WeaponType.Turret: - { - var projectile = item?.GetComponent(); - return projectile?.Launcher?.GetComponent() != null; - } + if (wt == WeaponType.Any || !weapontype.HasFlag(wt)) { continue; } + switch (wt) + { + // it is possible that an item that has both a melee and a projectile component will return true + // even when not used as a melee/ranged weapon respectively + // attackdata should contain data regarding whether the attack is melee or not + case WeaponType.Melee: + if (item?.GetComponent() != null) { return true; } + break; + case WeaponType.Ranged: + if (item?.GetComponent() != null) { return true; } + break; + case WeaponType.HandheldRanged: + { + var projectile = item?.GetComponent(); + if (projectile?.Launcher?.GetComponent() != null) { return true; } + } + break; + case WeaponType.Turret: + { + var projectile = item?.GetComponent(); + if (projectile?.Launcher?.GetComponent() != null) { return true; } + } + break; + case WeaponType.NoWeapon: + if (item == null) { return true; } + break; + } } - } + return false; + } return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs index 407446785..cc14b466a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Xml.Linq; namespace Barotrauma.Abilities @@ -7,9 +8,26 @@ namespace Barotrauma.Abilities { private readonly List targetTypes; + private List conditionals = new List(); + public AbilityConditionCharacter(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { targetTypes = ParseTargetTypes(conditionElement.GetAttributeStringArray("targettypes", new string[0], convertToLowerInvariant: true)); + + foreach (XElement subElement in conditionElement.Elements()) + { + if (subElement.Name.ToString().Equals("conditional", StringComparison.OrdinalIgnoreCase)) + { + foreach (XAttribute attribute in subElement.Attributes()) + { + if (PropertyConditional.IsValid(attribute)) + { + conditionals.Add(new PropertyConditional(attribute)); + } + } + break; + } + } } protected override bool MatchesConditionSpecific(AbilityObject abilityObject) @@ -18,7 +36,10 @@ namespace Barotrauma.Abilities { if (!(abilityCharacter.Character is Character character)) { return false; } if (!IsViableTarget(targetTypes, character)) { return false; } - + foreach (var conditional in conditionals) + { + if (!conditional.Matches(character)) { return false; } + } return true; } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemInSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemInSubmarine.cs new file mode 100644 index 000000000..330bcfcd2 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemInSubmarine.cs @@ -0,0 +1,38 @@ +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionItemInSubmarine : AbilityConditionData + { + private readonly SubmarineType? submarineType; + + public AbilityConditionItemInSubmarine(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + if (conditionElement.Attribute("submarinetype") != null) + { + submarineType = conditionElement.GetAttributeEnum("submarinetype", SubmarineType.Player); + } + } + + protected override bool MatchesConditionSpecific(AbilityObject abilityObject) + { + if ((abilityObject as IAbilityItem)?.Item is Item item) + { + if (item.Submarine == null) { return false; } + if (submarineType.HasValue) + { + return item.Submarine.Info?.Type == submarineType.Value; + } + else + { + return true; + } + } + else + { + LogAbilityConditionError(abilityObject, typeof(IAbilityItem)); + return false; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemOutsideSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemOutsideSubmarine.cs deleted file mode 100644 index d23794f56..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemOutsideSubmarine.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Xml.Linq; - -namespace Barotrauma.Abilities -{ - class AbilityConditionItemOutsideSubmarine : AbilityConditionData - { - - public AbilityConditionItemOutsideSubmarine(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } - - protected override bool MatchesConditionSpecific(AbilityObject abilityObject) - { - if ((abilityObject as IAbilityItem)?.Item is Item item) - { - return item.Submarine == null || item.Submarine.TeamID != character.Info.TeamID; - } - else - { - LogAbilityConditionError(abilityObject, typeof(IAbilityItem)); - return false; - } - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemWreck.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemWreck.cs deleted file mode 100644 index 81d1b1d06..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemWreck.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Xml.Linq; - -namespace Barotrauma.Abilities -{ - class AbilityConditionItemWreck : AbilityConditionData - { - - public AbilityConditionItemWreck(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } - - protected override bool MatchesConditionSpecific(AbilityObject abilityObject) - { - if ((abilityObject as IAbilityItem)?.Item is Item item) - { - return item.Submarine?.Info?.IsWreck ?? false; - } - else - { - LogAbilityConditionError(abilityObject, typeof(IAbilityItem)); - return false; - } - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionSkill.cs index 5c368df8f..52d189213 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionSkill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionSkill.cs @@ -18,13 +18,13 @@ namespace Barotrauma.Abilities protected override bool MatchesConditionSpecific(AbilityObject abilityObject) { - if ((abilityObject as IAbilityString)?.String is string skillIdentifier) + if ((abilityObject as IAbilitySkillIdentifier)?.SkillIdentifier is string skillIdentifier) { return MatchesConditionSpecific(skillIdentifier); } else { - LogAbilityConditionError(abilityObject, typeof(IAbilityString)); + LogAbilityConditionError(abilityObject, typeof(IAbilitySkillIdentifier)); return false; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityInterfaces.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityInterfaces.cs index 8c552ad82..ef57527d6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityInterfaces.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityInterfaces.cs @@ -30,9 +30,9 @@ public Character Character { get; set; } } - interface IAbilityString + interface IAbilitySkillIdentifier { - public string String { get; set; } + public string SkillIdentifier { get; set; } } interface IAbilityAffliction diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs index db98b843d..6d7038f4c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs @@ -16,173 +16,4 @@ namespace Barotrauma.Abilities public Character Character { get; set; } } - class AbilityItem : AbilityObject, IAbilityItem - { - public AbilityItem(Item item) - { - Item = item; - } - public Item Item { get; set; } - } - - class AbilityValue : AbilityObject, IAbilityValue - { - public AbilityValue(float value) - { - Value = value; - } - public float Value { get; set; } - } - - class AbilityAffliction : AbilityObject, IAbilityAffliction - { - public AbilityAffliction(Affliction affliction) - { - Affliction = affliction; - } - public Affliction Affliction { get; set; } - } - - class AbilityAfflictionCharacter : AbilityObject, IAbilityAffliction, IAbilityCharacter - { - public AbilityAfflictionCharacter(Affliction affliction, Character character) - { - Affliction = affliction; - Character = character; - } - public Character Character { get; set; } - public Affliction Affliction { get; set; } - } - - class AbilityValueItem : AbilityObject, IAbilityValue, IAbilityItemPrefab - { - public AbilityValueItem(float value, ItemPrefab itemPrefab) - { - Value = value; - ItemPrefab = itemPrefab; - } - public float Value { get; set; } - public ItemPrefab ItemPrefab { get; set; } - } - - class AbilityItemPrefabItem : AbilityObject, IAbilityItem, IAbilityItemPrefab - { - public AbilityItemPrefabItem(Item item, ItemPrefab itemPrefab) - { - Item = item; - ItemPrefab = itemPrefab; - } - public Item Item { get; set; } - public ItemPrefab ItemPrefab { get; set; } - } - - class AbilityValueString : AbilityObject, IAbilityValue, IAbilityString - { - public AbilityValueString(float value, string abilityString) - { - Value = value; - String = abilityString; - } - public float Value { get; set; } - public string String { get; set; } - } - - class AbilityStringCharacter : AbilityObject, IAbilityCharacter, IAbilityString - { - public AbilityStringCharacter(string abilityString, Character character) - { - String = abilityString; - Character = character; - } - public Character Character { get; set; } - public string String { get; set; } - } - - class AbilityValueAffliction : AbilityObject, IAbilityValue, IAbilityAffliction - { - public AbilityValueAffliction(float value, Affliction affliction) - { - Value = value; - Affliction = affliction; - } - public float Value { get; set; } - public Affliction Affliction { get; set; } - } - - class AbilityValueMission : AbilityObject, IAbilityValue, IAbilityMission - { - public AbilityValueMission(float value, Mission mission) - { - Value = value; - Mission = mission; - } - public float Value { get; set; } - public Mission Mission { get; set; } - } - - class AbilityLocation : AbilityObject, IAbilityLocation - { - public AbilityLocation(Location location) - { - Location = location; - } - - public Location Location { get; set; } - } - - // this is an exception class that should only be passed in this form, so classes that use it should cast into it directly - class AbilityAttackData : AbilityObject, IAbilityCharacter - { - public float DamageMultiplier { get; set; } = 1f; - public float AddedPenetration { get; set; } = 0f; - public List Afflictions { get; set; } - public bool ShouldImplode { get; set; } = false; - public Attack SourceAttack { get; } - public Character Character { get; set; } - public Character Attacker { get; set; } - - public AbilityAttackData(Attack sourceAttack, Character character) - { - SourceAttack = sourceAttack; - Character = character; - } - } - - class AbilityApplyTreatment : AbilityObject, IAbilityCharacter, IAbilityItem - { - public Character Character { get; set; } - - public Character User { get; set; } - - public Item Item { get; set; } - - public AbilityApplyTreatment(Character user, Character target, Item item) - { - Character = target; - User = user; - Item = item; - } - } - - class AbilityAttackResult : AbilityObject, IAbilityAttackResult - { - public AttackResult AttackResult { get; set; } - - public AbilityAttackResult(AttackResult attackResult) - { - AttackResult = attackResult; - } - } - - class AbilityCharacterSubmarine : AbilityObject, IAbilityCharacter, IAbilitySubmarine - { - public AbilityCharacterSubmarine(Character character, Submarine submarine) - { - Character = character; - Submarine = submarine; - } - public Character Character { get; set; } - public Submarine Submarine { get; set; } - } - } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs index 0abc6dbc1..27d856553 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs @@ -87,7 +87,7 @@ namespace Barotrauma.Abilities DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not take a parameter for ApplyEffect in talent {CharacterTalent.DebugIdentifier}"); } - protected void LogabilityObjectMismatch() + protected void LogAbilityObjectMismatch() { DebugConsole.ThrowError($"Incompatible ability! Ability {this} is incompatitible with this type of ability effect type in talent {CharacterTalent.DebugIdentifier}"); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs index f2c500a54..9f37a0e03 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs @@ -23,7 +23,7 @@ namespace Barotrauma.Abilities } else { - LogabilityObjectMismatch(); + LogAbilityObjectMismatch(); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs index 9c4dd581a..11aa9934e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs @@ -23,7 +23,9 @@ namespace Barotrauma.Abilities multiplier = 0 + Character.Info.GetSavedStatValue(StatTypes.None, scalingStatIdentifier); } - targetCharacter.GiveMoney((int)(multiplier * amount)); + int totalAmount = (int)(multiplier * amount); + targetCharacter.GiveMoney(totalAmount); + GameAnalyticsManager.AddMoneyGainedEvent(totalAmount, GameAnalyticsManager.MoneySource.Ability, CharacterTalent.Prefab.Identifier); } protected override void ApplyEffect(AbilityObject abilityObject) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs index 5d073a068..e3896090c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs @@ -29,7 +29,7 @@ namespace Barotrauma.Abilities } else { - LogabilityObjectMismatch(); + LogAbilityObjectMismatch(); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs index 8b4245dc9..9b4700fe5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs @@ -41,7 +41,7 @@ namespace Barotrauma.Abilities } else { - LogabilityObjectMismatch(); + LogAbilityObjectMismatch(); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySpawnItemsToContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySpawnItemsToContainer.cs index b13e97638..919848c3a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySpawnItemsToContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySpawnItemsToContainer.cs @@ -37,7 +37,7 @@ namespace Barotrauma.Abilities } else { - LogabilityObjectMismatch(); + LogAbilityObjectMismatch(); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAlienHoarder.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAlienHoarder.cs index 7c7141a25..a6907ca41 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAlienHoarder.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAlienHoarder.cs @@ -34,7 +34,7 @@ namespace Barotrauma.Abilities } else { - LogabilityObjectMismatch(); + LogAbilityObjectMismatch(); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs index 9bb79d132..2cc7a26f1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs @@ -1,6 +1,4 @@ -using Microsoft.Xna.Framework; -using System; -using System.Xml.Linq; +using System.Xml.Linq; namespace Barotrauma.Abilities { @@ -18,7 +16,7 @@ namespace Barotrauma.Abilities if (abilityObject is AbilitySkillGain abilitySkillGain && abilitySkillGain.Character != Character) { if (ignoreAbilitySkillGain && abilitySkillGain.GainedFromAbility) { return; } - Character.Info?.IncreaseSkillLevel(abilitySkillGain.String, 1.0f, gainedFromAbility: true); + Character.Info?.IncreaseSkillLevel(abilitySkillGain.SkillIdentifier, 1.0f, gainedFromAbility: true); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityBountyHunter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityBountyHunter.cs index 417b93972..00c39c0e0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityBountyHunter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityBountyHunter.cs @@ -16,7 +16,9 @@ namespace Barotrauma.Abilities { if ((abilityObject as IAbilityCharacter)?.Character is Character character) { - Character.GiveMoney((int)(vitalityPercentage * character.MaxVitality)); + int totalAmount = (int)(vitalityPercentage * character.MaxVitality); + Character.GiveMoney(totalAmount); + GameAnalyticsManager.AddMoneyGainedEvent(totalAmount, GameAnalyticsManager.MoneySource.Ability, CharacterTalent.Prefab.Identifier); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs index 8bfe07b9d..66a39efaa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs @@ -30,6 +30,7 @@ namespace Barotrauma.Abilities if (!enemyCharacter.LockHands) { continue; } if (timesGiven > max) { continue; } Character.GiveMoney(moneyAmount); + GameAnalyticsManager.AddMoneyGainedEvent(moneyAmount, GameAnalyticsManager.MoneySource.Ability, CharacterTalent.Prefab.Identifier); foreach (Character character in Character.GetFriendlyCrew(Character)) { character.Info?.GiveExperience(experienceAmount); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityInsurancePolicy.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityInsurancePolicy.cs index 94a89cc5b..fe4073afc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityInsurancePolicy.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityInsurancePolicy.cs @@ -12,8 +12,6 @@ namespace Barotrauma.Abilities private readonly int moneyPerMission; - private static List clientsAlreadyUsed = new List(); - public CharacterAbilityInsurancePolicy(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) { moneyPerMission = abilityElement.GetAttributeInt("moneypermission", 0); @@ -23,7 +21,9 @@ namespace Barotrauma.Abilities { if (Character?.Info is CharacterInfo info) { - Character.GiveMoney(moneyPerMission * info.MissionsCompletedSinceDeath); + int totalAmount = moneyPerMission * info.MissionsCompletedSinceDeath; + Character.GiveMoney(totalAmount); + GameAnalyticsManager.AddMoneyGainedEvent(totalAmount, GameAnalyticsManager.MoneySource.Ability, CharacterTalent.Prefab.Identifier); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityMultitasker.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityMultitasker.cs index 5cb3857fc..c66630989 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityMultitasker.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityMultitasker.cs @@ -13,7 +13,7 @@ namespace Barotrauma.Abilities protected override void ApplyEffect(AbilityObject abilityObject) { - if ((abilityObject as IAbilityString)?.String is string skillIdentifier) + if ((abilityObject as IAbilitySkillIdentifier)?.SkillIdentifier is string skillIdentifier) { if (skillIdentifier != lastSkillIdentifier) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs index 8d6c64ac5..2fbb95ec9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs @@ -9,7 +9,7 @@ namespace Barotrauma.Abilities class CharacterAbilityTandemFire : CharacterAbilityApplyStatusEffectsToNearestAlly { // this should just be its own class, misleading to inherit here - private string tag; + private readonly string tag; public CharacterAbilityTandemFire(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) { tag = abilityElement.GetAttributeString("tag", ""); @@ -20,7 +20,7 @@ namespace Barotrauma.Abilities if (Character.SelectedConstruction == null || !Character.SelectedConstruction.HasTag(tag)) { return; } Character closestCharacter = null; - float closestDistance = float.MaxValue; + float closestDistance = squaredMaxDistance; foreach (Character crewCharacter in Character.GetFriendlyCrew(Character)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 4e9abc062..cf1b71fa9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -522,6 +522,7 @@ namespace Barotrauma if (targetCharacter == null) { return; } targetCharacter.GodMode = !targetCharacter.GodMode; + NewMessage((targetCharacter.GodMode ? "Enabled godmode on " : "Disabled godmode on " + targetCharacter.Name), Color.White); }, () => { @@ -978,15 +979,18 @@ namespace Barotrauma else { NewMessage("Level seed: " + Level.Loaded.Seed); - NewMessage("Level size: " + Level.Loaded.Size.X+"x"+ Level.Loaded.Size.Y); + NewMessage("Level generation params: " + Level.Loaded.GenerationParams.Identifier); + NewMessage("Adjacent locations: " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none") + ", " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none")); + NewMessage("Mirrored: " + Level.Loaded.Mirrored); + NewMessage("Level size: " + Level.Loaded.Size.X + "x" + Level.Loaded.Size.Y); NewMessage("Minimum main path width: " + (Level.Loaded.LevelData?.MinMainPathWidth?.ToString() ?? "unknown")); } },null)); commands.Add(new Command("teleportsub", "teleportsub [start/end/cursor]: Teleport the submarine to the position of the cursor, or the start or end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.", (string[] args) => { - if (Submarine.MainSub == null || Level.Loaded == null) return; - if (Level.Loaded.Type == LevelData.LevelType.Outpost) + if (Submarine.MainSub == null) { return; } + if (Level.Loaded?.Type == LevelData.LevelType.Outpost && GameMain.GameSession != null) { NewMessage("The teleportsub command is unavailable in outpost levels!", Color.Red); return; @@ -1002,6 +1006,11 @@ namespace Barotrauma } else if (args[0].Equals("start", StringComparison.OrdinalIgnoreCase)) { + if (Level.Loaded == null) + { + NewMessage("Can't teleport the sub to the start of the level (no level loaded).", Color.Red); + return; + } Vector2 pos = Level.Loaded.StartPosition; if (Level.Loaded.StartOutpost != null) { @@ -1011,6 +1020,11 @@ namespace Barotrauma } else { + if (Level.Loaded == null) + { + NewMessage("Can't teleport the sub to the end of the level (no level loaded).", Color.Red); + return; + } Vector2 pos = Level.Loaded.EndPosition; if (Level.Loaded.EndOutpost != null) { @@ -1033,6 +1047,20 @@ namespace Barotrauma throw new Exception("crash command issued"); })); + commands.Add(new Command("fastforward", "fastforward [seconds]: Fast forwards the game by x seconds. Note that large numbers may cause a long freeze.", (string[] args) => + { + float seconds = 0; + if (args.Length > 0) { float.TryParse(args[0], out seconds); } + System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); + sw.Start(); + for (int i = 0; i < seconds * Timing.FixedUpdateRate; i++) + { + Screen.Selected?.Update(Timing.Step); + } + sw.Stop(); + NewMessage($"Fast-forwarded by {seconds} seconds (took {sw.ElapsedMilliseconds / 1000.0f} s)."); + })); + commands.Add(new Command("removecharacter", "removecharacter [character name]: Immediately deletes the specified character.", (string[] args) => { if (args.Length == 0) { return; } @@ -1190,6 +1218,7 @@ namespace Barotrauma { foreach (Item it in Item.ItemList) { + if (it.GetComponent() != null) { continue; } it.Condition = it.MaxCondition; } }, null, true)); @@ -1524,6 +1553,7 @@ namespace Barotrauma if (int.TryParse(args[0], out int money)) { campaign.Money += money; + GameAnalyticsManager.AddMoneyGainedEvent(money, GameAnalyticsManager.MoneySource.Cheat, "console"); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs index 8ffc28be3..5dfe8e9f0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs @@ -28,6 +28,7 @@ namespace Barotrauma if (GameMain.GameSession?.GameMode is CampaignMode campaign) { campaign.Money += Amount; + GameAnalyticsManager.AddMoneyGainedEvent(Amount, GameAnalyticsManager.MoneySource.Event, ParentEvent.Prefab.Identifier); #if SERVER (campaign as MultiPlayerCampaign).LastUpdateID++; #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs index 27197a60a..751731fbc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs @@ -17,7 +17,7 @@ namespace Barotrauma public NPCWaitAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } - private List affectedNpcs = null; + private IEnumerable affectedNpcs; private AIObjectiveGoTo gotoObjective; @@ -25,7 +25,7 @@ namespace Barotrauma { if (isFinished) { return; } - affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character).ToList(); + affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character); foreach (var npc in affectedNpcs) { @@ -62,7 +62,7 @@ namespace Barotrauma { foreach (var npc in affectedNpcs) { - if (npc.Removed || !(npc.AIController is HumanAIController humanAiController)) { continue; } + if (npc.Removed || !(npc.AIController is HumanAIController)) { continue; } if (gotoObjective != null) { gotoObjective.Abandon = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index f956de233..0fb1480a4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -796,8 +796,9 @@ namespace Barotrauma monsterStrength += enemyAI.CombatStrength; } - if (character.CurrentHull?.Submarine != null && - (character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine))) + if (character.CurrentHull?.Submarine?.Info != null && + (character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)) && + character.CurrentHull.Submarine.Info.Type == SubmarineType.Player) { // Enemy onboard -> Crawler inside the sub adds 0.2 to enemy danger, Mudraptor 0.42 enemyDanger += enemyAI.CombatStrength / 500.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index 60455b983..227528cee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -351,7 +351,7 @@ namespace Barotrauma } } - public static List GetDebugStatistics(int simulatedRoundCount = 100, Func filter = null) + public static List GetDebugStatistics(int simulatedRoundCount = 100, Func filter = null, bool fullLog = false) { List debugLines = new List(); @@ -365,7 +365,7 @@ namespace Barotrauma stats.Add(newStats); } debugLines.Add($"Event stats ({eventSet.DebugIdentifier}): "); - LogEventStats(stats, debugLines); + LogEventStats(stats, debugLines, fullLog); } return debugLines; @@ -415,14 +415,19 @@ namespace Barotrauma if (eventPrefab.EventType == typeof(MonsterEvent) && eventPrefab.TryCreateInstance(out MonsterEvent monsterEvent)) { if (filter != null && !filter(monsterEvent)) { return; } - float spawnProbability = monsterEvent.Prefab.Probability; if (Rand.Value() > spawnProbability) { return; } - - string character = monsterEvent.speciesName; int count = Rand.Range(monsterEvent.MinAmount, monsterEvent.MaxAmount + 1); if (count <= 0) { return; } - if (!stats.MonsterCounts.ContainsKey(character)) { stats.MonsterCounts[character] = 0; } + string character = monsterEvent.speciesName; + if (stats.MonsterCounts.TryGetValue(character, out int currentCount)) + { + if (currentCount >= monsterEvent.MaxAmountPerLevel) { return; } + } + else + { + stats.MonsterCounts[character] = 0; + } stats.MonsterCounts[character] += count; var aiElement = CharacterPrefab.FindBySpeciesName(character)?.XDocument?.Root?.GetChildElement("ai"); @@ -433,7 +438,7 @@ namespace Barotrauma } } - static void LogEventStats(List stats, List debugLines) + static void LogEventStats(List stats, List debugLines, bool fullLog) { if (stats.Count == 0 || stats.All(s => s.MonsterCounts.Values.Sum() == 0)) { @@ -442,28 +447,42 @@ namespace Barotrauma } else { + var allMonsters = new Dictionary(); + foreach (var stat in stats) + { + foreach (var monster in stat.MonsterCounts) + { + if (!allMonsters.TryAdd(monster.Key, monster.Value)) + { + allMonsters[monster.Key] += monster.Value; + } + } + } + allMonsters = allMonsters.OrderBy(m => m.Key).ToDictionary(m => m.Key, m => m.Value); stats.Sort((s1, s2) => s1.MonsterCounts.Values.Sum().CompareTo(s2.MonsterCounts.Values.Sum())); - debugLines.Add($" Minimum monster count: {stats.First().MonsterCounts.Values.Sum()}"); - debugLines.Add($" {LogMonsterCounts(stats.First())}"); - debugLines.Add($" Median monster count: {stats[stats.Count / 2].MonsterCounts.Values.Sum()}"); - debugLines.Add($" {LogMonsterCounts(stats[stats.Count / 2])}"); - debugLines.Add($" Maximum monster count: {stats.Last().MonsterCounts.Values.Sum()}"); - debugLines.Add($" {LogMonsterCounts(stats.Last())}"); - debugLines.Add($" Average monster count: {StringFormatter.FormatZeroDecimal((float)stats.Average(s => s.MonsterCounts.Values.Sum()))}"); - debugLines.Add($" "); - + debugLines.Add($" Average monster count: {StringFormatter.FormatZeroDecimal((float)stats.Average(s => s.MonsterCounts.Values.Sum()))} (Min: {stats.First().MonsterCounts.Values.Sum()}, Max: {stats.Last().MonsterCounts.Values.Sum()})"); + debugLines.Add($" {LogMonsterCounts(allMonsters, divider: stats.Count)}"); + if (fullLog) + { + debugLines.Add($" All samples:"); + stats.ForEach(s => debugLines.Add($" {LogMonsterCounts(s.MonsterCounts)}")); + } stats.Sort((s1, s2) => s1.MonsterStrength.CompareTo(s2.MonsterStrength)); - debugLines.Add($" Minimum monster strength: {StringFormatter.FormatZeroDecimal(stats.First().MonsterStrength)}"); - debugLines.Add($" Median monster strength: {StringFormatter.FormatZeroDecimal(stats[stats.Count / 2].MonsterStrength)}"); - debugLines.Add($" Maximum monster strength: {StringFormatter.FormatZeroDecimal(stats.Last().MonsterStrength)}"); - debugLines.Add($" Average monster strength: {StringFormatter.FormatZeroDecimal(stats.Average(s => s.MonsterStrength))}"); + debugLines.Add($" Average monster strength: {StringFormatter.FormatZeroDecimal(stats.Average(s => s.MonsterStrength))} (Min: {StringFormatter.FormatZeroDecimal(stats.First().MonsterStrength)}, Max: {StringFormatter.FormatZeroDecimal(stats.Last().MonsterStrength)})"); debugLines.Add($" "); } } - static string LogMonsterCounts(EventDebugStats stats) + static string LogMonsterCounts(Dictionary stats, float divider = 0) { - return string.Join(", ", stats.MonsterCounts.Select(mc => mc.Key + " x " + mc.Value)); + if (divider > 0) + { + return string.Join("\n ", stats.Select(mc => mc.Key + " x " + (mc.Value / divider).FormatSingleDecimal())); + } + else + { + return string.Join(", ", stats.Select(mc => mc.Key + " x " + mc.Value)); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs index 53bbb3732..32a2d3c6c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs @@ -152,7 +152,7 @@ namespace Barotrauma if (spawnPoint is WayPoint wp && wp.CurrentHull != null && wp.CurrentHull.Rect.Width > 100) { spawnPos = new Vector2( - MathHelper.Clamp(wp.WorldPosition.X + Rand.Range(-200, 200), wp.CurrentHull.WorldRect.X + 50, wp.CurrentHull.WorldRect.Right - 50), + MathHelper.Clamp(wp.WorldPosition.X + Rand.Range(-200, 201), wp.CurrentHull.WorldRect.X + 50, wp.CurrentHull.WorldRect.Right - 50), wp.CurrentHull.WorldRect.Y - wp.CurrentHull.Rect.Height + 16.0f); } var item = new Item(itemPrefab, spawnPos, null); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs index 3fc58b81a..d5ee46335 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs @@ -1,3 +1,4 @@ +using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -58,6 +59,18 @@ namespace Barotrauma if (IsClient) { return; } if (!swarmSpawned && level.CheckBeaconActive()) { + List connectedSubs = level.BeaconStation.GetConnectedSubs(); + foreach (Item item in Item.ItemList) + { + if (!connectedSubs.Contains(item.Submarine)) { continue; } + if (item.GetComponent() != null || + item.GetComponent() != null || + item.GetComponent() != null) + { + item.Indestructible = true; + } + } + State = 1; Vector2 spawnPos = level.BeaconStation.WorldPosition; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs index bffec355a..99bdf971e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs @@ -50,7 +50,8 @@ namespace Barotrauma return; } - int multiplier = CalculateScalingEscortedCharacterCount(); + // Disabled for now, because they make balancing the missions a pain. + int multiplier = 1;//CalculateScalingEscortedCharacterCount(); calculatedReward = Prefab.Reward * multiplier; string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", GetReward(missionSub))}‖end‖"; @@ -319,18 +320,33 @@ namespace Barotrauma } } - // characters that survived will take their items with them, in case players tried to be crafty and steal them - // this needs to run here in case players abort the mission by going back home - // TODO: I think this might feel like a bug. - foreach (var characterItem in characterItems) + if (!IsClient) { - if (Survived(characterItem.Key) || !completed) + foreach (Character character in characters) { - foreach (Item item in characterItem.Value) + if (character.Inventory == null) { continue; } + foreach (Item item in character.Inventory.AllItemsMod) { - if (!item.Removed) + //item didn't spawn with the characters -> drop it + if (!characterItems.Any(c => c.Value.Contains(item))) { - item.Remove(); + item.Drop(character); + } + } + } + + // characters that survived will take their items with them, in case players tried to be crafty and steal them + // this needs to run here in case players abort the mission by going back home + foreach (var characterItem in characterItems) + { + if (Survived(characterItem.Key) || !completed) + { + foreach (Item item in characterItem.Value) + { + if (!item.Removed) + { + item.Remove(); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index 507d7b61f..097d2ca16 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -30,6 +30,7 @@ namespace Barotrauma GameMain.Server?.UpdateMissionState(this); #endif ShowMessage(State); + OnMissionStateChanged?.Invoke(this); } } } @@ -145,7 +146,9 @@ namespace Barotrauma } private List delayedTriggerEvents = new List(); - + + public Action OnMissionStateChanged; + public Mission(MissionPrefab prefab, Location[] locations, Submarine sub) { System.Diagnostics.Debug.Assert(locations.Length == 2); @@ -355,7 +358,7 @@ namespace Barotrauma IEnumerable crewCharacters = GameSession.GetSessionCrewCharacters(); // use multipliers here so that we can easily add them together without introducing multiplicative XP stacking - var experienceGainMultiplier = new AbilityValue(1f); + var experienceGainMultiplier = new AbilityExperienceGainMultiplier(1f); crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnAllyGainMissionExperience, experienceGainMultiplier)); crewCharacters.ForEach(c => experienceGainMultiplier.Value += c.GetStatValue(StatTypes.MissionExperienceGainMultiplier)); @@ -374,11 +377,14 @@ namespace Barotrauma #endif // apply money gains afterwards to prevent them from affecting XP gains - var moneyGainMission = new AbilityValueMission(1f, this); - crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnGainMissionMoney, moneyGainMission)); - crewCharacters.ForEach(c => moneyGainMission.Value += c.GetStatValue(StatTypes.MissionMoneyGainMultiplier)); + var missionMoneyGainMultiplier = new AbilityMissionMoneyGainMultiplier(this, 1f); + crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnGainMissionMoney, missionMoneyGainMultiplier)); + crewCharacters.ForEach(c => missionMoneyGainMultiplier.Value += c.GetStatValue(StatTypes.MissionMoneyGainMultiplier)); - campaign.Money += (int)(reward * moneyGainMission.Value); + int totalReward = (int)(reward * missionMoneyGainMultiplier.Value); + campaign.Money += totalReward; + + GameAnalyticsManager.AddMoneyGainedEvent(totalReward, GameAnalyticsManager.MoneySource.MissionReward, Prefab.Identifier); foreach (Character character in crewCharacters) { @@ -534,4 +540,16 @@ namespace Barotrauma cargoRoom.Rect.Y - cargoRoom.Rect.Height + itemPrefab.Size.Y / 2); } } + + class AbilityMissionMoneyGainMultiplier : AbilityObject, IAbilityValue, IAbilityMission + { + public AbilityMissionMoneyGainMultiplier(Mission mission, float moneyGainMultiplier) + { + Value = moneyGainMultiplier; + Mission = mission; + } + public float Value { get; set; } + public Mission Mission { get; set; } + } + } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index 0fa150677..c6b93aab0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -96,9 +96,9 @@ namespace Barotrauma public readonly bool RequireWreck; /// - /// The mission can only be received when travelling from Pair.First to Pair.Second + /// The mission can only be received when travelling from a location of the first type to a location of the second type /// - public readonly List> AllowedConnectionTypes; + public readonly List<(string from, string to)> AllowedConnectionTypes; /// /// The mission can only be received in these location types @@ -185,7 +185,14 @@ namespace Barotrauma tags = element.GetAttributeStringArray("tags", new string[0], convertToLowerInvariant: true); - Name = TextManager.Get("MissionName." + TextIdentifier, true) ?? element.GetAttributeString("name", ""); + Name = TextManager.Get("MissionName." + TextIdentifier, true); + if (Name == null) + { +#if DEBUG + DebugConsole.ThrowError($"Error in mission \"{Identifier}\" - could not find a name in localization files. Make sure the texts are present in the loca file or that the mission is set to share texts with another mission using the TextIdentifier attribute."); +#endif + Name = element.GetAttributeString("name", ""); + } Description = TextManager.Get("MissionDescription." + TextIdentifier, true) ?? element.GetAttributeString("description", ""); Reward = element.GetAttributeInt("reward", 1); AllowRetry = element.GetAttributeBool("allowretry", false); @@ -209,10 +216,20 @@ namespace Barotrauma FailureMessage = element.GetAttributeString("failuremessage", ""); } - SonarLabel = - TextManager.Get("MissionSonarLabel." + TextIdentifier, true) ?? - TextManager.Get("MissionSonarLabel." + element.GetAttributeString("sonarlabel", ""), true) ?? - element.GetAttributeString("sonarlabel", ""); + if (element.Attribute("sonarlabel") == null) + { + SonarLabel = + TextManager.Get("MissionSonarLabel." + TextIdentifier, true) ?? + TextManager.Get("missionsonarlabel.target"); + } + else + { + SonarLabel = + TextManager.Get("MissionSonarLabel." + element.GetAttributeString("sonarlabel", ""), true) ?? + TextManager.Get(element.GetAttributeString("sonarlabel", ""), true) ?? + element.GetAttributeString("sonarlabel", ""); + } + SonarIconIdentifier = element.GetAttributeString("sonaricon", ""); MultiplayerOnly = element.GetAttributeBool("multiplayeronly", false); @@ -224,7 +241,7 @@ namespace Barotrauma Headers = new List(); Messages = new List(); - AllowedConnectionTypes = new List>(); + AllowedConnectionTypes = new List<(string from, string to)>(); for (int i = 0; i < 100; i++) { @@ -260,9 +277,7 @@ namespace Barotrauma } else { - AllowedConnectionTypes.Add(new Pair( - subElement.GetAttributeString("from", ""), - subElement.GetAttributeString("to", ""))); + AllowedConnectionTypes.Add((subElement.GetAttributeString("from", "").ToLowerInvariant(), subElement.GetAttributeString("to", "").ToLowerInvariant())); } break; case "locationtypechange": @@ -358,13 +373,15 @@ namespace Barotrauma AllowedLocationTypes.Any(lt => lt.Equals(from.Type.Identifier, StringComparison.OrdinalIgnoreCase)); } - foreach (Pair allowedConnectionType in AllowedConnectionTypes) + foreach ((string fromType, string toType) in AllowedConnectionTypes) { - if (allowedConnectionType.First.Equals("any", StringComparison.OrdinalIgnoreCase) || - allowedConnectionType.First.Equals(from.Type.Identifier, StringComparison.OrdinalIgnoreCase)) + if (fromType.Equals("any", StringComparison.OrdinalIgnoreCase) || + fromType.Equals(from.Type.Identifier, StringComparison.OrdinalIgnoreCase) || + (fromType == "anyoutpost" && from.HasOutpost())) { - if (allowedConnectionType.Second.Equals("any", StringComparison.OrdinalIgnoreCase) || - allowedConnectionType.Second.Equals(to.Type.Identifier, StringComparison.OrdinalIgnoreCase)) + if (toType.Equals("any", StringComparison.OrdinalIgnoreCase) || + toType.Equals(to.Type.Identifier, StringComparison.OrdinalIgnoreCase) || + (toType == "anyoutpost" && to.HasOutpost())) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs index f71ef9992..bc69390bf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs @@ -345,12 +345,13 @@ namespace Barotrauma protected override void UpdateMissionSpecific(float deltaTime) { - int newState = State; + if (state >= 2) { return; } + float sqrSonarRange = MathUtils.Pow2(Sonar.DefaultSonarRange); outsideOfSonarRange = Vector2.DistanceSquared(enemySub.WorldPosition, Submarine.MainSub.WorldPosition) > sqrSonarRange; - if (State < 2 && CheckWinState()) + if (CheckWinState()) { - newState = 2; + State = 2; } else { @@ -366,7 +367,7 @@ namespace Barotrauma } if (!outsideOfSonarRange || patrolPositions.None()) { - newState = 1; + State = 1; } break; case 1: @@ -391,14 +392,13 @@ namespace Barotrauma break; } } - State = newState; } private bool CheckWinState() => !IsClient && characters.All(m => DeadOrCaptured(m)); private bool DeadOrCaptured(Character character) { - return character == null || character.Removed || character.IsDead || (character.LockHands && character.Submarine == Submarine.MainSub); + return character == null || character.Removed || character.Submarine == null || (character.LockHands && character.Submarine == Submarine.MainSub) || character.IsIncapacitated; } public override void End() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index 5f628660c..0bbb70d87 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -60,8 +60,16 @@ namespace Barotrauma } else { - string itemIdentifier = prefab.ConfigElement.GetAttributeString("itemidentifier", ""); - itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab; + string itemIdentifier = prefab.ConfigElement.GetAttributeString("itemidentifier", null); + if (itemIdentifier != null) + { + itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab; + } + if (itemPrefab == null) + { + string itemTag = prefab.ConfigElement.GetAttributeString("itemtag", ""); + itemPrefab = MapEntityPrefab.GetRandom(p => p.Tags.Contains(itemTag), Rand.RandSync.Unsynced) as ItemPrefab; + } if (itemPrefab == null) { DebugConsole.ThrowError("Error in SalvageMission - couldn't find an item prefab with the identifier " + itemIdentifier); @@ -150,8 +158,8 @@ namespace Barotrauma if (item == null) { item = new Item(itemPrefab, position, null); + item.body.SetTransformIgnoreContacts(item.body.SimPosition, item.body.Rotation); item.body.FarseerBody.BodyType = BodyType.Kinematic; - item.FindHull(); } for (int i = 0; i < statusEffects.Count; i++) @@ -192,7 +200,7 @@ namespace Barotrauma } if (validContainers.Any()) { - var selectedContainer = validContainers.GetRandom(); + var selectedContainer = validContainers.GetRandom(Rand.RandSync.Unsynced); if (selectedContainer.Combine(item, user: null)) { #if SERVER diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index 21742b295..bae60c6e4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -15,6 +15,7 @@ namespace Barotrauma private readonly float scatter; private readonly float offset; + private readonly float delayBetweenSpawns; private Vector2? spawnPos; @@ -25,7 +26,7 @@ namespace Barotrauma private bool spawnPending; - private readonly int maxAmountPerLevel = int.MaxValue; + public readonly int MaxAmountPerLevel = int.MaxValue; public List Monsters => monsters; public Vector2? SpawnPos => spawnPos; @@ -73,7 +74,7 @@ namespace Barotrauma minAmount = prefab.ConfigElement.GetAttributeInt("minamount", defaultAmount); maxAmount = Math.Max(prefab.ConfigElement.GetAttributeInt("maxamount", 1), minAmount); - maxAmountPerLevel = prefab.ConfigElement.GetAttributeInt("maxamountperlevel", int.MaxValue); + MaxAmountPerLevel = prefab.ConfigElement.GetAttributeInt("maxamountperlevel", int.MaxValue); var spawnPosTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", ""); if (string.IsNullOrWhiteSpace(spawnPosTypeStr) || @@ -92,6 +93,7 @@ namespace Barotrauma offset = prefab.ConfigElement.GetAttributeFloat("offset", 0); scatter = Math.Clamp(prefab.ConfigElement.GetAttributeFloat("scatter", 500), 0, 3000); + delayBetweenSpawns = prefab.ConfigElement.GetAttributeFloat("delaybetweenspawns", 0.1f); if (GameMain.NetworkMember != null) { @@ -326,7 +328,7 @@ namespace Barotrauma } else { - dir = new Vector2(1, Rand.Range(-1, 1)); + dir = new Vector2(1, Rand.Range(-1f, 1f)); } Vector2 targetPos = spawnPos.Value + dir * offset; var targetWaypoint = waypoints.OrderBy(wp => Vector2.DistanceSquared(wp.WorldPosition, targetPos)).FirstOrDefault(); @@ -365,9 +367,9 @@ namespace Barotrauma if (spawnPos == null) { - if (maxAmountPerLevel < int.MaxValue) + if (MaxAmountPerLevel < int.MaxValue) { - if (Character.CharacterList.Count(c => c.SpeciesName == speciesName) >= maxAmountPerLevel) + if (Character.CharacterList.Count(c => c.SpeciesName == speciesName) >= MaxAmountPerLevel) { disallowed = true; return; @@ -473,6 +475,7 @@ namespace Barotrauma { scatterAmount = 0; } + for (int i = 0; i < amount; i++) { string seed = Level.Loaded.Seed + i.ToString(); @@ -538,7 +541,14 @@ namespace Barotrauma SwarmBehavior.CreateSwarm(monsters.Cast()); DebugConsole.NewMessage($"Spawned: {ToString()}. Strength: {StringFormatter.FormatZeroDecimal(monsters.Sum(m => m.Params.AI.CombatStrength))}.", Color.LightBlue, debugOnly: true); } - }, Rand.Range(0f, amount / 2f)); + + if (GameMain.GameSession != null) + { + GameAnalyticsManager.AddDesignEvent( + $"MonsterSpawn:{GameMain.GameSession.GameMode?.Preset?.Identifier ?? "none"}:{Level.Loaded?.LevelData?.Biome?.Identifier ?? "none"}:{SpawnPosType}:{speciesName}", + value: Timing.TotalTime - GameMain.GameSession.RoundStartTime); + } + }, delayBetweenSpawns * i); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs index 13c0fc231..5be24397f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs @@ -45,6 +45,8 @@ namespace Barotrauma requiredDestinationTypes = prefab.ConfigElement.GetAttributeStringArray("requireddestinationtypes", null); RequireBeaconStation = prefab.ConfigElement.GetAttributeBool("requirebeaconstation", false); + + GameAnalyticsManager.AddDesignEvent($"ScriptedEvent:{prefab.Identifier}:Start"); } public void AddTarget(string tag, Entity target) @@ -229,5 +231,11 @@ namespace Barotrauma } return false; } + + public override void Finished() + { + base.Finished(); + GameAnalyticsManager.AddDesignEvent($"ScriptedEvent:{prefab.Identifier}:Finished:{CurrentActionIndex}"); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs index bc72b77cf..89327c8f0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs @@ -133,7 +133,7 @@ namespace Barotrauma.Extensions return source.Count(predicate) > 1; } } - + public static IEnumerable ToEnumerable(this T item) { yield return item; @@ -196,5 +196,28 @@ namespace Barotrauma.Extensions } return -1; } + + /// + /// Same as FirstOrDefault but will always return null instead of default(T) when no element is found + /// + public static T? FirstOrNull(this IEnumerable source, Func predicate) where T : struct + { + if (source.FirstOrDefault(predicate) is var first && !first.Equals(default(T))) + { + return first; + } + + return null; + } + + public static T? FirstOrNull(this IEnumerable source) where T : struct + { + if (source.FirstOrDefault() is var first && !first.Equals(default(T))) + { + return first; + } + + return null; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs index 6d0e4d255..e8d0be2ea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs @@ -1,12 +1,11 @@ -using System; using Barotrauma.Steam; using RestSharp; +using System; using System.Net; -using System.Threading.Tasks; namespace Barotrauma { - public static partial class GameAnalyticsManager + static partial class GameAnalyticsManager { public enum Consent { @@ -149,6 +148,13 @@ namespace Barotrauma SetConsent(Consent.Error); } + if (!SteamManager.IsInitialized) + { + DebugConsole.AddWarning("Error in GameAnalyticsManager.GetConsent: Could not get a Steam authentication ticket (not connected to Steam)."); + SetConsent(Consent.Error); + return; + } + string authTicketStr; try { @@ -183,7 +189,7 @@ namespace Barotrauma return; } - var response = ((Task)t).Result; + if (!t.TryGetResult(out IRestResponse response)) { return; } if (!CheckResponse(response)) { SetConsent(Consent.Error); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs index 71a7a7a68..763f59a77 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs @@ -1,5 +1,6 @@ #nullable enable using Barotrauma.IO; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; @@ -9,7 +10,7 @@ using System.Text; namespace Barotrauma { - public static partial class GameAnalyticsManager + static partial class GameAnalyticsManager { public enum ErrorSeverity { @@ -29,6 +30,61 @@ namespace Barotrauma Fail = 3 } + public enum CustomDimensions01 + { + Vanilla, + Modded + } + + public enum CustomDimensions02 + { + None, + Difficulty0to10, + Difficulty10to20, + Difficulty20to30, + Difficulty30to40, + Difficulty40to50, + Difficulty50to60, + Difficulty60to70, + Difficulty70to80, + Difficulty80to90, + Difficulty90to100, + } + + public enum ResourceCurrency + { + Money + } + + public enum ResourceFlowType + { + Undefined = 0, + Source = 1, + Sink = 2 + } + + public enum MoneySource + { + Unknown, + MissionReward, + Store, + Event, + Ability, + Cheat + } + + public enum MoneySink + { + Unknown, + Store, + Service, + Crew, + SubmarineUpgrade, + SubmarineWeapon, + SubmarinePurchase, + SubmarineSwitch + } + private readonly static HashSet sentEventIdentifiers = new HashSet(); private class Implementation : IDisposable @@ -69,17 +125,41 @@ namespace Barotrauma internal void AddProgressionEvent(ProgressionStatus status, string progression01, string progression02, string progression03) => addProgressionEvent03(status, progression01, progression02, progression03); + private readonly Action addResourceEvent; + internal void AddResourceEvent(ResourceFlowType flowType, string currency, float amount, string itemType, string itemId) + => addResourceEvent(flowType, currency, amount, itemType, itemId); + private readonly Action setCustomDimension01; internal void SetCustomDimension01(string dimension01) => setCustomDimension01(dimension01); private readonly Action configureAvailableCustomDimensions01; - internal void ConfigureAvailableCustomDimensions01(params string[] customDimensions) - => configureAvailableCustomDimensions01(customDimensions); + internal void ConfigureAvailableCustomDimensions01(params CustomDimensions01[] customDimensions) + => configureAvailableCustomDimensions01(customDimensions.Select(d => d.ToString()).ToArray()); + + private readonly Action setCustomDimension02; + internal void SetCustomDimension02(string dimension02) + => setCustomDimension02(dimension02); + + private readonly Action configureAvailableCustomDimensions02; + internal void ConfigureAvailableCustomDimensions02(params CustomDimensions02[] customDimensions) + => configureAvailableCustomDimensions02(customDimensions.Select(d => d.ToString()).ToArray()); + + private readonly Action configureAvailableResourceCurrencies; + internal void ConfigureAvailableResourceCurrencies(params ResourceCurrency[] customDimensions) + => configureAvailableResourceCurrencies(customDimensions.Select(d => d.ToString()).ToArray()); + + private readonly Action configureAvailableResourceItemTypes; + internal void ConfigureAvailableResourceItemTypes(params string[] resourceItemTypes) + => configureAvailableResourceItemTypes(resourceItemTypes); private readonly Action setEnabledInfoLog; internal void SetEnabledInfoLog(bool enabled) => setEnabledInfoLog(enabled); + + private readonly Action setEnabledVerboseLog; + internal void SetEnabledVerboseLog(bool enabled) + => setEnabledVerboseLog(enabled); #endregion #region Data required to fetch methods via reflection @@ -94,6 +174,7 @@ namespace Barotrauma private readonly object?[] args2 = new object?[2]; private readonly object?[] args3 = new object?[3]; private readonly object?[] args4 = new object?[4]; + private readonly object?[] args5 = new object?[5]; private Action Call(MethodInfo methodInfo) => () => methodInfo?.Invoke(null, null); @@ -131,6 +212,17 @@ namespace Barotrauma args4[3] = arg4; methodInfo.Invoke(null, args4); }; + + private Action Call(MethodInfo methodInfo) + => (T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => + { + args5[0] = arg1; + args5[1] = arg2; + args5[2] = arg3; + args5[3] = arg4; + args5[4] = arg5; + methodInfo.Invoke(null, args5); + }; #endregion private AssemblyLoadContext? loadContext; @@ -165,9 +257,15 @@ namespace Barotrauma var mainClass = getType(MainClass); var errorSeverityEnumType = getType($"{EnumPrefix}{nameof(ErrorSeverity)}"); var progressionStatusEnumType = getType($"{EnumPrefix}{nameof(ProgressionStatus)}"); + var resourceFlowTypeEnumType = getType($"{EnumPrefix}{nameof(ResourceFlowType)}"); MethodInfo getMethod(string name, Type[] types) { + foreach (var me in mainClass.GetMethods()) + { + var aksjdnakjsdnf = me; + } + return mainClass?.GetMethod(name, BindingFlags.Public | BindingFlags.Static, binder: null, types: types, modifiers: null) ?? throw new Exception($"Could not find method \"{name}\" with types {string.Join(',', types.Select(t => t.Name))}"); } @@ -190,12 +288,26 @@ namespace Barotrauma new Type[] { progressionStatusEnumType, typeof(string), typeof(string) })); addProgressionEvent03 = Call(getMethod(nameof(AddProgressionEvent), new Type[] { progressionStatusEnumType, typeof(string), typeof(string), typeof(string) })); + setCustomDimension01 = Call(getMethod(nameof(SetCustomDimension01), new Type[] { typeof(string) })); configureAvailableCustomDimensions01 = Call(getMethod(nameof(ConfigureAvailableCustomDimensions01), new Type[] { typeof(string[]) })); + setCustomDimension02 = Call(getMethod(nameof(SetCustomDimension02), + new Type[] { typeof(string) })); + configureAvailableCustomDimensions02 = Call(getMethod(nameof(ConfigureAvailableCustomDimensions02), + new Type[] { typeof(string[]) })); + + configureAvailableResourceCurrencies = Call(getMethod(nameof(ConfigureAvailableResourceCurrencies), + new Type[] { typeof(string[]) })); + configureAvailableResourceItemTypes = Call(getMethod(nameof(ConfigureAvailableResourceItemTypes), + new Type[] { typeof(string[]) })); + addResourceEvent = Call(getMethod(nameof(AddResourceEvent), + new Type[] { resourceFlowTypeEnumType, typeof(string), typeof(float), typeof(string), typeof(string) })); setEnabledInfoLog = Call(getMethod(nameof(SetEnabledInfoLog), new Type[] { typeof(bool) })); + setEnabledVerboseLog = Call(getMethod(nameof(SetEnabledVerboseLog), + new Type[] { typeof(bool) })); onQuit = Call(getMethod("OnQuit", Array.Empty())); } @@ -204,8 +316,7 @@ namespace Barotrauma private void OnQuit() { try - { - + { if (assembly != null) { onQuit?.Invoke(); } } catch (Exception e) @@ -298,10 +409,40 @@ namespace Barotrauma loadedImplementation?.AddProgressionEvent(progressionStatus, progression01, progression02, progression03); } - public static void SetCustomDimension01(string dimension) + public static void SetCustomDimension01(CustomDimensions01 dimension) { if (!SendUserStatistics) { return; } - loadedImplementation?.SetCustomDimension01(dimension); + loadedImplementation?.SetCustomDimension01(dimension.ToString()); + } + + public static void SetCurrentLevel(LevelData levelData) + { + if (!SendUserStatistics) { return; } + + CustomDimensions02 customDimension = CustomDimensions02.None; + if (levelData != null) + { + float levelDifficulty = levelData.Difficulty; + customDimension = (CustomDimensions02)MathHelper.Clamp((int)(levelDifficulty / 10) + 1, 0, Enum.GetValues(typeof(CustomDimensions02)).Length - 1); + } + + loadedImplementation?.SetCustomDimension02(customDimension.ToString()); + } + + public static void AddMoneyGainedEvent(int amount, MoneySource moneySource, string eventId) + { + AddResourceEvent(ResourceFlowType.Source, ResourceCurrency.Money, amount, moneySource.ToString(), eventId); + } + + public static void AddMoneySpentEvent(int amount, MoneySink moneySink, string eventId) + { + AddResourceEvent(ResourceFlowType.Sink, ResourceCurrency.Money, amount, moneySink.ToString(), eventId); + } + + private static void AddResourceEvent(ResourceFlowType flowType, ResourceCurrency currency, float amount, string eventType, string eventId) + { + if (!SendUserStatistics) { return; } + loadedImplementation?.AddResourceEvent(flowType, currency.ToString(), amount, eventType, eventId); } private static void Init() @@ -321,6 +462,7 @@ namespace Barotrauma try { loadedImplementation?.SetEnabledInfoLog(true); + loadedImplementation?.SetEnabledVerboseLog(true); } catch (Exception e) { @@ -359,7 +501,11 @@ namespace Barotrauma + exeName + ":" + AssemblyInfo.GitRevision + ":" + buildConfiguration); - loadedImplementation?.ConfigureAvailableCustomDimensions01("singleplayer", "multiplayer", "editor"); + loadedImplementation?.ConfigureAvailableCustomDimensions01(Enum.GetValues(typeof(CustomDimensions01)).Cast().ToArray()); + loadedImplementation?.ConfigureAvailableCustomDimensions02(Enum.GetValues(typeof(CustomDimensions02)).Cast().ToArray()); + loadedImplementation?.ConfigureAvailableResourceCurrencies(Enum.GetValues(typeof(ResourceCurrency)).Cast().ToArray()); + loadedImplementation?.ConfigureAvailableResourceItemTypes( + Enum.GetValues(typeof(MoneySink)).Cast().Select(s => s.ToString()).Union(Enum.GetValues(typeof(MoneySource)).Cast().Select(s => s.ToString())).ToArray()); InitKeys(); @@ -367,7 +513,6 @@ namespace Barotrauma + GameMain.Version.ToString() + exeName + ":" + ((exeHash?.ShortHash == null) ? "Unknown" : exeHash.ShortHash) + ":" - + AssemblyInfo.GitBranch + ":" + AssemblyInfo.GitRevision + ":" + buildConfiguration); } @@ -378,19 +523,25 @@ namespace Barotrauma return; } - var allPackages = GameMain.Config?.AllEnabledPackages.ToList(); - if (allPackages?.Count > 0) + if (GameMain.Config != null) { - StringBuilder sb = new StringBuilder("ContentPackage: "); - int i = 0; - foreach (ContentPackage cp in allPackages) + var allPackages = GameMain.Config.AllEnabledPackages.ToList(); + if (allPackages?.Count > 0) { - string trimmedName = cp.Name.Replace(":", "").Replace(" ", ""); - sb.Append(trimmedName.Substring(0, Math.Min(32, trimmedName.Length))); - if (i < allPackages.Count - 1) { sb.Append(" "); } + List packageNames = new List(); + foreach (ContentPackage cp in allPackages) + { + string sanitizedName = cp.Name.Replace(":", "").Replace(" ", ""); + sanitizedName = sanitizedName.Substring(0, Math.Min(32, sanitizedName.Length)); + packageNames.Add(sanitizedName); + loadedImplementation?.AddDesignEvent("ContentPackage:" + sanitizedName); + } + packageNames.Sort(); + loadedImplementation?.AddDesignEvent("AllContentPackages:" + string.Join(" ", packageNames)); } - loadedImplementation?.AddDesignEvent(sb.ToString()); + loadedImplementation?.AddDesignEvent("Language:" + GameMain.Config.Language); } + } static partial void InitKeys(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index 5d20749f1..e10ee447a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -220,10 +220,10 @@ namespace Barotrauma private static readonly (int quality, float commonness)[] qualityCommonnesses = new (int quality, float commonness)[Quality.MaxQuality + 1] { - (0, 0.85f), - (1, 0.125f), - (2, 0.0225f), - (3, 0.0025f), + (0, 1.0f), + (1, 0.0f), + (2, 0.0f), + (3, 0.0f), }; private static List SpawnItem(ItemPrefab itemPrefab, List containers, KeyValuePair validContainer, float difficultyModifier) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index 1d7049542..6b20e104d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -16,32 +16,98 @@ namespace Barotrauma { public ItemPrefab ItemPrefab { get; } public int Quantity { get; set; } + public bool? IsStoreComponentEnabled { get; set; } public PurchasedItem(ItemPrefab itemPrefab, int quantity) { ItemPrefab = itemPrefab; Quantity = quantity; + IsStoreComponentEnabled = null; } } class SoldItem { public ItemPrefab ItemPrefab { get; } - public ushort ID { get; } + public ushort ID { get; private set; } public bool Removed { get; set; } public byte SellerID { get; } + public SellOrigin Origin { get; } - public SoldItem(ItemPrefab itemPrefab, ushort id, bool removed, byte sellerId) + public enum SellOrigin + { + Character, + Submarine + } + + public SoldItem(ItemPrefab itemPrefab, ushort id, bool removed, byte sellerId, SellOrigin origin) { ItemPrefab = itemPrefab; ID = id; Removed = removed; SellerID = sellerId; + Origin = origin; + } + + public void SetItemId(ushort id) + { + if (ID != Entity.NullEntityID) + { + DebugConsole.ShowError("Error setting SoldItem.ID: ID has already been set and should not be changed."); + return; + } + ID = id; } } partial class CargoManager { + private class SoldEntity + { + public enum SellStatus + { + /// + /// Entity sold in SP. Or, entity sold by client and confirmed by server in MP. + /// + Confirmed, + /// + /// Entity sold by client in MP. Client has received at least one update from server after selling, but this entity wasn't yet confirmed. + /// + Unconfirmed, + /// + /// Entity sold by client in MP. Client hasn't yet received an update from server after selling. + /// + Local + } + + public Item Item { get; private set; } + public ItemPrefab ItemPrefab { get; } + public SellStatus Status { get; set; } + + public SoldEntity(Item item, SellStatus status) + { + Item = item; + ItemPrefab = item?.Prefab; + Status = status; + } + + public SoldEntity(ItemPrefab itemPrefab, SellStatus status) + { + ItemPrefab = itemPrefab; + Status = status; + } + + public void SetItem(Item item) + { + if (Item != null) + { + DebugConsole.ShowError($"Trying to set SoldEntity.Item, but it's already set!\n{Environment.StackTrace.CleanupStackTrace()}"); + return; + } + Item = item; + } + } + public const int MaxQuantity = 100; public List ItemsInBuyCrate { get; } = new List(); @@ -92,7 +158,7 @@ namespace Barotrauma public void ModifyItemQuantityInBuyCrate(ItemPrefab itemPrefab, int changeInQuantity) { - PurchasedItem itemInCrate = ItemsInBuyCrate.Find(i => i.ItemPrefab == itemPrefab); + var itemInCrate = ItemsInBuyCrate.Find(i => i.ItemPrefab == itemPrefab); if (itemInCrate != null) { itemInCrate.Quantity += changeInQuantity; @@ -109,6 +175,25 @@ namespace Barotrauma OnItemsInBuyCrateChanged?.Invoke(); } + public void ModifyItemQuantityInSubSellCrate(ItemPrefab itemPrefab, int changeInQuantity) + { + var itemInCrate = ItemsInSellFromSubCrate.Find(i => i.ItemPrefab == itemPrefab); + if (itemInCrate != null) + { + itemInCrate.Quantity += changeInQuantity; + if (itemInCrate.Quantity < 1) + { + ItemsInSellFromSubCrate.Remove(itemInCrate); + } + } + else if (changeInQuantity > 0) + { + itemInCrate = new PurchasedItem(itemPrefab, changeInQuantity); + ItemsInSellFromSubCrate.Add(itemInCrate); + } + OnItemsInSellFromSubCrateChanged?.Invoke(); + } + public void PurchaseItems(List itemsToPurchase, bool removeFromCrate) { // Check all the prices before starting the transaction @@ -132,6 +217,7 @@ namespace Barotrauma // Exchange money var itemValue = item.Quantity * buyValues[item.ItemPrefab]; campaign.Money -= itemValue; + GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier); Location.StoreCurrentBalance += itemValue; if (removeFromCrate) @@ -184,6 +270,82 @@ namespace Barotrauma OnPurchasedItemsChanged?.Invoke(); } + private Dictionary UndeterminedSoldEntities { get; } = new Dictionary(); + + public IEnumerable GetSellableItemsFromSub() + { + if (Submarine.MainSub == null) { return new List(); } + var confirmedSoldEntities = Enumerable.Empty(); + UndeterminedSoldEntities.Clear(); +#if CLIENT + confirmedSoldEntities = GetConfirmedSoldEntities(); + foreach (var soldEntity in SoldEntities) + { + if (soldEntity.Item != null) { continue; } + if (UndeterminedSoldEntities.TryGetValue(soldEntity.ItemPrefab, out int count)) + { + UndeterminedSoldEntities[soldEntity.ItemPrefab] = count + 1; + } + else + { + UndeterminedSoldEntities.Add(soldEntity.ItemPrefab, 1); + } + } +#endif + return Submarine.MainSub.GetItems(true).FindAll(item => + { + if (!IsItemSellable(item, confirmedSoldEntities)) { return false; } + if (item.GetRootInventoryOwner() is Character) { return false; } + if (!item.Components.All(c => !(c is Holdable h) || !h.Attachable || !h.Attached)) { return false; } + if (!item.Components.All(c => !(c is Wire w) || w.Connections.All(c => c == null))) { return false; } + if (!ItemAndAllContainersInteractable(item)) { return false; } + if (item.GetRootContainer() is Item rootContainer && rootContainer.HasTag("donttakeitems")) { return false; } + return true; + }).Distinct(); + + static bool ItemAndAllContainersInteractable(Item item) + { + do + { + if (!item.IsPlayerTeamInteractable) { return false; } + item = item.Container; + } while (item != null); + return true; + } + } + + private bool IsItemSellable(Item item, IEnumerable confirmedItems) + { + if (item.Removed) { return false; } + if (!item.Prefab.CanBeSold) { return false; } + if (item.SpawnedInCurrentOutpost) { return false; } + if (!item.Prefab.AllowSellingWhenBroken && item.ConditionPercentage < 90.0f) { return false; } + if (confirmedItems.Any(ci => ci.Item == item)) { return false; } + if (UndeterminedSoldEntities.TryGetValue(item.Prefab, out int count)) + { + int newCount = count - 1; + if (newCount > 0) + { + UndeterminedSoldEntities[item.Prefab] = newCount; + } + else + { + UndeterminedSoldEntities.Remove(item.Prefab); + } + return false; + } + if (item.OwnInventory?.Container is ItemContainer itemContainer) + { + var containedItems = item.ContainedItems; + if (containedItems.None()) { return true; } + // Allow selling the item if contained items are unsellable and set to be removed on deconstruct + if (itemContainer.RemoveContainedItemsOnDeconstruct && containedItems.All(it => !it.Prefab.CanBeSold)) { return true; } + // Otherwise there must be no contained items or the contained items must be confirmed as sold + if (!containedItems.All(it => confirmedItems.Any(ci => ci.Item == it))) { return false; } + } + return true; + } + public static void CreateItems(List itemsToSpawn, Submarine sub) { if (itemsToSpawn.Count == 0) { return; } @@ -265,11 +427,13 @@ namespace Barotrauma } var item = new Item(pi.ItemPrefab, position, wp.Submarine); - itemContainer?.Inventory.TryPutItem(item, null); - itemSpawned(item); + itemContainer?.Inventory.TryPutItem(item, null); + + itemSpawned(item); #if SERVER Entity.Spawner?.CreateNetworkEvent(item, false); #endif + (itemContainer?.Item ?? item).CampaignInteractionType = CampaignMode.InteractionType.Cargo; static void itemSpawned(Item item) { Submarine sub = item.Submarine ?? item.GetRootContainer()?.Submarine; @@ -291,7 +455,7 @@ namespace Barotrauma float floorPos = hull.Rect.Y - hull.Rect.Height; Vector2 position = new Vector2( - hull.Rect.Width > 40 ? Rand.Range(hull.Rect.X + 20, hull.Rect.Right - 20) : hull.Rect.Center.X, + hull.Rect.Width > 40 ? Rand.Range(hull.Rect.X + 20f, hull.Rect.Right - 20f) : hull.Rect.Center.X, floorPos); //check where the actual floor structure is in case the bottom of the hull extends below it diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index 5aa20f202..4856ff843 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -37,7 +37,6 @@ namespace Barotrauma { IsSinglePlayer = isSinglePlayer; conversationTimer = 5.0f; - InitProjectSpecific(); } @@ -47,7 +46,9 @@ namespace Barotrauma { if (order.TargetEntity == null) { - DebugConsole.ThrowError("Attempted to add an order with no target entity to CrewManager!\n" + Environment.StackTrace.CleanupStackTrace()); + string message = $"Attempted to add a \"{order.Name}\" order with no target entity to CrewManager!\n{Environment.StackTrace.CleanupStackTrace()}"; + DebugConsole.AddWarning(message); + GameAnalyticsManager.AddErrorEventOnce("CrewManager.AddOrder:OrderTargetEntityNull", GameAnalyticsManager.ErrorSeverity.Error, message); return false; } @@ -98,10 +99,10 @@ namespace Barotrauma foreach (XElement characterElement in element.Elements()) { if (!characterElement.Name.ToString().Equals("character", StringComparison.OrdinalIgnoreCase)) { continue; } - CharacterInfo characterInfo = new CharacterInfo(characterElement); #if CLIENT if (characterElement.GetAttributeBool("lastcontrolled", false)) { characterInfo.LastControlled = true; } + characterInfo.CrewListIndex = characterElement.GetAttributeInt("crewlistindex", -1); #endif characterInfos.Add(characterInfo); foreach (XElement subElement in characterElement.Elements()) @@ -131,7 +132,7 @@ namespace Barotrauma characterInfos.Remove(characterInfo); } - public void AddCharacter(Character character) + public void AddCharacter(Character character, bool sortCrewList = true) { if (character.Removed) { @@ -153,7 +154,11 @@ namespace Barotrauma characterInfos.Add(character.Info); } #if CLIENT - AddCharacterToCrewList(character); + var characterComponent = AddCharacterToCrewList(character); + if (sortCrewList) + { + SortCrewList(); + } if (character.CurrentOrders != null) { foreach (var order in character.CurrentOrders) @@ -185,6 +190,10 @@ namespace Barotrauma public void InitRound() { +#if CLIENT + GUIContextMenu.CurrentContextMenu = null; +#endif + characters.Clear(); List spawnWaypoints = null; @@ -248,12 +257,16 @@ namespace Barotrauma } } - AddCharacter(character); + AddCharacter(character, sortCrewList: false); #if CLIENT if (IsSinglePlayer && (Character.Controlled == null || character.Info.LastControlled)) { Character.Controlled = character; } #endif } +#if CLIENT + if (IsSinglePlayer) { SortCrewList(); } +#endif + //longer delay in multiplayer to prevent the server from triggering NPC conversations while the players are still loading the round conversationTimer = IsSinglePlayer ? Rand.Range(5.0f, 10.0f) : Rand.Range(45.0f, 60.0f); } @@ -435,19 +448,21 @@ namespace Barotrauma filteredCharacters = filteredCharacters.Union(extraCharacters); } return filteredCharacters - // 1. Prioritize those who are on the same submarine than the controlled character + // Prioritize those who are on the same submarine as the controlled character .OrderByDescending(c => Character.Controlled == null || c.Submarine == Character.Controlled.Submarine) - // 2. Prioritize those who have been given the same maintenance or operate order as now issued - .ThenByDescending(c => c.CurrentOrders.Any(o => - o.Order != null && o.Order.Identifier == order.Identifier && - (order.Category == OrderCategory.Maintenance || order.Category == OrderCategory.Operate))) - // 3. Prioritize those with the appropriate job for the order + // Prioritize those who are already ordered to operate the device + .ThenByDescending(c => order.Category == OrderCategory.Operate && c.CurrentOrders.Any(o => o.Order != null && o.Order.Identifier == order.Identifier && o.Order.TargetEntity == order.TargetEntity)) + // Prioritize those with the appropriate job for the order .ThenByDescending(c => order.HasAppropriateJob(c)) - // 4. Prioritize bots over player controlled characters + // Prioritize those who don't yet have the same order (which allows quick-assigning the order to different characters) + .ThenByDescending(c => c.CurrentOrders.None(o => o.Order != null && o.Order.Identifier == order.Identifier)) + // Prioritize those with the preferred job for the order + .ThenByDescending(c => order.HasPreferredJob(c)) + // Prioritize bots over player-controlled characters .ThenByDescending(c => c.IsBot) - // 5. Use the priority value of the current objective + // Prioritize those with a lower current objective priority .ThenBy(c => c.AIController is HumanAIController humanAI ? humanAI.ObjectiveManager.CurrentObjective?.Priority : 0) - // 6. Prioritize those with the best skill for the order + // Prioritize those with a higher order skill level .ThenByDescending(c => c.GetSkillLevel(order.AppropriateSkill)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs index b63562956..d22a8e83c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs @@ -14,7 +14,7 @@ namespace Barotrauma public Faction(CampaignMetadata metadata, FactionPrefab prefab) { Prefab = prefab; - Reputation = new Reputation(metadata, $"faction.{prefab.Identifier}", prefab.MinReputation, prefab.MaxReputation, prefab.InitialReputation); + Reputation = new Reputation(metadata, this, prefab.MinReputation, prefab.MaxReputation, prefab.InitialReputation); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs index 85a3ef2f4..ada24867c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs @@ -35,9 +35,22 @@ namespace Barotrauma private set { if (MathUtils.NearlyEqual(Value, value)) { return; } + + float prevValue = Value; + Metadata.SetValue(metaDataIdentifier, Math.Clamp(value, MinReputation, MaxReputation)); OnReputationValueChanged?.Invoke(); OnAnyReputationValueChanged?.Invoke(); +#if CLIENT + int increase = (int)Value - (int)prevValue; + if (increase != 0 && Character.Controlled != null) + { + Character.Controlled.AddMessage( + TextManager.GetWithVariable("reputationgainnotification", "[reputationname]", Location?.Name ?? Faction.Prefab.Name), + increase > 0 ? GUI.Style.Green : GUI.Style.Red, + playSound: true, Identifier, increase, lifetime: 5.0f); + } +#endif } } @@ -63,15 +76,32 @@ namespace Barotrauma public Action OnReputationValueChanged; public static Action OnAnyReputationValueChanged; - public Reputation(CampaignMetadata metadata, string identifier, int minReputation, int maxReputation, int initialReputation) + public readonly Faction Faction; + public readonly Location Location; + + + public Reputation(CampaignMetadata metadata, Location location, string identifier, int minReputation, int maxReputation, int initialReputation) + : this(metadata, null, location, identifier, minReputation, maxReputation, initialReputation) + { + } + + public Reputation(CampaignMetadata metadata, Faction faction, int minReputation, int maxReputation, int initialReputation) + : this(metadata, faction, null, $"faction.{faction.Prefab.Identifier}", minReputation, maxReputation, initialReputation) + { + } + + private Reputation(CampaignMetadata metadata, Faction faction, Location location, string identifier, int minReputation, int maxReputation, int initialReputation) { System.Diagnostics.Debug.Assert(metadata != null); + System.Diagnostics.Debug.Assert(faction != null || location != null); Metadata = metadata; Identifier = identifier.ToLowerInvariant(); metaDataIdentifier = $"reputation.{Identifier}"; MinReputation = minReputation; MaxReputation = maxReputation; InitialReputation = initialReputation; + Faction = faction; + Location = location; } public string GetReputationName() diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index ff3462958..21e7ecf93 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -78,10 +78,19 @@ namespace Barotrauma //there can be no events before this time has passed during the 1st campaign round const float FirstRoundEventDelay = 0.0f; - public enum InteractionType { None, Talk, Examine, Map, Crew, Store, Repair, Upgrade, PurchaseSub } + public double TotalPlayTime; + public int TotalPassedLevels; + + public enum InteractionType { None, Talk, Examine, Map, Crew, Store, Repair, Upgrade, PurchaseSub, MedicalClinic, Cargo } + + public static bool BlocksInteraction(InteractionType interactionType) + { + return interactionType != InteractionType.None && interactionType != InteractionType.Cargo; + } public readonly CargoManager CargoManager; public UpgradeManager UpgradeManager; + public MedicalClinic MedicalClinic; public List Factions; @@ -91,7 +100,7 @@ namespace Barotrauma public CampaignSettings Settings; - private List extraMissions = new List(); + private readonly List extraMissions = new List(); public enum TransitionType { @@ -176,6 +185,7 @@ namespace Barotrauma { Money = InitialMoney; CargoManager = new CargoManager(this); + MedicalClinic = new MedicalClinic(this); } /// @@ -688,11 +698,13 @@ namespace Barotrauma GameAnalyticsManager.AddProgressionEvent( GameAnalyticsManager.ProgressionStatus.Complete, - Name ?? "none"); + Preset?.Identifier ?? "none"); 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", Money); + GameAnalyticsManager.AddDesignEvent(eventId + "Playtime", TotalPlayTime); + GameAnalyticsManager.AddDesignEvent(eventId + "PassedLevels", TotalPassedLevels); } protected virtual void EndCampaignProjSpecific() { } @@ -705,12 +717,14 @@ namespace Barotrauma location.RemoveHireableCharacter(characterInfo); CrewManager.AddCharacterInfo(characterInfo); Money -= characterInfo.Salary; + GameAnalyticsManager.AddMoneySpentEvent(characterInfo.Salary, GameAnalyticsManager.MoneySink.Crew, characterInfo.Job?.Prefab.Identifier ?? "unknown"); return true; } private void NPCInteract(Character npc, Character interactor) { if (!npc.AllowCustomInteract) { return; } + GameAnalyticsManager.AddDesignEvent("CampaignInteraction:" + Preset.Identifier + ":" + npc.CampaignInteractionType); NPCInteractProjSpecific(npc, interactor); string coroutineName = "DoCharacterWait." + (npc?.ID ?? Entity.NullEntityID); if (!CoroutineManager.IsCoroutineRunning(coroutineName)) @@ -874,6 +888,19 @@ namespace Barotrauma } public abstract void Save(XElement element); + + protected void LoadStats(XElement element) + { + TotalPlayTime = element.GetAttributeDouble(nameof(TotalPlayTime).ToLowerInvariant(), 0); + TotalPassedLevels = element.GetAttributeInt(nameof(TotalPassedLevels).ToLowerInvariant(), 0); + } + + protected XElement SaveStats() + { + return new XElement("stats", + new XAttribute(nameof(TotalPlayTime).ToLowerInvariant(), TotalPlayTime), + new XAttribute(nameof(TotalPassedLevels).ToLowerInvariant(), TotalPassedLevels)); + } public void LogState() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index 1191f30c4..533a3773e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -126,6 +126,10 @@ namespace Barotrauma { case "campaignsettings": Settings = new CampaignSettings(subElement); +#if CLIENT + GameMain.NetworkMember.ServerSettings.MaxMissionCount = Settings.MaxMissionCount; + GameMain.NetworkMember.ServerSettings.RadiationEnabled = Settings.RadiationEnabled; +#endif break; case "map": if (map == null) @@ -159,6 +163,9 @@ namespace Barotrauma case "pets": petsElement = subElement; break; + case "stats": + LoadStats(subElement); + break; #if SERVER case "savedexperiencepoints": foreach (XElement savedExp in subElement.Elements()) @@ -192,6 +199,37 @@ namespace Barotrauma } #endif } + + + public static List GetCampaignSubs() + { + bool isSubmarineVisible(SubmarineInfo s) + => !GameMain.NetworkMember.ServerSettings.HiddenSubs.Any(h + => s.Name.Equals(h, StringComparison.OrdinalIgnoreCase)); + + List availableSubs = + SubmarineInfo.SavedSubmarines + .Where(s => + s.IsCampaignCompatible + && isSubmarineVisible(s)) + .ToList(); + + if (!availableSubs.Any()) + { + //None of the available subs were marked as campaign-compatible, just include all visible subs + availableSubs.AddRange( + SubmarineInfo.SavedSubmarines + .Where(isSubmarineVisible)); + } + + if (!availableSubs.Any()) + { + //No subs are visible at all! Just make the selected one available + availableSubs.Add(GameMain.NetLobbyScreen.SelectedSub); + } + + return availableSubs; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index c26a39e80..64f517272 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -22,6 +22,8 @@ namespace Barotrauma public double RoundStartTime; + public double TimeSpentCleaning, TimeSpentPainting; + private readonly List missions = new List(); public IEnumerable Missions { get { return missions; } } @@ -276,6 +278,7 @@ namespace Barotrauma } Campaign.Money -= cost; + GameAnalyticsManager.AddMoneySpentEvent(cost, GameAnalyticsManager.MoneySink.SubmarineSwitch, newSubmarine.Name); ((CampaignMode)GameMode).PendingSubmarineSwitch = newSubmarine; return newSubmarine; @@ -288,6 +291,7 @@ namespace Barotrauma if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name)) { Campaign.Money -= newSubmarine.Price; + GameAnalyticsManager.AddMoneySpentEvent(newSubmarine.Price, GameAnalyticsManager.MoneySink.SubmarinePurchase, newSubmarine.Name); OwnedSubmarines.Add(newSubmarine); } } @@ -409,18 +413,52 @@ namespace Barotrauma GameAnalyticsManager.AddProgressionEvent( GameAnalyticsManager.ProgressionStatus.Start, - GameMode?.Name ?? "none"); + GameMode?.Preset?.Identifier ?? "none"); - string eventId = "StartRound:GameMode:" + (GameMode?.Name ?? "none") + ":"; + string eventId = "StartRound:" + (GameMode?.Preset?.Identifier ?? "none") + ":"; GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none")); - GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name ?? "none")); + GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Preset?.Identifier ?? "none")); GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0)); foreach (Mission mission in missions) { GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier); } - GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none")); + if (Level.Loaded != null) + { + string levelId = Level.Loaded.Type == LevelData.LevelType.Outpost ? + Level.Loaded.StartOutpost?.Info?.OutpostGenerationParams?.Identifier : + Level.Loaded.GenerationParams?.Identifier; + GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + Level.Loaded.Type.ToString() + ":" + (levelId ?? "null")); + } GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none")); +#if CLIENT + if (GameMode is TutorialMode tutorialMode) + { + GameAnalyticsManager.AddDesignEvent(eventId + tutorialMode.Tutorial.Identifier); + if (GameMain.IsFirstLaunch) + { + GameAnalyticsManager.AddDesignEvent("FirstLaunch:" + eventId + tutorialMode.Tutorial.Identifier); + } + } + GameAnalyticsManager.AddDesignEvent($"{eventId}HintManager:{(HintManager.Enabled ? "Enabled" : "Disabled")}"); +#endif + if (GameMode is CampaignMode campaignMode) + { + if (campaignMode.Map?.Radiation != null && campaignMode.Map.Radiation.Enabled) + { + GameAnalyticsManager.AddDesignEvent(eventId + "Radiation:Enabled"); + } + else + { + GameAnalyticsManager.AddDesignEvent(eventId + "Radiation:Disabled"); + } + bool firstTimeInBiome = Map != null && !Map.Connections.Any(c => c.Passed && c.Biome == LevelData.Biome); + if (firstTimeInBiome) + { + GameAnalyticsManager.AddDesignEvent(eventId + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none") + "Discovered:Playtime", campaignMode.TotalPlayTime); + GameAnalyticsManager.AddDesignEvent(eventId + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none") + "Discovered:PassedLevels", campaignMode.TotalPassedLevels); + } + } #if CLIENT if (GameMode is CampaignMode) { SteamAchievementManager.OnBiomeDiscovered(levelData.Biome); } @@ -457,6 +495,8 @@ namespace Barotrauma } } + ReadyCheck.ReadyCheckCooldown = DateTime.MinValue; + GUI.PreventPauseMenuToggle = false; HintManager.OnRoundStarted(); @@ -752,38 +792,29 @@ namespace Barotrauma GameMode?.End(transitionType); EventManager?.EndRound(); StatusEffect.StopAll(); - missions.Clear(); IsRunning = false; - - bool success = false; #if CLIENT - success = CrewManager.GetCharacters().Any(c => !c.IsDead); + bool success = CrewManager.GetCharacters().Any(c => !c.IsDead); #else - success = GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead); + bool success = GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead); #endif double roundDuration = Timing.TotalTime - RoundStartTime; GameAnalyticsManager.AddProgressionEvent( success ? GameAnalyticsManager.ProgressionStatus.Complete : GameAnalyticsManager.ProgressionStatus.Fail, GameMode?.Name ?? "none", roundDuration); - string eventId = "EndRound:GameMode:" + (GameMode?.Name ?? "none") + ":"; - GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"), roundDuration); - GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name ?? "none"), roundDuration); - GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0), roundDuration); - foreach (Mission mission in missions) - { - GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier + ":" + (mission.Completed ? "Completed" : "Failed"), roundDuration); - } - GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none"), roundDuration); - GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none"), roundDuration); + string eventId = "EndRound:" + (GameMode?.Preset?.Identifier ?? "none") + ":"; + LogEndRoundStats(eventId); if (GameMode is CampaignMode campaignMode) { GameAnalyticsManager.AddDesignEvent(eventId + "MoneyEarned", campaignMode.Money - prevMoney); + campaignMode.TotalPlayTime += roundDuration; } #if CLIENT HintManager.OnRoundEnded(); #endif + missions.Clear(); } finally { @@ -791,6 +822,82 @@ namespace Barotrauma } } + public void LogEndRoundStats(string eventId) + { + double roundDuration = Timing.TotalTime - RoundStartTime; + GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"), roundDuration); + GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name ?? "none"), roundDuration); + GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0), roundDuration); + foreach (Mission mission in missions) + { + GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier + ":" + (mission.Completed ? "Completed" : "Failed"), roundDuration); + } + if (Level.Loaded != null) + { + string levelId = Level.Loaded.Type == LevelData.LevelType.Outpost ? + Level.Loaded.StartOutpost?.Info?.OutpostGenerationParams?.Identifier : + Level.Loaded.GenerationParams?.Identifier; + GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none" + ":" + (levelId ?? "null")), roundDuration); + GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none"), roundDuration); + } + + if (Submarine.MainSub != null) + { + Dictionary submarineInventory = new Dictionary(); + foreach (Item item in Item.ItemList) + { + var rootContainer = item.GetRootContainer() ?? item; + if (rootContainer.Submarine?.Info == null || rootContainer.Submarine.Info.Type != SubmarineType.Player) { continue; } + if (rootContainer.Submarine != Submarine.MainSub && !Submarine.MainSub.DockedTo.Contains(rootContainer.Submarine)) { continue; } + + var holdable = item.GetComponent(); + if (holdable == null || holdable.Attached) { continue; } + var wire = item.GetComponent(); + if (wire != null && wire.Connections.Any(c => c != null)) { continue; } + + if (!submarineInventory.ContainsKey(item.Prefab)) + { + submarineInventory.Add(item.Prefab, 0); + } + submarineInventory[item.Prefab]++; + } + foreach (var subItem in submarineInventory) + { + GameAnalyticsManager.AddDesignEvent(eventId + "SubmarineInventory:" + subItem.Key.Identifier, subItem.Value); + } + } + + foreach (Character c in GetSessionCrewCharacters()) + { + foreach (var itemSelectedDuration in c.ItemSelectedDurations) + { + string characterType = "Unknown"; + if (c.IsBot) + { + characterType = "Bot"; + } + else if (c.IsPlayer) + { + characterType = "Player"; + } + GameAnalyticsManager.AddDesignEvent("TimeSpentOnDevices:" + (GameMode?.Preset?.Identifier ?? "none") + ":" + characterType + ":" + (c.Info?.Job?.Prefab.Identifier ?? "NoJob") + ":" + itemSelectedDuration.Key.Identifier, itemSelectedDuration.Value); + } + } +#if CLIENT + if (GameMode is TutorialMode tutorialMode) + { + GameAnalyticsManager.AddDesignEvent(eventId + tutorialMode.Tutorial.Identifier); + if (GameMain.IsFirstLaunch) + { + GameAnalyticsManager.AddDesignEvent("FirstLaunch:" + eventId + tutorialMode.Tutorial.Identifier); + } + } + GameAnalyticsManager.AddDesignEvent(eventId + "TimeSpentCleaning", TimeSpentCleaning); + GameAnalyticsManager.AddDesignEvent(eventId + "TimeSpentPainting", TimeSpentPainting); + TimeSpentCleaning = TimeSpentPainting = 0.0; +#endif + } + public void KillCharacter(Character character) { #if CLIENT @@ -901,14 +1008,7 @@ namespace Barotrauma ((CampaignMode)GameMode).Save(doc.Root); - try - { - doc.SaveSafe(filePath); - } - catch (Exception e) - { - DebugConsole.ThrowError("Saving gamesession to \"" + filePath + "\" failed!", e); - } + doc.SaveSafe(filePath, throwExceptions: true); } /*public void Load(XElement saveElement) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs new file mode 100644 index 000000000..80fe1bb35 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs @@ -0,0 +1,354 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Barotrauma.Extensions; + +namespace Barotrauma +{ + internal partial class MedicalClinic + { + public enum NetworkHeader + { + REQUEST_AFFLICTIONS, + REQUEST_PENDING, + ADD_PENDING, + REMOVE_PENDING, + CLEAR_PENDING, + HEAL_PENDING + } + + public enum AfflictionSeverity + { + Low, + Medium, + High + } + + public enum MessageFlag + { + Response, // responding to your request + Announce // responding to someone else's request + } + + public enum HealRequestResult + { + Unknown, // everything is not ok + Success, // everything ok + InsufficientFunds, // not enough money + Refused // the outpost has refused to provide medical assistance + } + + [NetworkSerialize] + public struct NetHealRequest : INetSerializableStruct + { + public HealRequestResult Result; + } + + [NetworkSerialize] + public struct NetRemovedAffliction : INetSerializableStruct + { + public NetCrewMember CrewMember; + public NetAffliction Affliction; + } + + public struct NetPendingCrew : INetSerializableStruct + { + [NetworkSerialize(ArrayMaxSize = CrewManager.MaxCrewSize)] + public NetCrewMember[] CrewMembers; + } + + public struct NetAffliction : INetSerializableStruct + { + [NetworkSerialize] + public string Identifier; + + [NetworkSerialize] + public ushort Strength; + + [NetworkSerialize] + public ushort Price; + + public AfflictionSeverity AfflictionSeverity + { + get + { + if (Prefab is null) { return AfflictionSeverity.Low; } + + float normalizedStrength = Strength / Prefab.MaxStrength; + + // lesser than 0.1 + if (normalizedStrength <= 0.1) + { + return AfflictionSeverity.Low; + } + + // between 0.1 and 0.5 + if (normalizedStrength > 0.1f && normalizedStrength < 0.5f) + { + return AfflictionSeverity.Medium; + } + + // greater than 0.5 + return AfflictionSeverity.High; + } + } + + public Affliction Affliction + { + set + { + Identifier = value.Identifier; + Strength = (ushort)Math.Ceiling(value.Strength); + Price = (ushort)(value.Prefab.BaseHealCost + Strength * value.Prefab.HealCostMultiplier); + } + } + + private AfflictionPrefab? cachedPrefab; + + public AfflictionPrefab? Prefab + { + get + { + if (cachedPrefab is { } cached) { return cached; } + + foreach (AfflictionPrefab prefab in AfflictionPrefab.List) + { + if (prefab.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase)) + { + cachedPrefab = prefab; + return prefab; + } + } + + return null; + } + set + { + cachedPrefab = value; + Identifier = value?.Identifier ?? string.Empty; + Strength = 0; + Price = 0; + } + } + + public readonly bool AfflictionEquals(AfflictionPrefab prefab) + { + return prefab.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase); + } + + public readonly bool AfflictionEquals(NetAffliction affliction) + { + return affliction.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase); + } + } + + public struct NetCrewMember : INetSerializableStruct + { + [NetworkSerialize] + public int CharacterInfoID; + + [NetworkSerialize] + public NetAffliction[] Afflictions; + + public CharacterInfo CharacterInfo + { + set => CharacterInfoID = value.GetIdentifierUsingOriginalName(); + } + + public readonly CharacterInfo? FindCharacterInfo(ImmutableArray crew) + { + foreach (CharacterInfo info in crew) + { + if (info.GetIdentifierUsingOriginalName() == CharacterInfoID) + { + return info; + } + } + + return null; + } + + public readonly bool CharacterEquals(NetCrewMember crewMember) + { + return crewMember.CharacterInfoID == CharacterInfoID; + } + } + + private readonly CampaignMode? campaign; + + public MedicalClinic(CampaignMode campaign) + { + this.campaign = campaign; + } + + public readonly List PendingHeals = new List(); + + public Action? OnUpdate; + + private static bool IsOutpostInCombat() + { + if (!(Level.Loaded is { Type: LevelData.LevelType.Outpost })) { return false; } + + IEnumerable crew = GetCrewCharacters().Where(c => c.Character != null).Select(c => c.Character).ToImmutableHashSet(); + + foreach (Character npc in Character.CharacterList.Where(c => c.TeamID == CharacterTeamType.FriendlyNPC)) + { + bool isInCombatWithCrew = !npc.IsInstigator && npc.AIController is HumanAIController { ObjectiveManager: { CurrentObjective: AIObjectiveCombat combatObjective } } && crew.Contains(combatObjective.Enemy); + if (isInCombatWithCrew) { return true; } + } + + return false; + } + + private HealRequestResult HealAllPending(bool force = false) + { + int totalCost = GetTotalCost(); + if (!force) + { + if (GetMoney() < totalCost) { return HealRequestResult.InsufficientFunds; } + + if (IsOutpostInCombat()) { return HealRequestResult.Refused; } + } + + ImmutableArray crew = GetCrewCharacters(); + foreach (NetCrewMember crewMember in PendingHeals) + { + CharacterInfo? targetCharacter = crewMember.FindCharacterInfo(crew); + if (!(targetCharacter?.Character is { CharacterHealth: { } health })) { continue; } + + foreach (NetAffliction affliction in crewMember.Afflictions) + { + health.ReduceAffliction(null, affliction.Identifier, affliction.Prefab?.MaxStrength ?? affliction.Strength); + } + } + + if (campaign != null) + { + campaign.Money -= totalCost; + } + + ClearPendingHeals(); + + return HealRequestResult.Success; + } + + private void ClearPendingHeals() + { + PendingHeals.Clear(); + } + + private void RemovePendingAffliction(NetCrewMember crewMember, NetAffliction affliction) + { + foreach (NetCrewMember listMember in PendingHeals.ToList()) + { + PendingHeals.Remove(listMember); + NetCrewMember pendingMember = listMember; + + if (pendingMember.CharacterEquals(crewMember)) + { + List newAfflictions = new List(); + foreach (NetAffliction pendingAffliction in pendingMember.Afflictions) + { + if (pendingAffliction.AfflictionEquals(affliction)) { continue; } + + newAfflictions.Add(pendingAffliction); + } + + pendingMember.Afflictions = newAfflictions.ToArray(); + } + + if (!pendingMember.Afflictions.Any()) { continue; } + + PendingHeals.Add(pendingMember); + } + } + + private void InsertPendingCrewMember(NetCrewMember crewMember) + { + if (PendingHeals.FirstOrNull(m => m.CharacterEquals(crewMember)) is { } foundHeal) + { + PendingHeals.Remove(foundHeal); + } + + PendingHeals.Add(crewMember); + } + + public static bool IsHealable(Affliction affliction) + { + return affliction.Prefab.HealableInMedicalClinic && affliction.Strength > GetShowTreshold(affliction); + static float GetShowTreshold(Affliction affliction) => Math.Max(0, Math.Min(affliction.Prefab.ShowIconToOthersThreshold, affliction.Prefab.ShowInHealthScannerThreshold)); + } + + private NetAffliction[] GetAllAfflictions(CharacterHealth health) + { + IEnumerable rawAfflictions = health.GetAllAfflictions().Where(a => IsHealable(a)); + + List afflictions = new List(); + + foreach (Affliction affliction in rawAfflictions) + { + NetAffliction newAffliction; + if (afflictions.FirstOrNull(netAffliction => netAffliction.AfflictionEquals(affliction.Prefab)) is { } foundAffliction) + { + afflictions.Remove(foundAffliction); + foundAffliction.Strength += (ushort)affliction.Strength; + foundAffliction.Price += (ushort)GetAdjustedPrice(GetHealPrice(affliction)); + newAffliction = foundAffliction; + } + else + { + newAffliction = new NetAffliction { Affliction = affliction }; + newAffliction.Price = (ushort)GetAdjustedPrice(newAffliction.Price); + } + + afflictions.Add(newAffliction); + } + + return afflictions.ToArray(); + + static int GetHealPrice(Affliction affliction) => (int)(affliction.Prefab.BaseHealCost + (affliction.Prefab.HealCostMultiplier * affliction.Strength)); + } + + public int GetTotalCost() => PendingHeals.SelectMany(h => h.Afflictions).Aggregate(0, (current, affliction) => current + affliction.Price); + + 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 static ImmutableArray GetCrewCharacters() + { +#if DEBUG && CLIENT + if (Screen.Selected is TestScreen) + { + return TestInfos.ToImmutableArray(); + } +#endif + + return Character.CharacterList.Where(c => c.Info != null && c.TeamID == CharacterTeamType.Team1).Select(c => c.Info).ToImmutableArray(); + } + +#if DEBUG && CLIENT + private static readonly CharacterInfo[] TestInfos = + { + new CharacterInfo("human"), + new CharacterInfo("human"), + new CharacterInfo("human"), + new CharacterInfo("human"), + new CharacterInfo("human"), + new CharacterInfo("human"), + new CharacterInfo("human") + }; + + private static readonly NetAffliction[] TestAfflictions = + { + new NetAffliction { Identifier = "internaldamage", Strength = 80, Price = 10 }, + new NetAffliction { Identifier = "blunttrauma", Strength = 50, Price = 10 }, + new NetAffliction { Identifier = "lacerations", Strength = 20, Price = 10 }, + new NetAffliction { Identifier = "burn", Strength = 10, Price = 10 } + }; +#endif + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs index 63c55fad1..5c0429327 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs @@ -225,6 +225,7 @@ namespace Barotrauma } Campaign.Money -= price; + GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarineUpgrade, prefab.Identifier); PurchasedUpgrade? upgrade = FindMatchingUpgrade(prefab, category); @@ -323,6 +324,7 @@ namespace Barotrauma } Campaign.Money -= price; + GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarineWeapon, itemToInstall.Identifier); foreach (Item itemToSwap in linkedItems) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs index 28a6d9571..c8b5cd5e7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs @@ -488,7 +488,11 @@ namespace Barotrauma var sortedSelected = enabledRegularPackages .OrderBy(p => -ContentPackage.RegularPackages.IndexOf(p)) .ToList(); - if (previousEnabledRegularPackages.SequenceEqual(sortedSelected)) { return; } + if (previousEnabledRegularPackages.SequenceEqual(sortedSelected)) + { + CheckModded(); + return; + } enabledRegularPackages.Clear(); enabledRegularPackages.AddRange(sortedSelected); CharacterPrefab.Prefabs.SortAll(); @@ -508,6 +512,20 @@ namespace Barotrauma { RefreshContentPackageItems(AllEnabledPackages.SelectMany(p => p.Files)); } + + CheckModded(); + + void CheckModded() + { + if (AllEnabledPackages.Any(p => p != GameMain.VanillaContent && p.HasMultiplayerIncompatibleContent)) + { + GameAnalyticsManager.SetCustomDimension01(GameAnalyticsManager.CustomDimensions01.Modded); + } + else + { + GameAnalyticsManager.SetCustomDimension01(GameAnalyticsManager.CustomDimensions01.Vanilla); + } + } } public void EnableContentPackageItems(IEnumerable unorderedFiles) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index 2aeb8ca59..bdabc58e7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -478,5 +478,18 @@ namespace Barotrauma return TryPutItem(item, user, new List() { placeToSlots }, createNetworkEvent, ignoreCondition); } + + protected override void PutItem(Item item, int i, Character user, bool removeItem = true, bool createNetworkEvent = true) + { + base.PutItem(item, i, user, removeItem, createNetworkEvent); +#if CLIENT + CreateSlots(); +#endif + if (item.CampaignInteractionType == CampaignMode.InteractionType.Cargo) + { + item.CampaignInteractionType = CampaignMode.InteractionType.None; + } + } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index 0258a3eee..9ec10aff0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -74,6 +74,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(true, false, description: "Should the OnUse StatusEffects trigger when docking (on vanilla docking ports these effects emit particles and play a sound).)")] + public bool ApplyEffectsOnDocking + { + get; + set; + } + [Editable, Serialize(DirectionType.None, false, description: "Which direction the port is allowed to dock in. For example, \"Top\" would mean the port can dock to another port above it.\n"+ "Normally there's no need to touch this setting, but if you notice the docking position is incorrect (for example due to some unusual docking port configuration without hulls or doors), you can use this to enforce the direction.")] public DirectionType ForceDockingDirection { get; set; } @@ -261,7 +268,7 @@ namespace Barotrauma.Items.Components DockingDir = GetDir(DockingTarget); DockingTarget.DockingDir = -DockingDir; - if (applyEffects) + if (applyEffects && ApplyEffectsOnDocking) { ApplyStatusEffects(ActionType.OnUse, 1.0f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index 2ff2e7d77..1689b9909 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -144,16 +144,6 @@ namespace Barotrauma.Items.Components if (linkedGap == null) { Rectangle rect = item.Rect; - if (IsHorizontal) - { - rect.Y += 5; - rect.Height += 10; - } - else - { - rect.X -= 5; - rect.Width += 10; - } linkedGap = new Gap(rect, !IsHorizontal, Item.Submarine) { Submarine = item.Submarine diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs index 279fa113d..eb7b5381c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs @@ -118,6 +118,7 @@ namespace Barotrauma.Items.Components if (character != null && !CharacterUsable) { return false; } CurrPowerConsumption = powerConsumption; + Voltage = 0.0f; charging = true; timer = Duration; IsActive = true; @@ -141,7 +142,7 @@ namespace Barotrauma.Items.Components timer -= deltaTime; if (charging) { - if (GetAvailableBatteryPower() >= powerConsumption) + if (GetAvailableInstantaneousBatteryPower() >= powerConsumption) { var batteries = item.GetConnectedComponents(); float neededPower = powerConsumption; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs index e73ea86f1..5ab363484 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs @@ -55,17 +55,22 @@ namespace Barotrauma.Items.Components [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 1f, ValueStep = 1f, DecimalCount = 0), Serialize("1,3", true, "Minumum and maximum amount of items or creatures to spawn in one attempt")] public Vector2 SpawnAmountRange { get; set; } - [Editable(MinValueInt = int.MinValue, MaxValueInt = int.MaxValue), Serialize(8, true, "Amount of items or creatures in the spawn area that will prevent further items or creatures from being spawned")] + [Editable(MinValueInt = 0, MaxValueInt = int.MaxValue), Serialize(8, true, "Total maximum amount of items or creatures that can be spawned. 0 = unrestricted.")] public int MaximumAmount { get; set; } - [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = int.MinValue, ValueStep = 10f), Serialize(500f, true, "Inflate the circle of rectangle by this value to extend the area that counts towards the maximum amount of items or enemies to be spawned")] + [Editable(MinValueInt = 0, MaxValueInt = int.MaxValue), Serialize(8, true, "Amount of items or creatures in the spawn area that will prevent further items or creatures from being spawned. 0 = unrestricted.")] + public int MaximumAmountInArea { get; set; } + + [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize(500f, true, "Inflate the circle of rectangle by this value to extend the area that counts towards the maximum amount of items or enemies to be spawned")] public float MaximumAmountRangePadding { get; set; } [Serialize(true, true, "")] public bool CanSpawn { get; set; } = true; - private float SpawnTimer; - private float? SpawnTimerGoal; + private float spawnTimer; + private float? spawnTimerGoal; + + private int spawnedAmount = 0; public EntitySpawnerComponent(Item item, XElement element) : base(item, element) { @@ -115,15 +120,15 @@ namespace Barotrauma.Items.Components if (minTime < 0 && maxTime < 0) { return; } - SpawnTimerGoal ??= Rand.Range(minTime, maxTime, Rand.RandSync.Unsynced); + spawnTimerGoal ??= Rand.Range(minTime, maxTime, Rand.RandSync.Unsynced); - SpawnTimer += deltaTime; + spawnTimer += deltaTime; - if (SpawnTimer > SpawnTimerGoal) + if (spawnTimer > spawnTimerGoal) { Spawn(); - SpawnTimerGoal = null; - SpawnTimer = 0; + spawnTimerGoal = null; + spawnTimer = 0; } } @@ -149,12 +154,12 @@ namespace Barotrauma.Items.Components private RectangleF GetAreaRectangle(Vector2 size, Vector2 offset, bool draw) { Vector2 pos = item.WorldPosition; + pos += offset; if (draw) { pos.Y = -pos.Y; } - pos += offset; RectangleF rect = new RectangleF(pos.X - size.X / 2f, pos.Y - size.Y / 2f, size.X, size.Y); return rect; } @@ -162,6 +167,7 @@ namespace Barotrauma.Items.Components private bool CanSpawnMore() { if (!CanSpawn) { return false; } + if (MaximumAmount > 0 && spawnedAmount >= MaximumAmount) { return false; } if (OnlySpawnWhenCrewInRange) { @@ -171,10 +177,9 @@ namespace Barotrauma.Items.Components } } - if (MaximumAmount < 0) { return true; } + if (MaximumAmountInArea <= 0) { return true; } int amount; - if (!string.IsNullOrWhiteSpace(SpeciesName)) { amount = Character.CharacterList.Count(c => !c.IsDead && c.SpeciesName.Equals(SpeciesName, StringComparison.OrdinalIgnoreCase) && IsInRange(c.WorldPosition, crewArea: false, rangePad: true)); @@ -188,13 +193,12 @@ namespace Barotrauma.Items.Components return false; } - return amount < MaximumAmount; + return amount < MaximumAmountInArea; } private bool IsInRange(Vector2 worldPos, bool crewArea = false, bool rangePad = false) { Vector2 offset = crewArea ? CrewAreaOffset : SpawnAreaOffset; - offset.Y = -offset.Y; switch (crewArea ? CrewAreaShape : SpawnAreaShape) { case AreaShape.Circle: @@ -269,6 +273,7 @@ namespace Barotrauma.Items.Components string[] allSpecies = SpeciesName.Split(','); string species = allSpecies.GetRandom().Trim(); Entity.Spawner?.AddToSpawnQueue(species, pos); + spawnedAmount++; } else if (!string.IsNullOrWhiteSpace(ItemIdentifier)) { @@ -283,6 +288,7 @@ namespace Barotrauma.Items.Components } Entity.Spawner?.AddToSpawnQueue(prefab, pos, item.Submarine); + spawnedAmount++; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs index 636c96300..c8b8782ba 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs @@ -94,37 +94,28 @@ namespace Barotrauma.Items.Components if (targetCharacter != null) { return; } - if (tainted) - { - if (selectedTaintedEffect != null) - { - float selectedTaintedEffectStrength = item.ConditionPercentage / 100.0f * selectedTaintedEffect.MaxStrength; - character.CharacterHealth.ApplyAffliction(null, selectedTaintedEffect.Instantiate(selectedTaintedEffectStrength)); - var existingAffliction = character.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedTaintedEffect); - if (existingAffliction != null) - { - existingAffliction.Strength = selectedTaintedEffectStrength; - } - targetCharacter = character; -#if SERVER - item.CreateServerEvent(this); -#endif - } - } if (selectedEffect != null) { - ApplyStatusEffects(ActionType.OnWearing, 1.0f); - float selectedEffectStrength = item.ConditionPercentage / 100.0f * selectedEffect.MaxStrength; - character.CharacterHealth.ApplyAffliction(null, selectedEffect.Instantiate(selectedEffectStrength)); - var existingAffliction = character.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedEffect); - if (existingAffliction != null) - { - existingAffliction.Strength = selectedEffectStrength; - } targetCharacter = character; + ApplyStatusEffects(ActionType.OnWearing, 1.0f); + float selectedEffectStrength = GetCombinedEffectStrength(); + character.CharacterHealth.ApplyAffliction(null, selectedEffect.Instantiate(selectedEffectStrength)); + var affliction = character.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedEffect); + if (affliction != null) { affliction.Strength = selectedEffectStrength; } #if SERVER item.CreateServerEvent(this); #endif + } + if (tainted && selectedTaintedEffect != null) + { + float selectedTaintedEffectStrength = GetCombinedTaintedEffectStrength(); + character.CharacterHealth.ApplyAffliction(null, selectedTaintedEffect.Instantiate(selectedTaintedEffectStrength)); + var affliction = character.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedTaintedEffect); + if (affliction != null) { affliction.Strength = selectedTaintedEffectStrength; } + targetCharacter = character; +#if SERVER + item.CreateServerEvent(this); +#endif } foreach (Item containedItem in item.ContainedItems) { @@ -142,13 +133,14 @@ namespace Barotrauma.Items.Components (rootContainer == null || !targetCharacter.HasEquippedItem(rootContainer) || !targetCharacter.Inventory.IsInLimbSlot(rootContainer, InvSlotType.HealthInterface))) { item.ApplyStatusEffects(ActionType.OnSevered, 1.0f, targetCharacter); - targetCharacter.CharacterHealth.ReduceAffliction(null, selectedEffect.Identifier, selectedEffect.MaxStrength); - if (tainted) - { - targetCharacter.CharacterHealth.ReduceAffliction(null, selectedTaintedEffect.Identifier, selectedTaintedEffect.MaxStrength); - } - targetCharacter = null; IsActive = false; + + var affliction = targetCharacter.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedEffect); + if (affliction != null) { affliction.Strength = GetCombinedEffectStrength(); } + var taintedAffliction = targetCharacter.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedTaintedEffect); + if (taintedAffliction != null) { taintedAffliction.Strength = GetCombinedTaintedEffectStrength(); } + + targetCharacter = null; } } } @@ -184,6 +176,36 @@ namespace Barotrauma.Items.Components } } + private float GetCombinedEffectStrength() + { + float effectStrength = 0.0f; + foreach (Item otherItem in targetCharacter.Inventory.FindAllItems(recursive: true)) + { + var geneticMaterial = otherItem.GetComponent(); + if (geneticMaterial == null || !geneticMaterial.IsActive) { continue; } + if (geneticMaterial.selectedEffect == selectedEffect) + { + effectStrength += otherItem.ConditionPercentage / 100.0f * selectedEffect.MaxStrength; + } + } + return effectStrength; + } + + private float GetCombinedTaintedEffectStrength() + { + float taintedEffectStrength = 0.0f; + foreach (Item otherItem in targetCharacter.Inventory.FindAllItems(recursive: true)) + { + var geneticMaterial = otherItem.GetComponent(); + if (geneticMaterial == null || !geneticMaterial.IsActive) { continue; } + if (selectedTaintedEffect != null && geneticMaterial.selectedTaintedEffect == selectedTaintedEffect) + { + taintedEffectStrength += otherItem.ConditionPercentage / 100.0f * selectedTaintedEffect.MaxStrength; + } + } + return taintedEffectStrength; + } + private float GetTaintedProbabilityOnRefine(Character user) { if (user == null) { return 1.0f; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs index bb67dea57..a9bd794cf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs @@ -1,13 +1,13 @@ #nullable enable -using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; using Barotrauma.Extensions; using Barotrauma.Networking; using FarseerPhysics; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; using Vector2 = Microsoft.Xna.Framework.Vector2; using Vector4 = Microsoft.Xna.Framework.Vector4; @@ -278,7 +278,7 @@ namespace Barotrauma.Items.Components for (int i = 0, j = 0; i < maxSides; i++) { - if (!occupiedSides.IsBitSet((TileSide) (1 << i))) + if (!occupiedSides.HasFlag((TileSide) (1 << i))) { pool[j] = i; j++; @@ -303,7 +303,7 @@ namespace Barotrauma.Items.Components public bool CanGrowMore() => (Sides | BlockedSides).Count() < 4; - public bool IsSideBlocked(TileSide side) => BlockedSides.IsBitSet(side) || Sides.IsBitSet(side); + public bool IsSideBlocked(TileSide side) => BlockedSides.HasFlag(side) || Sides.HasFlag(side); public static Rectangle CreatePlantRect(Vector2 pos) => new Rectangle((int) pos.X - Size / 2, (int) pos.Y + Size / 2, Size, Size); } @@ -398,8 +398,6 @@ namespace Barotrauma.Items.Components private int flowerVariants; private int leafVariants; private int[] flowerTiles; - private const int serverHealthUpdateDelay = 10; - private int serverHealthUpdateTimer; public float Health { @@ -553,19 +551,21 @@ namespace Barotrauma.Items.Components if (spawnProduct && ProducedItems.Any()) { - SpawnItem(ProducedItems.RandomElementByWeight(it => it.Probability), spawnPos); + SpawnItem(Item, ProducedItems.RandomElementByWeight(it => it.Probability), spawnPos); return; } if (spawnSeed) { - SpawnItem(ProducedSeed, spawnPos); + SpawnItem(Item, ProducedSeed, spawnPos); } - static void SpawnItem(ProducedItem producedItem, Vector2 pos) + static void SpawnItem(Item thisItem, ProducedItem producedItem, Vector2 pos) { if (producedItem.Prefab == null) { return; } + GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":GardeningProduce:" + thisItem.prefab.Identifier + ":" + producedItem.Prefab.Identifier); + Entity.Spawner?.AddToSpawnQueue(producedItem.Prefab, pos, onSpawned: it => { foreach (StatusEffect effect in producedItem.StatusEffects) @@ -586,8 +586,13 @@ namespace Barotrauma.Items.Components { if (Decayed) { return true; } - if (0 >= Health) + if (Health <= 0) { + if (!Decayed) + { + GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":GardeningDied:" + item.prefab.Identifier); + } + Decayed = true; #if CLIENT foreach (VineTile vine in Vines) @@ -774,7 +779,7 @@ namespace Barotrauma.Items.Components TileSide oppositeSide = connectingSide.GetOppositeSide(); - if (otherVine.BlockedSides.IsBitSet(connectingSide)) + if (otherVine.BlockedSides.HasFlag(connectingSide)) { newVine.BlockedSides |= oppositeSide; continue; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index c03c26b72..7d623bc39 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -166,7 +166,7 @@ namespace Barotrauma.Items.Components Pusher = new PhysicsBody(item.body.width, item.body.height, item.body.radius, item.body.Density) { BodyType = BodyType.Dynamic, - CollidesWith = Physics.CollisionCharacter, + CollidesWith = Physics.CollisionCharacter | Physics.CollisionProjectile, CollisionCategories = Physics.CollisionItemBlocking, Enabled = false, UserData = this @@ -237,10 +237,13 @@ namespace Barotrauma.Items.Components } } + private bool loadedFromXml; public override void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap) { base.Load(componentElement, usePrefabValues, idRemap); + loadedFromXml = true; + if (usePrefabValues) { //this needs to be loaded regardless @@ -536,7 +539,16 @@ namespace Barotrauma.Items.Components else { attachTargetCell = GetAttachTargetCell(150.0f); - if (attachTargetCell != null) { IsActive = true; } + if (attachTargetCell != null && attachTargetCell.IsDestructible) + { + attachTargetCell.OnDestroyed += () => + { + if (attachTargetCell != null && attachTargetCell.CellType != Voronoi2.CellType.Solid) + { + Drop(dropConnectedWires: true, dropper: null); + } + }; + } } } @@ -562,7 +574,7 @@ namespace Barotrauma.Items.Components public void DeattachFromWall() { - if (!attachable) return; + if (!attachable) { return; } Attached = false; attachTargetCell = null; @@ -604,7 +616,14 @@ namespace Barotrauma.Items.Components int maxAttachableCount = (int)character.Info.GetSavedStatValue(StatTypes.MaxAttachableCount, item.Prefab.Identifier); int currentlyAttachedCount = Item.ItemList.Count( i => i.Submarine == attachTarget?.Submarine && i.GetComponent() is Holdable holdable && holdable.Attached && i.Prefab.Identifier == item.prefab.Identifier); - if (currentlyAttachedCount >= maxAttachableCount) + if (maxAttachableCount == 0) + { +#if CLIENT + GUI.AddMessage(TextManager.Get("itemmsgrequiretraining"), Color.Red); +#endif + return false; + } + else if (currentlyAttachedCount >= maxAttachableCount) { #if CLIENT GUI.AddMessage($"{TextManager.Get("itemmsgtotalnumberlimited")} ({currentlyAttachedCount}/{maxAttachableCount})", Color.Red); @@ -726,15 +745,6 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - if (attachTargetCell != null) - { - if (attachTargetCell.CellType != Voronoi2.CellType.Solid) - { - Drop(dropConnectedWires: true, dropper: null); - } - return; - } - if (item.body == null || !item.body.Enabled) { return; } if (picker == null || !picker.HasEquippedItem(item)) { @@ -801,7 +811,7 @@ namespace Barotrauma.Items.Components equipLimb = picker.AnimController.GetLimb(LimbType.Torso); } - if (equipLimb != null) + if (equipLimb != null && !equipLimb.Removed) { float itemAngle = (equipLimb.Rotation + holdAngle * picker.AnimController.Dir); @@ -814,6 +824,11 @@ namespace Barotrauma.Items.Components } } + public override void ReceiveSignal(Signal signal, Connection connection) + { + //do nothing + } + public override void FlipX(bool relativeToSub) { handlePos[0].X = -handlePos[0].X; @@ -826,15 +841,25 @@ namespace Barotrauma.Items.Components public override void OnItemLoaded() { - if (item.Submarine != null && item.Submarine.Loading) return; + if (item.Submarine != null && item.Submarine.Loading) { return; } OnMapLoaded(); item.SetActiveSprite(); } public override void OnMapLoaded() { - if (!attachable) return; + 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) + { + if (attachedByDefault) + { + AttachToWall(); + return; + } + } + if (Attached) { AttachToWall(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs index c45a4894d..319c60ce4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs @@ -51,7 +51,11 @@ namespace Barotrauma.Items.Components #else if (deattachTimer >= DeattachDuration) { - holdable.DeattachFromWall(); + if (holdable.Attached) + { + GameAnalyticsManager.AddDesignEvent("ResourceCollected:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none") + ":" + item.Prefab.Identifier); + holdable.DeattachFromWall(); + } trigger.Enabled = false; } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index fcecfa35f..8099bd5d1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -115,7 +115,7 @@ namespace Barotrauma.Items.Components reloadTimer /= (1f + item.GetQualityModifier(Quality.StatType.StrikingSpeedMultiplier)); item.body.FarseerBody.CollisionCategories = Physics.CollisionProjectile; - item.body.FarseerBody.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall; + item.body.FarseerBody.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionItemBlocking; item.body.FarseerBody.OnCollision += OnCollision; item.body.FarseerBody.IsBullet = true; item.body.PhysEnabled = true; @@ -361,6 +361,10 @@ namespace Barotrauma.Items.Components } hitTargets.Add(targetItem); } + else if (f2.Body.UserData is Holdable holdable && holdable.CanPush) + { + hitTargets.Add(holdable.Item); + } else { return false; @@ -412,6 +416,14 @@ namespace Barotrauma.Items.Components if (targetItem.Removed) { return; } Attack.DoDamage(User, targetItem, item.WorldPosition, 1.0f); } + else if (target.UserData is Holdable holdable && holdable.CanPush) + { + if (holdable.Item.Removed) { return; } + Attack.DoDamage(User, holdable.Item, item.WorldPosition, 1.0f); + RestoreCollision(); + hitting = false; + User = null; + } else { return; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs index f2561695e..93001af68 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs @@ -74,7 +74,7 @@ namespace Barotrauma.Items.Components if (PickingTime > 0.0f) { - var abilityPickingTime = new AbilityValueItem(PickingTime, item.Prefab); + var abilityPickingTime = new AbilityItemPickingTime(PickingTime, item.Prefab); picker.CheckTalents(AbilityEffectType.OnItemPicked, abilityPickingTime); if (requiredItems.ContainsKey(RelatedItem.RelationType.Equipped)) @@ -300,4 +300,15 @@ namespace Barotrauma.Items.Components } } } + + class AbilityItemPickingTime : AbilityObject, IAbilityValue, IAbilityItemPrefab + { + public AbilityItemPickingTime(float pickingTime, ItemPrefab itemPrefab) + { + Value = pickingTime; + ItemPrefab = itemPrefab; + } + public float Value { get; set; } + public ItemPrefab ItemPrefab { get; set; } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index bee423e3d..e3ff2e8be 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -158,7 +158,7 @@ namespace Barotrauma.Items.Components return MathHelper.ToRadians(spread); } - private readonly List limbBodies = new List(); + private readonly List ignoredBodies = new List(); public override bool Use(float deltaTime, Character character = null) { tryingToCharge = true; @@ -172,8 +172,8 @@ namespace Barotrauma.Items.Components if (character != null) { - var abilityItem = new AbilityItem(item); - character.CheckTalents(AbilityEffectType.OnUseRangedWeapon, abilityItem); + var abilityRangedWeapon = new AbilityRangedWeapon(item); + character.CheckTalents(AbilityEffectType.OnUseRangedWeapon, abilityRangedWeapon); } if (item.AiTarget != null) @@ -182,11 +182,20 @@ namespace Barotrauma.Items.Components item.AiTarget.SightRange = item.AiTarget.MaxSightRange; } - limbBodies.Clear(); + ignoredBodies.Clear(); foreach (Limb l in character.AnimController.Limbs) { if (l.IsSevered) { continue; } - limbBodies.Add(l.body.FarseerBody); + ignoredBodies.Add(l.body.FarseerBody); + } + + foreach (Item heldItem in character.HeldItems) + { + var holdable = heldItem.GetComponent(); + if (holdable?.Pusher != null) + { + ignoredBodies.Add(holdable.Pusher.FarseerBody); + } } float degreeOfFailure = 1.0f - DegreeOfSuccess(character); @@ -211,7 +220,7 @@ namespace Barotrauma.Items.Components } float damageMultiplier = 1f + item.GetQualityModifier(Quality.StatType.FirepowerMultiplier); projectile.Launcher = item; - projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: limbBodies.ToList(), createNetworkEvent: false, damageMultiplier); + projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: ignoredBodies.ToList(), createNetworkEvent: false, damageMultiplier); projectile.Item.GetComponent()?.Attach(Item, projectile.Item); if (i == 0) { @@ -270,4 +279,12 @@ namespace Barotrauma.Items.Components partial void LaunchProjSpecific(); } + class AbilityRangedWeapon : AbilityObject, IAbilityItem + { + public AbilityRangedWeapon(Item item) + { + Item = item; + } + public Item Item { get; set; } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index bc9c658ae..e172945b6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -521,7 +521,7 @@ namespace Barotrauma.Items.Components if (!fixableEntities.Contains("structure") && !fixableEntities.Contains(targetStructure.Prefab.Identifier)) { return true; } - ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, new ISerializableEntity[] { targetStructure }); + ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, structure: targetStructure); FixStructureProjSpecific(user, deltaTime, targetStructure, sectionIndex); float structureFixAmount = StructureFixAmount; @@ -589,8 +589,7 @@ namespace Barotrauma.Items.Components closestLimb.body.ApplyForce(dir * TargetForce, maxVelocity: 10.0f); } - ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, - closestLimb == null ? new ISerializableEntity[] { targetCharacter } : new ISerializableEntity[] { targetCharacter, closestLimb }); + ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, character: targetCharacter, limb: closestLimb); FixCharacterProjSpecific(user, deltaTime, targetCharacter); return true; } @@ -606,7 +605,7 @@ namespace Barotrauma.Items.Components } targetLimb.character.LastDamageSource = item; - ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, new ISerializableEntity[] { targetLimb.character, targetLimb }); + ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, character: targetLimb.character, limb: targetLimb); FixCharacterProjSpecific(user, deltaTime, targetLimb.character); return true; } @@ -645,7 +644,7 @@ namespace Barotrauma.Items.Components targetItem.IsHighlighted = true; - ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, targetItem.AllPropertyObjects); + ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, targetItem); if (targetItem.body != null && !MathUtils.NearlyEqual(TargetForce, 0.0f)) { @@ -682,7 +681,7 @@ namespace Barotrauma.Items.Components Reset(); return true; } - if (leak.Submarine == null) + if (leak.Submarine == null || leak.Submarine != character.Submarine) { Reset(); return true; @@ -836,32 +835,48 @@ namespace Barotrauma.Items.Components } } - private void ApplyStatusEffectsOnTarget(Character user, float deltaTime, ActionType actionType, IEnumerable targets) + private static List currentTargets = new List(); + private void ApplyStatusEffectsOnTarget(Character user, float deltaTime, ActionType actionType, Item targetItem = null, Character character = null, Limb limb = null, Structure structure = null) { if (statusEffectLists == null) { return; } if (!statusEffectLists.TryGetValue(actionType, out List statusEffects)) { return; } foreach (StatusEffect effect in statusEffects) { + currentTargets.Clear(); effect.SetUser(user); if (effect.HasTargetType(StatusEffect.TargetType.UseTarget)) { - effect.Apply(actionType, deltaTime, item, targets); + if (targetItem != null) + { + currentTargets.AddRange(targetItem.AllPropertyObjects); + } + if (structure != null) + { + currentTargets.Add(structure); + } + if (character != null) + { + currentTargets.Add(character); + } + effect.Apply(actionType, deltaTime, item, currentTargets); } else if (effect.HasTargetType(StatusEffect.TargetType.Character)) { - effect.Apply(actionType, deltaTime, item, targets.Where(t => t is Character)); + currentTargets.Add(character); + effect.Apply(actionType, deltaTime, item, currentTargets); } else if (effect.HasTargetType(StatusEffect.TargetType.Limb)) { - effect.Apply(actionType, deltaTime, item, targets.Where(t => t is Limb)); + currentTargets.Add(limb); + effect.Apply(actionType, deltaTime, item, currentTargets); } #if CLIENT if (user == null) { return; } // Hard-coded progress bars for welding doors stuck. // A general purpose system could be better, but it would most likely require changes in the way we define the status effects in xml. - foreach (ISerializableEntity target in targets) + foreach (ISerializableEntity target in currentTargets) { if (!(target is Door door)) { continue; } if (!door.CanBeWelded || !door.Item.IsInteractable(user)) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index e140b3cd5..a1bb06f5f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -286,6 +286,43 @@ namespace Barotrauma.Items.Components SerializableProperties = SerializableProperty.DeserializeProperties(this, element); ParseMsg(); + string inheritRequiredSkillsFrom = element.GetAttributeString("inheritrequiredskillsfrom", ""); + if (!string.IsNullOrEmpty(inheritRequiredSkillsFrom)) + { + var component = item.Components.Find(ic => ic.Name.Equals(inheritRequiredSkillsFrom, StringComparison.OrdinalIgnoreCase)); + if (component == null) + { + DebugConsole.ThrowError($"Error in item \"{item.Name}\" - component \"{name}\" is set to inherit its required skills from \"{inheritRequiredSkillsFrom}\", but a component of that type couldn't be found."); + } + else + { + requiredSkills = component.requiredSkills; + } + } + + string inheritStatusEffectsFrom = element.GetAttributeString("inheritstatuseffectsfrom", ""); + if (!string.IsNullOrEmpty(inheritStatusEffectsFrom)) + { + var component = item.Components.Find(ic => ic.Name.Equals(inheritStatusEffectsFrom, StringComparison.OrdinalIgnoreCase)); + if (component == null) + { + DebugConsole.ThrowError($"Error in item \"{item.Name}\" - component \"{name}\" is set to inherit its StatusEffects from \"{inheritStatusEffectsFrom}\", but a component of that type couldn't be found."); + } + else if (component.statusEffectLists != null) + { + statusEffectLists ??= new Dictionary>(); + foreach (KeyValuePair> kvp in component.statusEffectLists) + { + if (!statusEffectLists.TryGetValue(kvp.Key, out List effectList)) + { + effectList = new List(); + statusEffectLists.Add(kvp.Key, effectList); + } + effectList.AddRange(kvp.Value); + } + } + } + foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -317,19 +354,8 @@ namespace Barotrauma.Items.Components requiredSkills.Add(new Skill(skillIdentifier, subElement.GetAttributeInt("level", 0))); break; case "statuseffect": - var statusEffect = StatusEffect.Load(subElement, item.Name); - - if (statusEffectLists == null) statusEffectLists = new Dictionary>(); - - List effectList; - if (!statusEffectLists.TryGetValue(statusEffect.type, out effectList)) - { - effectList = new List(); - statusEffectLists.Add(statusEffect.type, effectList); - } - - effectList.Add(statusEffect); - + statusEffectLists ??= new Dictionary>(); + LoadStatusEffect(subElement); break; default: if (LoadElemProjSpecific(subElement)) { break; } @@ -344,6 +370,17 @@ namespace Barotrauma.Items.Components break; } } + + void LoadStatusEffect(XElement subElement) + { + var statusEffect = StatusEffect.Load(subElement, item.Name); + if (!statusEffectLists.TryGetValue(statusEffect.type, out List effectList)) + { + effectList = new List(); + statusEffectLists.Add(statusEffect.type, effectList); + } + effectList.Add(statusEffect); + } } private void SetActiveState(bool isActive) @@ -401,6 +438,8 @@ namespace Barotrauma.Items.Components return false; } + public virtual bool UpdateWhenInactive => false; + //called when isActive is true and condition > 0.0f public virtual void Update(float deltaTime, Camera cam) { @@ -545,6 +584,7 @@ namespace Barotrauma.Items.Components { GUI.RemoveFromUpdateList(GuiFrame, true); GuiFrame.RectTransform.Parent = null; + GuiFrame = null; } #endif @@ -812,7 +852,10 @@ namespace Barotrauma.Items.Components foreach (ItemComponent ic in item.Components) { if (ic.statusEffectLists == null || !ic.statusEffectLists.TryGetValue(ActionType.OnBroken, out List brokenEffects)) { continue; } - brokenEffects.ForEach(e => e.SetUser(user)); + foreach (var brokenEffect in brokenEffects) + { + brokenEffect.SetUser(user); + } } } @@ -1021,7 +1064,8 @@ namespace Barotrauma.Items.Components return 0.0f; } } - return 1.0f; + // Prefer items with the same identifier as the contained items' + return container.ContainsItemsWithSameIdentifier(i) ? 1.0f : 0.5f; } }; containObjective.Abandoned += () => aiController.IgnoredItems.Add(container.Item); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index a7559b690..e32673000 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -208,12 +208,13 @@ namespace Barotrauma.Items.Components public override bool RecreateGUIOnResolutionChange => true; + public List ContainableItems { get; } + public ItemContainer(Item item, XElement element) : base(item, element) { int totalCapacity = capacity; - List containableItems = null; foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -225,8 +226,8 @@ namespace Barotrauma.Items.Components DebugConsole.ThrowError("Error in item config \"" + item.ConfigFile + "\" - containable with no identifiers."); continue; } - containableItems ??= new List(); - containableItems.Add(containable); + ContainableItems ??= new List(); + ContainableItems.Add(containable); break; case "subcontainer": totalCapacity += subElement.GetAttributeInt("capacity", 1); @@ -237,7 +238,7 @@ namespace Barotrauma.Items.Components slotRestrictions = new SlotRestrictions[totalCapacity]; for (int i = 0; i < capacity; i++) { - slotRestrictions[i] = new SlotRestrictions(maxStackSize, containableItems); + slotRestrictions[i] = new SlotRestrictions(maxStackSize, ContainableItems); } int subContainerIndex = capacity; @@ -303,7 +304,12 @@ namespace Barotrauma.Items.Components } } } - } + } + + if (item.GetComponent() != null) + { + GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":GardeningPlanted:" + containedItem.prefab.Identifier); + } //no need to Update() if this item has no statuseffects and no physics body IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null); @@ -344,6 +350,19 @@ namespace Barotrauma.Items.Components return slotRestrictions[index].MatchesItem(itemPrefab); } + public bool ContainsItemsWithSameIdentifier(Item item) + { + if (item == null) { return false; } + foreach (var containedItem in Inventory.AllItems) + { + if (containedItem.Prefab.Identifier == item.Prefab.Identifier) + { + return true; + } + } + return false; + } + readonly List targets = new List(); public override void Update(float deltaTime, Camera cam) @@ -432,7 +451,7 @@ namespace Barotrauma.Items.Components } } } - var abilityItem = new AbilityItem(item); + var abilityItem = new AbilityItemContainer(item); character.CheckTalents(AbilityEffectType.OnOpenItemContainer, abilityItem); return base.Select(character); @@ -494,6 +513,21 @@ namespace Barotrauma.Items.Components IsActive = true; } + public override void ReceiveSignal(Signal signal, Connection connection) + { + switch (connection.Name) + { + case "activate": + case "use": + case "trigger_in": + if (signal.value != "0") + { + item.Use(1.0f, signal.sender); + } + break; + } + } + public void SetContainedItemPositions() { Vector2 transformedItemPos = ItemPos * item.Scale; @@ -689,7 +723,6 @@ namespace Barotrauma.Items.Components } } - protected override void ShallowRemoveComponentSpecific() { } @@ -743,4 +776,13 @@ namespace Barotrauma.Items.Components return componentElement; } } + + class AbilityItemContainer : AbilityObject, IAbilityItem + { + public AbilityItemContainer(Item item) + { + Item = item; + } + public Item Item { get; set; } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index 38420fcd6..944fd6055 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -170,7 +170,7 @@ namespace Barotrauma.Items.Components character.CheckTalents(AbilityEffectType.OnItemDeconstructedByAlly, abilityTargetItem); } - var itemCreationMultiplier = new AbilityValueItem(amountMultiplier, targetItem.Prefab); + var itemCreationMultiplier = new AbilityItemCreationMultiplier(targetItem.Prefab, amountMultiplier); user.CheckTalents(AbilityEffectType.OnItemDeconstructedMaterial, itemCreationMultiplier); amountMultiplier = (int)itemCreationMultiplier.Value; } @@ -261,8 +261,8 @@ namespace Barotrauma.Items.Components if (user != null && !user.Removed) { // used to spawn items directly into the deconstructor - var itemContainer = new AbilityItemPrefabItem(item, targetItem.Prefab); - user.CheckTalents(AbilityEffectType.OnItemDeconstructedInventory, itemContainer); + var itemDeconstructedInventory = new AbilityItemDeconstructedInventory(targetItem.Prefab, item); + user.CheckTalents(AbilityEffectType.OnItemDeconstructedInventory, itemDeconstructedInventory); } int amount = (int)amountMultiplier; @@ -300,6 +300,8 @@ namespace Barotrauma.Items.Components } } + GameAnalyticsManager.AddDesignEvent("ItemDeconstructed:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none") + ":" + targetItem.prefab.Identifier); + if (targetItem.AllowDeconstruct && allowRemove) { //drop all items that are inside the deconstructed item @@ -333,7 +335,7 @@ namespace Barotrauma.Items.Components for (int i = 0; i < outputContainer.Capacity; i++) { var containedItem = outputContainer.Inventory.GetItemAt(i); - if (containedItem?.OwnInventory != null && containedItem.OwnInventory.TryPutItem(item, user: null)) + if (containedItem?.OwnInventory != null && containedItem.GetComponent() == null && containedItem.OwnInventory.TryPutItem(item, user: null)) { return; } @@ -454,4 +456,26 @@ namespace Barotrauma.Items.Components public Character Character { get; set; } } + class AbilityItemCreationMultiplier : AbilityObject, IAbilityValue, IAbilityItemPrefab + { + public AbilityItemCreationMultiplier(ItemPrefab itemPrefab, float itemAmountMultiplier) + { + ItemPrefab = itemPrefab; + Value = itemAmountMultiplier; + } + public ItemPrefab ItemPrefab { get; set; } + public float Value { get; set; } + } + + class AbilityItemDeconstructedInventory : AbilityObject, IAbilityItem, IAbilityItemPrefab + { + public AbilityItemDeconstructedInventory(ItemPrefab itemPrefab, Item item) + { + ItemPrefab = itemPrefab; + Item = item; + } + public ItemPrefab ItemPrefab { get; set; } + public Item Item { get; set; } + } + } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs index 266c28fff..177571933 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs @@ -112,7 +112,7 @@ namespace Barotrauma.Items.Components prevVoltage = Voltage; hasPower = Voltage > MinVoltage; - Force = MathHelper.Lerp(force, (Voltage < MinVoltage) ? 0.0f : targetForce, 0.1f); + Force = MathHelper.Lerp(force, (Voltage < MinVoltage) ? 0.0f : targetForce, deltaTime * 10.0f); if (Math.Abs(Force) > 1.0f) { float voltageFactor = MinVoltage <= 0.0f ? 1.0f : Math.Min(Voltage, 1.0f); @@ -137,18 +137,19 @@ namespace Barotrauma.Items.Components currForce *= MathHelper.Lerp(0.5f, 2.0f, condition); if (item.Submarine.FlippedX) { currForce *= -1; } Vector2 forceVector = new Vector2(currForce, 0); - item.Submarine.ApplyForce(forceVector); + item.Submarine.ApplyForce(forceVector * deltaTime * Timing.FixedUpdateRate); UpdatePropellerDamage(deltaTime); #if CLIENT - particleTimer -= deltaTime; - if (particleTimer <= 0.0f) + float particleInterval = 1.0f / particlesPerSec; + particleTimer += deltaTime; + while (particleTimer > particleInterval) { Vector2 particleVel = -forceVector.ClampLength(5000.0f) / 5.0f; GameMain.ParticleManager.CreateParticle("bubbles", item.WorldPosition + PropellerPos * item.Scale, - particleVel * Rand.Range(0.9f, 1.1f), + particleVel * Rand.Range(0.8f, 1.1f), 0.0f, item.CurrentHull); - particleTimer = 1.0f / particlesPerSec; - } + particleTimer -= particleInterval; + } #endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index c29086343..43dfd81d0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -179,8 +179,6 @@ namespace Barotrauma.Items.Components if (selectedItem == null) { return; } if (!outputContainer.Inventory.CanBePut(selectedItem.TargetItem, selectedItem.OutCondition * selectedItem.TargetItem.Health)) { return; } - RefreshAvailableIngredients(); - #if CLIENT itemList.Enabled = false; activateButton.Text = TextManager.Get("FabricatorCancel"); @@ -189,7 +187,13 @@ namespace Barotrauma.Items.Components IsActive = true; this.user = user; fabricatedItem = selectedItem; - MoveIngredientsToInputContainer(selectedItem); + RefreshAvailableIngredients(); + + bool isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; + if (!isClient) + { + MoveIngredientsToInputContainer(selectedItem); + } requiredTime = GetRequiredTime(fabricatedItem, user); timeUntilReady = requiredTime; @@ -230,21 +234,19 @@ namespace Barotrauma.Items.Components } if (fabricatedItem == null) { return; } - fabricatedItem = null; - -#if CLIENT +#if SERVER + if (user != null) + { + GameServer.Log(GameServer.CharacterLogName(user) + " cancelled the fabrication of " + fabricatedItem.DisplayName + " in " + item.Name, ServerLog.MessageType.ItemInteraction); + } +#elif CLIENT itemList.Enabled = true; if (activateButton != null) { activateButton.Text = TextManager.Get("FabricatorCreate"); } #endif -#if SERVER - if (user != null) - { - GameServer.Log(GameServer.CharacterLogName(user) + " cancelled the fabrication of " + fabricatedItem.DisplayName + " in " + item.Name, ServerLog.MessageType.ItemInteraction); - } -#endif + fabricatedItem = null; } public override void Update(float deltaTime, Camera cam) @@ -256,15 +258,20 @@ namespace Barotrauma.Items.Components } refreshIngredientsTimer -= deltaTime; - if (fabricatedItem == null || !CanBeFabricated(fabricatedItem, availableIngredients, user)) + bool isClient = GameMain.NetworkMember?.IsClient ?? false; + + if (!isClient) { - CancelFabricating(); - return; + if (fabricatedItem == null || !CanBeFabricated(fabricatedItem, availableIngredients, user)) + { + CancelFabricating(); + return; + } } progressState = fabricatedItem == null ? 0.0f : (requiredTime - timeUntilReady) / requiredTime; - if (GameMain.NetworkMember?.IsClient ?? false) + if (isClient) { hasPower = State != FabricatorState.Paused; if (!hasPower) @@ -365,30 +372,32 @@ namespace Barotrauma.Items.Components availableItems.Remove(availableItem); Entity.Spawner.AddToRemoveQueue(availableItem); inputContainer.Inventory.RemoveItem(availableItem); + break; } } }); int amountFittingContainer = outputContainer.Inventory.HowManyCanBePut(fabricatedItem.TargetItem, fabricatedItem.OutCondition * fabricatedItem.TargetItem.Health); - var fabricationValueItem = new AbilityValueItem(fabricatedItem.Amount, fabricatedItem.TargetItem); + var fabricationitemAmount = new AbilityFabricationItemAmount(fabricatedItem.TargetItem, fabricatedItem.Amount); int quality = 0; if (user?.Info != null) { foreach (Character character in Character.GetFriendlyCrew(user)) { - character.CheckTalents(AbilityEffectType.OnAllyItemFabricatedAmount, fabricationValueItem); + character.CheckTalents(AbilityEffectType.OnAllyItemFabricatedAmount, fabricationitemAmount); } - user.CheckTalents(AbilityEffectType.OnItemFabricatedAmount, fabricationValueItem); + user.CheckTalents(AbilityEffectType.OnItemFabricatedAmount, fabricationitemAmount); quality = GetFabricatedItemQuality(fabricatedItem, user); } var tempUser = user; - for (int i = 0; i < (int)fabricationValueItem.Value; i++) + for (int i = 0; i < (int)fabricationitemAmount.Value; i++) { float outCondition = fabricatedItem.OutCondition; + GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none") + ":" + fabricatedItem.TargetItem.Identifier); if (i < amountFittingContainer) { Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, outputContainer.Inventory, fabricatedItem.TargetItem.Health * outCondition, quality, @@ -433,7 +442,7 @@ namespace Barotrauma.Items.Components { float userSkill = user.GetSkillLevel(skill.Identifier); float addedSkill = skill.Level * SkillSettings.Current.SkillIncreasePerFabricatorRequiredSkill / Math.Max(userSkill, 1.0f); - var addedSkillValue = new AbilityValueString(addedSkill, skill.Identifier); + var addedSkillValue = new AbilityFabricatorSkillGain(skill.Identifier, addedSkill); user.CheckTalents(AbilityEffectType.OnItemFabricationSkillGain, addedSkillValue); user.Info.IncreaseSkillLevel( @@ -542,6 +551,11 @@ namespace Barotrauma.Items.Components private void RefreshAvailableIngredients() { + Character user = this.user; +#if CLIENT + user ??= Character.Controlled; +#endif + List itemList = new List(); itemList.AddRange(inputContainer.Inventory.AllItems); foreach (MapEntity linkedTo in item.linkedTo) @@ -550,6 +564,10 @@ namespace Barotrauma.Items.Components { var itemContainer = linkedItem.GetComponent(); if (itemContainer == null) { continue; } + if (user != null) + { + if (!itemContainer.HasRequiredItems(user, addMessage: false)) { continue; } + } var deconstructor = linkedItem.GetComponent(); if (deconstructor != null) @@ -568,17 +586,10 @@ namespace Barotrauma.Items.Components itemList.AddRange(container.Inventory.AllItems); } } -#if CLIENT - if (Character.Controlled?.Inventory != null) - { - itemList.AddRange(Character.Controlled.Inventory.AllItems); - } -#else if (user?.Inventory != null) { itemList.AddRange(user.Inventory.AllItems); } -#endif availableIngredients.Clear(); foreach (Item item in itemList) { @@ -600,8 +611,6 @@ namespace Barotrauma.Items.Components //required ingredients that are already present in the input container List usedItems = new List(); - bool isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; - targetItem.RequiredItems.ForEach(requiredItem => { for (int i = 0; i < requiredItem.Amount; i++) { @@ -630,10 +639,11 @@ namespace Barotrauma.Items.Components if (!inputContainer.Inventory.CanBePut(availablePrefab)) { var unneededItem = inputContainer.Inventory.AllItems.FirstOrDefault(it => !usedItems.Contains(it)); - unneededItem?.Drop(null, createNetworkEvent: !isClient); + unneededItem?.Drop(null); } - inputContainer.Inventory.TryPutItem(availablePrefab, user: null, createNetworkEvent: !isClient); + inputContainer.Inventory.TryPutItem(availablePrefab, user: null); } + break; } } }); @@ -684,5 +694,26 @@ namespace Barotrauma.Items.Components } savedFabricatedItem = null; } + class AbilityFabricatorSkillGain : AbilityObject, IAbilityValue, IAbilitySkillIdentifier + { + public AbilityFabricatorSkillGain(string skillIdentifier, float skillAmount) + { + SkillIdentifier = skillIdentifier; + Value = skillAmount; + } + public float Value { get; set; } + public string SkillIdentifier { get; set; } + } + + class AbilityFabricationItemAmount : AbilityObject, IAbilityValue, IAbilityItemPrefab + { + public AbilityFabricationItemAmount(ItemPrefab itemPrefab, float itemAmount) + { + ItemPrefab = itemPrefab; + Value = itemAmount; + } + public float Value { get; set; } + public ItemPrefab ItemPrefab { get; set; } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index 9317f68b9..b7cb7b076 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -29,6 +29,15 @@ namespace Barotrauma.Items.Components } } + public float CurrentBrokenVolume + { + get + { + if (item.ConditionPercentage > 10.0f || !IsActive) { return 0.0f; } + return (1.0f - item.ConditionPercentage / 10.0f) * 100.0f; + } + } + private float pumpSpeedLockTimer, isActiveLockTimer; [Serialize(0.0f, true, description: "How fast the item is currently pumping water (-100 = full speed out, 100 = full speed in). Intended to be used by StatusEffect conditionals (setting this value in XML has no effect).")] @@ -72,6 +81,8 @@ namespace Barotrauma.Items.Components private const float TinkeringSpeedIncrease = 4.0f; + public override bool UpdateWhenInactive => true; + public Pump(Item item, XElement element) : base(item, element) { @@ -82,13 +93,30 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { + pumpSpeedLockTimer -= deltaTime; + isActiveLockTimer -= deltaTime; + + if (!IsActive) { return; } + currFlow = 0.0f; if (TargetLevel != null) { - pumpSpeedLockTimer -= deltaTime; float hullPercentage = 0.0f; - if (item.CurrentHull != null) { hullPercentage = (item.CurrentHull.WaterVolume / item.CurrentHull.Volume) * 100.0f; } + if (item.CurrentHull != null) + { + float hullWaterVolume = item.CurrentHull.WaterVolume; + float totalHullVolume = item.CurrentHull.Volume; + foreach (var linked in item.CurrentHull.linkedTo) + { + if ((linked is Hull linkedHull)) + { + hullWaterVolume += linkedHull.WaterVolume; + totalHullVolume += linkedHull.Volume; + } + } + hullPercentage = hullWaterVolume / totalHullVolume * 100.0f; + } FlowPercentage = ((float)TargetLevel - hullPercentage) * 10.0f; } @@ -116,8 +144,8 @@ namespace Barotrauma.Items.Components //less effective when in a bad condition currFlow *= MathHelper.Lerp(0.5f, 1.0f, item.Condition / item.MaxCondition); - item.CurrentHull.WaterVolume += currFlow; - if (item.CurrentHull.WaterVolume > item.CurrentHull.Volume) { item.CurrentHull.Pressure += 0.5f; } + item.CurrentHull.WaterVolume += currFlow * deltaTime * Timing.FixedUpdateRate; + if (item.CurrentHull.WaterVolume > item.CurrentHull.Volume) { item.CurrentHull.Pressure += 30.0f * deltaTime; } Voltage -= deltaTime; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs index 91d3d3b5f..02028708a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs @@ -76,7 +76,7 @@ namespace Barotrauma.Items.Components { if (lastUser == value) { return; } lastUser = value; - degreeOfSuccess = lastUser == null ? 0.0f : DegreeOfSuccess(lastUser); + degreeOfSuccess = lastUser == null ? 0.0f : Math.Min(DegreeOfSuccess(lastUser), 1.0f); LastUserWasPlayer = lastUser.IsPlayer; } } @@ -601,7 +601,7 @@ namespace Barotrauma.Items.Components if (!shutDown) { - float degreeOfSuccess = DegreeOfSuccess(character); + float degreeOfSuccess = Math.Min(DegreeOfSuccess(character), 1.0f); float refuelLimit = 0.3f; //characters with insufficient skill levels don't refuel the reactor if (degreeOfSuccess > refuelLimit) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs index c0a64ef76..ec4fd008e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs @@ -106,6 +106,13 @@ namespace Barotrauma.Items.Components set; } + [Editable, Serialize(false, false, description: "Should the sonar view be centered on the transducers or the submarine's center of mass. Only has an effect if UseTransducers is enabled.")] + public bool CenterOnTransducers + { + get; + set; + } + [Editable, Serialize(false, false, description: "Does the sonar have mineral scanning mode. " + "Only available in-game when the Item has no Steering component.")] public bool HasMineralScanner { get; set; } @@ -307,26 +314,6 @@ namespace Barotrauma.Items.Components return TextManager.GetWithVariable("roomname.subdiroclock", "[dir]", clockDir.ToString()); } - private Vector2 GetTransducerPos() - { - if (!UseTransducers || connectedTransducers.Count == 0) - { - //use the position of the sub if the item is static (no body) and inside a sub - return item.Submarine != null && item.body == null ? item.Submarine.WorldPosition : item.WorldPosition; - } - - Vector2 transducerPosSum = Vector2.Zero; - foreach (ConnectedTransducer transducer in connectedTransducers) - { - if (transducer.Transducer.Item.Submarine != null) - { - return transducer.Transducer.Item.Submarine.WorldPosition; - } - transducerPosSum += transducer.Transducer.Item.WorldPosition; - } - return transducerPosSum / connectedTransducers.Count; - } - public override void ReceiveSignal(Signal signal, Connection connection) { base.ReceiveSignal(signal, connection); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index 44e6461b3..e56dcf7e9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -311,7 +311,7 @@ namespace Barotrauma.Items.Components } // override autopilot pathing while the AI rams, and go full speed ahead - if (AIRamTimer > 0f) + if (AIRamTimer > 0f && controlledSub != null) { AIRamTimer -= deltaTime; TargetVelocity = GetSteeringVelocity(AITacticalTarget, 0f); @@ -370,8 +370,22 @@ namespace Barotrauma.Items.Components item.SendSignal(new Signal((ConvertUnits.ToDisplayUnits(sub.Velocity.X * Physics.DisplayToRealWorldRatio) * 3.6f).ToString("0.0000", CultureInfo.InvariantCulture), sender: user), "current_velocity_x"); item.SendSignal(new Signal((ConvertUnits.ToDisplayUnits(sub.Velocity.Y * Physics.DisplayToRealWorldRatio) * -3.6f).ToString("0.0000", CultureInfo.InvariantCulture), sender: user), "current_velocity_y"); - item.SendSignal(new Signal((sub.WorldPosition.X * Physics.DisplayToRealWorldRatio).ToString("0.0000", CultureInfo.InvariantCulture), sender: user), "current_position_x"); - item.SendSignal(new Signal(sub.RealWorldDepth.ToString("0.0000", CultureInfo.InvariantCulture), sender: user), "current_position_y"); + Vector2 pos = new Vector2(sub.WorldPosition.X * Physics.DisplayToRealWorldRatio, sub.RealWorldDepth); + if (sonar != null && sonar.UseTransducers && sonar.CenterOnTransducers && sonar.ConnectedTransducers.Any()) + { + pos = Vector2.Zero; + foreach (var connectedTransducer in sonar.ConnectedTransducers) + { + pos += connectedTransducer.Item.WorldPosition; + } + pos /= sonar.ConnectedTransducers.Count(); + pos = new Vector2( + pos.X * Physics.DisplayToRealWorldRatio, + Level.Loaded?.GetRealWorldDepth(pos.Y) ?? (-pos.Y * Physics.DisplayToRealWorldRatio)); + } + + item.SendSignal(new Signal(pos.X.ToString("0.0000", CultureInfo.InvariantCulture), sender: user), "current_position_x"); + item.SendSignal(new Signal(pos.Y.ToString("0.0000", CultureInfo.InvariantCulture), sender: user), "current_position_y"); } // if our tactical AI pilot has left, revert back to maintaining position diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs index 3c999c05c..576ab086d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs @@ -1,7 +1,6 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; -using System.Collections.Generic; using System.Globalization; using System.Xml.Linq; @@ -111,6 +110,20 @@ namespace Barotrauma.Items.Components } } + [Serialize(false, true, description: "If true, the recharge speed (and power consumption) of the device goes up exponentially as the recharge rate is increased.")] + public bool ExponentialRechargeSpeed { get; set; } + + [Editable(minValue: 0.0f, maxValue: 10.0f, decimals: 2), Serialize(0.5f, true)] + public float RechargeAdjustSpeed { get; set; } + + private float efficiency; + [Editable(minValue: 0.0f, maxValue: 1.0f, decimals: 2), Serialize(0.95f, true, description: "The amount of power you can get out of a item relative to the amount of power that's put into it.")] + public float Efficiency + { + get { return efficiency; } + set { efficiency = MathHelper.Clamp(value, 0.0f, 1.0f); } + } + public float RechargeRatio => RechargeSpeed / MaxRechargeSpeed; public const float aiRechargeTargetRatio = 0.5f; @@ -170,7 +183,7 @@ namespace Barotrauma.Items.Components { ApplyStatusEffects(ActionType.OnActive, deltaTime, null); } - + if (charge >= capacity) { //rechargeVoltage = 0.0f; @@ -181,13 +194,24 @@ namespace Barotrauma.Items.Components { float missingCharge = capacity - charge; float targetRechargeSpeed = rechargeSpeed; + if (ExponentialRechargeSpeed) + { + targetRechargeSpeed = MathF.Pow(rechargeSpeed / maxRechargeSpeed, 2) * maxRechargeSpeed; + } if (missingCharge < 1.0f) { targetRechargeSpeed *= missingCharge; } - currPowerConsumption = MathHelper.Lerp(currPowerConsumption, targetRechargeSpeed, 0.05f); - Charge += currPowerConsumption * Math.Min(Voltage, 1.0f) / 3600.0f; - } + if (currPowerConsumption < targetRechargeSpeed) + { + currPowerConsumption = Math.Min(currPowerConsumption + deltaTime * maxRechargeSpeed * RechargeAdjustSpeed, targetRechargeSpeed); + } + else + { + currPowerConsumption = Math.Max(currPowerConsumption - deltaTime * maxRechargeSpeed * RechargeAdjustSpeed, targetRechargeSpeed); + } + Charge += currPowerConsumption * Math.Min(Voltage, 1.0f) / 3600.0f * efficiency; + } if (charge <= 0.0f) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs index 52c065e75..33f934b24 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs @@ -10,6 +10,8 @@ namespace Barotrauma.Items.Components { public List PowerConnections { get; private set; } + private readonly HashSet signalConnections = new HashSet(); + private readonly Dictionary connectionDirty = new Dictionary(); //a list of connections a given connection is connected to, either directly or via other power transfer components @@ -121,6 +123,7 @@ namespace Barotrauma.Items.Components partial void InitProjectSpecific(XElement element); + private static readonly HashSet recipientsToRefresh = new HashSet(); public override void UpdateBroken(float deltaTime, Camera cam) { base.UpdateBroken(deltaTime, cam); @@ -132,7 +135,8 @@ namespace Barotrauma.Items.Components powerLoad = 0.0f; currPowerConsumption = 0.0f; SetAllConnectionsDirty(); - foreach (HashSet recipientList in connectedRecipients.Values.ToList()) + recipientsToRefresh.Clear(); + foreach (HashSet recipientList in connectedRecipients.Values) { foreach (Connection c in recipientList) { @@ -140,16 +144,26 @@ namespace Barotrauma.Items.Components var recipientPowerTransfer = c.Item.GetComponent(); if (recipientPowerTransfer != null) { - recipientPowerTransfer.SetAllConnectionsDirty(); - recipientPowerTransfer.RefreshConnections(); + recipientsToRefresh.Add(recipientPowerTransfer); } } } + foreach (PowerTransfer recipientPowerTransfer in recipientsToRefresh) + { + recipientPowerTransfer.SetAllConnectionsDirty(); + recipientPowerTransfer.RefreshConnections(); + } RefreshConnections(); isBroken = true; } } + + private int prevSentPowerValue; + private string powerSignal; + private int prevSentLoadValue; + private string loadSignal; + public override void Update(float deltaTime, Camera cam) { RefreshConnections(); @@ -172,6 +186,19 @@ namespace Barotrauma.Items.Components //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) + { + prevSentPowerValue = (int)Math.Round(-CurrPowerConsumption); + powerSignal = prevSentPowerValue.ToString(); + } + if (prevSentLoadValue != (int)powerLoad || loadSignal == null) + { + prevSentLoadValue = (int)Math.Round(powerLoad); + loadSignal = prevSentLoadValue.ToString(); + } + item.SendSignal(powerSignal, "power_value_out"); + item.SendSignal(loadSignal, "load_value_out"); + float maxOverVoltage = Math.Max(OverloadVoltage, 1.0f); Overload = -currPowerConsumption > Math.Max(powerLoad, 200.0f) * maxOverVoltage; if (Overload && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)) @@ -229,46 +256,54 @@ namespace Barotrauma.Items.Components else if (!connectionDirty[c]) { continue; - } + } //find all connections that are connected to this one (directly or via another PowerTransfer) - HashSet connected = new HashSet(); + HashSet tempConnected; + if (!connectedRecipients.ContainsKey(c)) + { + tempConnected = new HashSet(); + connectedRecipients.Add(c, tempConnected); + } + else + { + tempConnected = connectedRecipients[c]; + tempConnected.Clear(); + //mark all previous recipients as dirty + foreach (Connection recipient in tempConnected) + { + var pt = recipient.Item.GetComponent(); + if (pt != null) { pt.connectionDirty[recipient] = true; } + } + } + + tempConnected.Add(c); if (item.Condition > 0.0f) { - if (!connectedRecipients.ContainsKey(c)) + GetConnected(c, tempConnected); + //go through all the PowerTransfers that we're connected to and set their connections to match the ones we just calculated + //(no need to go through the recursive GetConnected method again) + foreach (Connection recipient in tempConnected) { - connectedRecipients.Add(c, connected); - } - else - { - //mark all previous recipients as dirty - foreach (Connection recipient in connectedRecipients[c]) + if (recipient == c) { continue; } + var recipientPowerTransfer = recipient.Item.GetComponent(); + if (recipientPowerTransfer == null) { continue; } + if (!recipientPowerTransfer.connectedRecipients.ContainsKey(recipient)) { - var pt = recipient.Item.GetComponent(); - if (pt != null) pt.connectionDirty[recipient] = true; + recipientPowerTransfer.connectedRecipients.Add(recipient, new HashSet()); } + else + { + recipientPowerTransfer.connectedRecipients[recipient].Clear(); + } + foreach (var connection in tempConnected) + { + recipientPowerTransfer.connectedRecipients[recipient].Add(connection); + } + recipientPowerTransfer.connectionDirty[recipient] = false; } - - connected.Add(c); - GetConnected(c, connected); - } - connectedRecipients[c] = connected; - - //go through all the PowerTransfers that we're connected to and set their connections to match the ones we just calculated - //(no need to go through the recursive GetConnected method again) - foreach (Connection recipient in connected) - { - var recipientPowerTransfer = recipient.Item.GetComponent(); - if (recipientPowerTransfer == null) continue; - - if (!connectedRecipients.ContainsKey(recipient)) - { - connectedRecipients.Add(recipient, connected); - } - - recipientPowerTransfer.connectedRecipients[recipient] = connected; - recipientPowerTransfer.connectionDirty[recipient] = false; } + connectionDirty[c] = false; } } @@ -296,7 +331,7 @@ namespace Barotrauma.Items.Components public void SetAllConnectionsDirty() { - if (item.Connections == null) return; + if (item.Connections == null) { return; } foreach (Connection c in item.Connections) { connectionDirty[c] = true; @@ -321,6 +356,14 @@ namespace Barotrauma.Items.Components return; } + foreach (Connection c in connections) + { + if (c.Name.Length > 5 && c.Name.Substring(0, 6) == "signal") + { + signalConnections.Add(c); + } + } + if (!(this is RelayComponent)) { if (PowerConnections.Any(p => !p.IsOutput) && PowerConnections.Any(p => p.IsOutput)) @@ -356,29 +399,30 @@ namespace Barotrauma.Items.Components { if (item.Condition <= 0.0f || connection.IsPower) { return; } if (!connectedRecipients.ContainsKey(connection)) { return; } + if (!signalConnections.Contains(connection)) { return; } - if (connection.Name.Length > 5 && connection.Name.Substring(0, 6) == "signal") + foreach (Connection recipient in connectedRecipients[connection]) { - foreach (Connection recipient in connectedRecipients[connection]) + if (recipient.Item == item || recipient.Item == signal.source) { continue; } + + signal.source?.LastSentSignalRecipients.Add(recipient); + + foreach (ItemComponent ic in recipient.Item.Components) { - if (recipient.Item == item || recipient.Item == signal.source) { continue; } - - signal.source?.LastSentSignalRecipients.Add(recipient); - - foreach (ItemComponent ic in recipient.Item.Components) - { - //other junction boxes don't need to receive the signal in the pass-through signal connections - //because we relay it straight to the connected items without going through the whole chain of junction boxes - if (ic is PowerTransfer && !(ic is RelayComponent)) { continue; } - ic.ReceiveSignal(signal, recipient); - } + //other junction boxes don't need to receive the signal in the pass-through signal connections + //because we relay it straight to the connected items without going through the whole chain of junction boxes + if (ic is PowerTransfer && !(ic is RelayComponent)) { continue; } + ic.ReceiveSignal(signal, recipient); + } + if (recipient.Effects != null && signal.value != "0" && !string.IsNullOrEmpty(signal.value)) + { foreach (StatusEffect effect in recipient.Effects) { recipient.Item.ApplyStatusEffect(effect, ActionType.OnUse, 1.0f); } } - } + } } protected override void RemoveComponentSpecific() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs index 1db23da24..07e8ab3f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs @@ -272,7 +272,7 @@ namespace Barotrauma.Items.Components powered.voltage = -pt1.CurrPowerConsumption / Math.Max(pt1.PowerLoad, 1.0f); continue; } - if (powered.powerConsumption <= 0.0f && !(powered is PowerContainer)) + if ((powered.powerConsumption <= 0.0f || (powered.Item.GetComponent() is Repairable repairable && repairable.IsTinkering && repairable.TinkeringPowersDevices)) && !(powered is PowerContainer)) { powered.voltage = 1.0f; continue; @@ -302,17 +302,24 @@ namespace Barotrauma.Items.Components /// /// Returns the amount of power that can be supplied by batteries directly connected to the item /// - protected float GetAvailableBatteryPower() + protected float GetAvailableInstantaneousBatteryPower() { - var batteries = item.GetConnectedComponents(); - + if (item.Connections == null) { return 0.0f; } float availablePower = 0.0f; - foreach (PowerContainer battery in batteries) + foreach (Connection c in item.Connections) { - float batteryPower = Math.Min(battery.Charge * 3600.0f, battery.MaxOutPut); - availablePower += batteryPower; - } + var recipients = c.Recipients; + foreach (Connection recipient in recipients) + { + if (!recipient.IsPower || !recipient.IsOutput) { continue; } + var battery = recipient.Item?.GetComponent(); + if (battery == null) { continue; } + float maxOutputPerFrame = battery.MaxOutPut / 60.0f; + float framesPerMinute = 3600.0f; + availablePower += Math.Min(battery.Charge * framesPerMinute, maxOutputPerFrame); + } + } return availablePower; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index 9b1c13af8..fb4d7bbd1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -196,6 +196,15 @@ namespace Barotrauma.Items.Components set; } + private float deactivationTimer; + + [Serialize(0f, false)] + public float DeactivationTime + { + get; + set; + } + public Body StickTarget { get; @@ -207,6 +216,9 @@ namespace Barotrauma.Items.Components get { return StickTarget != null; } } + private Category originalCollisionCategories; + private Category originalCollisionTargets; + public Projectile(Item item, XElement element) : base (item, element) { @@ -223,21 +235,26 @@ namespace Barotrauma.Items.Components public override void OnItemLoaded() { - if (Attack != null && Attack.DamageRange <= 0.0f && item.body != null) + if (item.body != null) { - switch (item.body.BodyShape) + if (Attack != null && Attack.DamageRange <= 0.0f) { - case PhysicsBody.Shape.Circle: - Attack.DamageRange = item.body.radius; - break; - case PhysicsBody.Shape.Capsule: - Attack.DamageRange = item.body.height / 2 + item.body.radius; - break; - case PhysicsBody.Shape.Rectangle: - Attack.DamageRange = new Vector2(item.body.width / 2.0f, item.body.height / 2.0f).Length(); - break; + switch (item.body.BodyShape) + { + case PhysicsBody.Shape.Circle: + Attack.DamageRange = item.body.radius; + break; + case PhysicsBody.Shape.Capsule: + Attack.DamageRange = item.body.height / 2 + item.body.radius; + break; + case PhysicsBody.Shape.Rectangle: + Attack.DamageRange = new Vector2(item.body.width / 2.0f, item.body.height / 2.0f).Length(); + break; + } + Attack.DamageRange = ConvertUnits.ToDisplayUnits(Attack.DamageRange); } - Attack.DamageRange = ConvertUnits.ToDisplayUnits(Attack.DamageRange); + originalCollisionCategories = item.body.CollisionCategories; + originalCollisionTargets = item.body.CollidesWith; } } @@ -259,6 +276,10 @@ namespace Barotrauma.Items.Components launchPos = simPosition; //set the rotation of the projectile again because dropping the projectile resets the rotation Item.SetTransform(simPosition, rotation + (Item.body.Dir * LaunchRotationRadians)); + if (DeactivationTime > 0) + { + deactivationTimer = DeactivationTime; + } } public void Shoot(Character user, Vector2 weaponPos, Vector2 spawnPos, float rotation, List ignoredBodies, bool createNetworkEvent, float damageMultiplier = 1f) @@ -268,7 +289,8 @@ namespace Barotrauma.Items.Components IgnoredBodies = ignoredBodies; Vector2 projectilePos = weaponPos; //make sure there's no obstacles between the base of the weapon (or the shoulder of the character) and the end of the barrel - if (Submarine.PickBody(weaponPos, spawnPos, IgnoredBodies, Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking) == null) + if (Submarine.PickBody(weaponPos, spawnPos, IgnoredBodies, Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking, + customPredicate: (Fixture f) => { return !IgnoredBodies.Contains(f.Body); }) == null) { //no obstacles -> we can spawn the projectile at the barrel projectilePos = spawnPos; @@ -359,7 +381,7 @@ namespace Barotrauma.Items.Components item.body.FarseerBody.IsBullet = true; item.body.CollisionCategories = Physics.CollisionProjectile; - item.body.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel; + item.body.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking; IsActive = true; @@ -542,6 +564,7 @@ namespace Barotrauma.Items.Components { if (fixture.Body.UserData is VoronoiCell) { return -1; } if (fixture.Body.UserData is Entity entity && entity.Submarine != submarine) { return -1; } + if (fixture.Body.UserData is Limb limb && limb.character?.Submarine != submarine) { return -1; } } //ignore level cells if the item and the point of impact are inside a sub @@ -583,7 +606,7 @@ namespace Barotrauma.Items.Components { if (dropper != null) { - Deactivate(); + DisableProjectileCollisions(); Unstick(); } base.Drop(dropper); @@ -591,6 +614,14 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { + if (DeactivationTime > 0) + { + deactivationTimer -= deltaTime; + if (deactivationTimer < 0) + { + DisableProjectileCollisions(); + } + } while (impactQueue.Count > 0) { var impact = impactQueue.Dequeue(); @@ -612,8 +643,12 @@ namespace Barotrauma.Items.Components } //projectiles with a stickjoint don't become inactive until the stickjoint is detached if (stickJoint == null && !item.body.FarseerBody.IsBullet) - { - IsActive = false; + { + IsActive = false; + if (DeactivationTime > 0 && deactivationTimer > 0) + { + DisableProjectileCollisions(); + } } if (stickJoint == null) { return; } @@ -713,7 +748,7 @@ namespace Barotrauma.Items.Components } if (hits.Count() >= MaxTargetsToHit || target.Body.UserData is VoronoiCell) { - Deactivate(); + DisableProjectileCollisions(); return true; } else @@ -736,8 +771,9 @@ namespace Barotrauma.Items.Components } lastTarget = target; - float projectileNewSpeed = 0.5f; - float projectileDeflectedNewSpeed = 0.1f; + int remainingHits = Math.Max(MaxTargetsToHit - hits.Count, 0); + float speedMultiplier = Math.Min(0.4f + remainingHits * 0.1f, 1.0f); + float deflectedSpeedMultiplier = 0.1f; AttackResult attackResult = new AttackResult(); Character character = null; @@ -753,8 +789,8 @@ namespace Barotrauma.Items.Components // when hitting limbs with piercing ammo, don't lose as much speed if (MaxTargetsToHit > 1) { - projectileNewSpeed = 1f; - projectileDeflectedNewSpeed = 0.8f; + speedMultiplier = 1f; + deflectedSpeedMultiplier = 0.8f; } if (limb.IsSevered || limb.character == null || limb.character.Removed) { return false; } @@ -861,13 +897,13 @@ namespace Barotrauma.Items.Components if (hits.Count() >= MaxTargetsToHit || hits.LastOrDefault()?.UserData is VoronoiCell) { - Deactivate(); + DisableProjectileCollisions(); } if (attackResult.AppliedDamageModifiers != null && (attackResult.AppliedDamageModifiers.Any(dm => dm.DeflectProjectiles) && !StickToDeflective)) { - item.body.LinearVelocity *= projectileDeflectedNewSpeed; + item.body.LinearVelocity *= deflectedSpeedMultiplier; } else if ( // When hitting characters the collision normal seems to sometimes point into wrong direction, resulting in a failed attempt to stick //Vector2.Dot(Vector2.Normalize(velocity), collisionNormal) < 0.0f && @@ -899,13 +935,13 @@ namespace Barotrauma.Items.Components item.CreateServerEvent(this); } #endif - item.body.LinearVelocity *= projectileNewSpeed; + item.body.LinearVelocity *= speedMultiplier; return Hitscan; } else { - item.body.LinearVelocity *= projectileNewSpeed; + item.body.LinearVelocity *= speedMultiplier; } var containedItems = item.OwnInventory?.AllItems; @@ -931,18 +967,26 @@ namespace Barotrauma.Items.Components return true; } - private void Deactivate() + private void DisableProjectileCollisions() { item.body.FarseerBody.OnCollision -= OnProjectileCollision; - if ((item.Prefab.DamagedByProjectiles || item.Prefab.DamagedByMeleeWeapons) && item.Condition > 0) + if (originalCollisionCategories != Category.None && originalCollisionTargets != Category.None) { - item.body.CollisionCategories = Physics.CollisionCharacter; - item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform | Physics.CollisionProjectile; + item.body.CollisionCategories = originalCollisionCategories; + item.body.CollidesWith = originalCollisionTargets; } else { - item.body.CollisionCategories = Physics.CollisionItem; - item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel; + if ((item.Prefab.DamagedByProjectiles || item.Prefab.DamagedByMeleeWeapons) && item.Condition > 0) + { + item.body.CollisionCategories = Physics.CollisionCharacter; + item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform | Physics.CollisionProjectile; + } + else + { + item.body.CollisionCategories = Physics.CollisionItem; + item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel; + } } IgnoredBodies.Clear(); } @@ -993,7 +1037,14 @@ namespace Barotrauma.Items.Components } stickJoint = null; } - if (!item.body.FarseerBody.IsBullet) { IsActive = false; } + if (!item.body.FarseerBody.IsBullet) + { + IsActive = false; + if (DeactivationTime > 0 && deactivationTimer > 0) + { + DisableProjectileCollisions(); + } + } item.GetComponent()?.Snap(); if (stickTargetCharacter != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index 0952af1c7..2893f94f1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -16,6 +16,9 @@ namespace Barotrauma.Items.Components private float deteriorationTimer; private float deteriorateAlwaysResetTimer; + private int prevSentConditionValue; + private string conditionSignal; + bool wasBroken; bool wasGoodCondition; @@ -113,6 +116,9 @@ namespace Barotrauma.Items.Components public float TinkeringStrength => tinkeringStrength; + private bool tinkeringPowersDevices; + public bool TinkeringPowersDevices => tinkeringPowersDevices; + public bool IsBelowRepairThreshold => item.ConditionPercentage <= RepairThreshold; public bool IsBelowRepairIconThreshold => item.ConditionPercentage <= RepairThreshold / 2; @@ -266,6 +272,7 @@ namespace Barotrauma.Items.Components if (action == FixActions.Tinker) { tinkeringStrength = 1f + CurrentFixer.GetStatValue(StatTypes.TinkeringStrength); + tinkeringPowersDevices = CurrentFixer.HasAbilityFlag(AbilityFlags.TinkeringPowersDevices); if (character.HasAbilityFlag(AbilityFlags.CanTinkerFabricatorsAndDeconstructors) && item.GetComponent() != null || item.GetComponent() != null) { @@ -346,7 +353,13 @@ namespace Barotrauma.Items.Components UpdateProjSpecific(deltaTime); IsTinkering = false; - item.SendSignal($"{(int) item.ConditionPercentage}", "condition_out"); + if (prevSentConditionValue != (int)item.ConditionPercentage || conditionSignal == null) + { + prevSentConditionValue = (int)item.ConditionPercentage; + conditionSignal = prevSentConditionValue.ToString(); + } + + item.SendSignal(conditionSignal, "condition_out"); if (CurrentFixer == null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs index d54ec8b92..9ae8bede2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs @@ -12,6 +12,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]; [InGameEditable(DecimalCount = 2), Serialize(0.0f, true, description: "The item sends the output if both inputs have received a non-zero signal within the timeframe. If set to 0, the inputs must receive a signal at the same time.", alwaysUseInstanceValues: true)] public float TimeFrame @@ -80,14 +82,18 @@ namespace Barotrauma.Items.Components bool sendOutput = true; for (int i = 0; i < timeSinceReceived.Length; i++) { - if (timeSinceReceived[i] > timeFrame) sendOutput = false; + if (timeSinceReceived[i] > timeFrame) { sendOutput = false; } timeSinceReceived[i] += deltaTime; } string signalOut = sendOutput ? output : falseOutput; - if (string.IsNullOrEmpty(signalOut)) return; + if (string.IsNullOrEmpty(signalOut)) + { + IsActive = false; + return; + } - item.SendSignal(signalOut, "signal_out"); + item.SendSignal(new Signal(signalOut, sender: signalSender[0] ?? signalSender[1]), "signal_out"); } public override void ReceiveSignal(Signal signal, Connection connection) @@ -95,12 +101,16 @@ namespace Barotrauma.Items.Components switch (connection.Name) { case "signal_in1": - if (signal.value == "0") return; + if (signal.value == "0") { return; } timeSinceReceived[0] = 0.0f; + signalSender[0] = signal.sender; + IsActive = true; break; case "signal_in2": - if (signal.value == "0") return; + if (signal.value == "0") { return; } timeSinceReceived[1] = 0.0f; + signalSender[1] = signal.sender; + IsActive = true; break; case "set_output": output = signal.value; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs index 88883ccca..38489ca15 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs @@ -19,11 +19,10 @@ namespace Barotrauma.Items.Components private HashSet ActivatingItemPrefabs { get; set; } = new HashSet(); - private bool AllowUsingButtons => ActivatingItemPrefabs.None() || Container.Inventory.AllItems.Any(i => i != null && ActivatingItemPrefabs.Any(p => p == i.Prefab)); + private bool AllowUsingButtons => ActivatingItemPrefabs.None() || (Container != null && Container.Inventory.AllItems.Any(i => i != null && ActivatingItemPrefabs.Any(p => p == i.Prefab))); public ButtonTerminal(Item item, XElement element) : base(item, element) { - IsActive = true; RequiredSignalCount = element.GetChildElements("TerminalButton").Count(c => c.GetAttribute("style") != null); if (RequiredSignalCount < 1) { @@ -88,25 +87,25 @@ namespace Barotrauma.Items.Components } } - var containers = item.GetComponents().ToList(); - if (containers.Count != 1) + var containers = item.GetComponents(); + if (containers.Count() != 1) { DebugConsole.ThrowError($"Error in item \"{item.Name}\": the ButtonTerminal component requires exactly one ItemContainer component!"); return; } - Container = containers[0]; + Container = containers.FirstOrDefault(); OnItemLoadedProjSpecific(); } partial void OnItemLoadedProjSpecific(); - private bool SendSignal(int signalIndex, bool isServerMessage = false) + private bool SendSignal(int signalIndex, Character sender, bool isServerMessage = false) { if (!isServerMessage && !AllowUsingButtons) { return false; } string signal = Signals[signalIndex]; string connectionName = $"signal_out{signalIndex + 1}"; - item.SendSignal(signal, connectionName); + item.SendSignal(new Signal(signal, sender: sender), connectionName); return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConcatComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConcatComponent.cs index 4870ffd67..afcf91f2d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConcatComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConcatComponent.cs @@ -17,6 +17,12 @@ namespace Barotrauma.Items.Components } } + [Editable, Serialize("", false)] + public string Separator + { + get; + set; + } public ConcatComponent(Item item, XElement element) : base(item, element) @@ -25,7 +31,15 @@ namespace Barotrauma.Items.Components protected override string Calculate(string signal1, string signal2) { - string output = signal1 + signal2; + string output; + if (string.IsNullOrEmpty(Separator)) + { + output = signal1 + signal2; + } + else + { + output = signal1 + Separator + signal2; + } return output.Length <= maxOutputLength ? output : output.Substring(0, MaxOutputLength); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs index 90db70255..185d8a158 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs @@ -25,7 +25,7 @@ namespace Barotrauma.Items.Components get { return wires; } } - private Item item; + private readonly Item item; public readonly bool IsOutput; @@ -142,7 +142,6 @@ namespace Barotrauma.Items.Components IsPower = Name == "power_in" || Name == "power" || Name == "power_out"; - Effects = new List(); wireId = new ushort[MaxWires]; @@ -164,6 +163,7 @@ namespace Barotrauma.Items.Components break; case "statuseffect": + Effects ??= new List(); Effects.Add(StatusEffect.Load(subElement, item.Name + ", connection " + Name)); break; } @@ -272,7 +272,7 @@ namespace Barotrauma.Items.Components ic.ReceiveSignal(signal, connection); } - if (signal.value != "0") + if (recipient.Effects != null && signal.value != "0") { foreach (StatusEffect effect in recipient.Effects) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs index 79bc7f6a9..92103f633 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs @@ -24,7 +24,19 @@ namespace Barotrauma.Items.Components /// public bool AlwaysAllowRewiring { - get { return item.Submarine?.Info.Type == SubmarineType.BeaconStation; } + get + { + if (item.Submarine == null) { return true; } + switch (item.Submarine.Info.Type) + { + case SubmarineType.Wreck: + case SubmarineType.BeaconStation: + case SubmarineType.EnemySubmarine: + case SubmarineType.Ruin: + return true; + } + return false; + } } [Editable, Serialize(false, true, description: "Locked connection panels cannot be rewired in-game.", alwaysUseInstanceValues: true)] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs index 2e3e79f43..265f5a56c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs @@ -236,6 +236,13 @@ namespace Barotrauma.Items.Components { ciElement.Connection = item.Connections?.FirstOrDefault(c => c.Name == ciElement.ConnectionName); } +#if SERVER + //make sure the clients know about the states of the checkboxes and text fields + if (item.Submarine == null || !item.Submarine.Loading) + { + item.CreateServerEvent(this); + } +#endif } partial void UpdateLabelsProjSpecific(); @@ -301,7 +308,6 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - UpdateProjSpecific(); foreach (CustomInterfaceElement ciElement in customInterfaceElementList) { if (!ciElement.ContinuousSignal) { continue; } @@ -318,8 +324,6 @@ namespace Barotrauma.Items.Components } } - partial void UpdateProjSpecific(); - public override XElement Save(XElement parentElement) { labels = customInterfaceElementList.Select(ci => ci.Label).ToArray(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs index 3249973db..c0c7c2872 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Xml.Linq; using Microsoft.Xna.Framework; namespace Barotrauma.Items.Components @@ -24,7 +25,7 @@ namespace Barotrauma.Items.Components private int signalQueueSize; private int delayTicks; - private readonly Queue signalQueue; + private readonly Queue signalQueue = new Queue(); private DelayedSignal prevQueuedSignal; @@ -39,6 +40,7 @@ namespace Barotrauma.Items.Components delay = value; delayTicks = (int)(delay / Timing.Step); signalQueueSize = Math.Max(delayTicks, 1) * 2; + signalQueue.Clear(); } } @@ -59,22 +61,26 @@ namespace Barotrauma.Items.Components public DelayComponent(Item item, XElement element) : base (item, element) { - signalQueue = new Queue(); IsActive = true; } public override void Update(float deltaTime, Camera cam) { + if (signalQueue.Count == 0) + { + IsActive = false; + return; + } + foreach (var val in signalQueue) { val.SendTimer -= 1; } - while (signalQueue.Count > 0 && signalQueue.Peek().SendTimer <= 0) { var signalOut = signalQueue.Peek(); signalOut.SendDuration -= 1; - item.SendSignal(new Signal(signalOut.Signal.value, strength: signalOut.Signal.strength), "signal_out"); + item.SendSignal(new Signal(signalOut.Signal.value, sender: signalOut.Signal.sender, strength: signalOut.Signal.strength), "signal_out"); if (signalOut.SendDuration <= 0) { signalQueue.Dequeue(); @@ -113,9 +119,10 @@ namespace Barotrauma.Items.Components SendDuration = 1 }; signalQueue.Enqueue(prevQueuedSignal); + IsActive = true; break; case "set_delay": - if (float.TryParse(signal.value, out float newDelay)) + if (float.TryParse(signal.value, NumberStyles.Any, CultureInfo.InvariantCulture, out float newDelay)) { newDelay = MathHelper.Clamp(newDelay, 0, 60); if (signalQueue.Count > 0 && newDelay != Delay) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs index 19175aaf3..4ffc063f7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs @@ -12,6 +12,8 @@ namespace Barotrauma.Items.Components protected string[] receivedSignal; + private readonly Character[] signalSender = new Character[2]; + //the output is sent if both inputs have received a signal within the timeframe protected float timeFrame; @@ -90,9 +92,8 @@ namespace Barotrauma.Items.Components if (sendOutput) { string signalOut = receivedSignal[0] == receivedSignal[1] ? output : falseOutput; - if (string.IsNullOrEmpty(signalOut)) return; - - item.SendSignal(signalOut, "signal_out"); + if (string.IsNullOrEmpty(signalOut)) { return; } + item.SendSignal(new Signal(signalOut, sender: signalSender[0] ?? signalSender[1]), "signal_out"); } } @@ -103,10 +104,15 @@ namespace Barotrauma.Items.Components case "signal_in1": receivedSignal[0] = signal.value; timeSinceReceived[0] = 0.0f; + signalSender[0] = signal.sender; break; case "signal_in2": receivedSignal[1] = signal.value; timeSinceReceived[1] = 0.0f; + signalSender[1] = signal.sender; + break; + case "set_output": + output = signal.value; break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/GreaterComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/GreaterComponent.cs index fa8109691..0f15476c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/GreaterComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/GreaterComponent.cs @@ -32,10 +32,22 @@ namespace Barotrauma.Items.Components } public override void ReceiveSignal(Signal signal, Connection connection) - { - base.ReceiveSignal(signal, connection); - float.TryParse(receivedSignal[0], NumberStyles.Float, CultureInfo.InvariantCulture, out val1); - float.TryParse(receivedSignal[1], NumberStyles.Float, CultureInfo.InvariantCulture, out val2); + { + //base.ReceiveSignal(signal, connection); + switch (connection.Name) + { + case "signal_in1": + float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out val1); + timeSinceReceived[0] = 0.0f; + break; + case "signal_in2": + float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out val2); + timeSinceReceived[1] = 0.0f; + break; + case "set_output": + output = signal.value; + break; + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index 876e785e3..c11c33e5f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -26,6 +26,8 @@ namespace Barotrauma.Items.Components public PhysicsBody ParentBody; + private bool isOn; + private Turret turret; [Serialize(100.0f, true, description: "The range of the emitted light. Higher values are more performance-intensive.", alwaysUseInstanceValues: true), @@ -50,7 +52,7 @@ namespace Barotrauma.Items.Components set { rotation = value; - SetLightSourceTransform(); + SetLightSourceTransformProjSpecific(); } } @@ -85,12 +87,13 @@ namespace Barotrauma.Items.Components [Editable, Serialize(false, true, description: "Is the light currently on.", alwaysUseInstanceValues: true)] public bool IsOn { - get { return IsActive; } + get { return isOn; } set { - if (IsActive == value) { return; } + if (isOn == value && IsActive == value) { return; } - IsActive = value; + IsActive = isOn = value; + SetLightSourceState(value, value ? lightBrightness : 0.0f); OnStateChanged(); } } @@ -170,7 +173,7 @@ namespace Barotrauma.Items.Components #if CLIENT if (Light != null) { - Light.Color = IsActive ? lightColor : Color.Transparent; + Light.Color = IsOn ? lightColor : Color.Transparent; } #endif } @@ -200,9 +203,8 @@ namespace Barotrauma.Items.Components set { if (base.IsActive == value) { return; } - base.IsActive = value; - - SetLightSourceState(value, value ? lightBrightness : 0.0f); + base.IsActive = isOn = value; + SetLightSourceState(value, value ? lightBrightness : 0.0f); } } @@ -237,6 +239,23 @@ namespace Barotrauma.Items.Components turret = item.GetComponent(); } + public override void OnMapLoaded() + { + if (item.body == null && powerConsumption <= 0.0f && Parent == null && turret == null && IsOn && + (statusEffectLists == null || !statusEffectLists.ContainsKey(ActionType.OnActive)) && + (IsActiveConditionals == null || IsActiveConditionals.Count == 0)) + { + lightBrightness = 1.0f; + SetLightSourceState(true, lightBrightness); + SetLightSourceTransformProjSpecific(); + base.IsActive = false; + isOn = true; +#if CLIENT + Light.ParentSub = item.Submarine; +#endif + } + } + public override void Update(float deltaTime, Camera cam) { if (item.AiTarget != null) @@ -256,7 +275,7 @@ namespace Barotrauma.Items.Components return; } - SetLightSourceTransform(); + SetLightSourceTransformProjSpecific(); PhysicsBody body = ParentBody ?? item.body; if (body != null && !body.Enabled) @@ -338,7 +357,11 @@ namespace Barotrauma.Items.Components partial void SetLightSourceState(bool enabled, float brightness); - partial void SetLightSourceTransform(); + public void SetLightSourceTransform() + { + SetLightSourceTransformProjSpecific(); + } + partial void SetLightSourceTransformProjSpecific(); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs index 14abecf89..3c4db92a5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs @@ -74,6 +74,17 @@ namespace Barotrauma.Items.Components } } + public Vector2 TransformedDetectOffset + { + get + { + Vector2 transformedDetectOffset = detectOffset; + if (item.FlippedX) { transformedDetectOffset.X = -transformedDetectOffset.X; } + if (item.FlippedY) { transformedDetectOffset.Y = -transformedDetectOffset.Y; } + return transformedDetectOffset; + } + } + [Editable(MinValueFloat = 0.1f, MaxValueFloat = 100.0f, DecimalCount = 2), Serialize(0.1f, true, description: "How often the sensor checks if there's something moving near it. Higher values are better for performance.", alwaysUseInstanceValues: true)] public float UpdateInterval { @@ -184,15 +195,15 @@ namespace Barotrauma.Items.Components } } - Vector2 detectPos = item.WorldPosition + detectOffset; + Vector2 detectPos = item.WorldPosition + TransformedDetectOffset; Rectangle detectRect = new Rectangle((int)(detectPos.X - rangeX), (int)(detectPos.Y - rangeY), (int)(rangeX * 2), (int)(rangeY * 2)); float broadRangeX = Math.Max(rangeX * 2, 500); float broadRangeY = Math.Max(rangeY * 2, 500); - if (item.CurrentHull == null && item.Submarine != null && Level.Loaded != null && + if (item.CurrentHull == null && item.Submarine != null && (Target == TargetType.Wall || Target == TargetType.Any)) { - if (Math.Abs(item.Submarine.Velocity.X) > MinimumVelocity || Math.Abs(item.Submarine.Velocity.Y) > MinimumVelocity) + if (Level.Loaded != null && (Math.Abs(item.Submarine.Velocity.X) > MinimumVelocity || Math.Abs(item.Submarine.Velocity.Y) > MinimumVelocity)) { var cells = Level.Loaded.GetCells(item.WorldPosition, 1); foreach (var cell in cells) @@ -268,7 +279,7 @@ namespace Barotrauma.Items.Components foreach (Limb limb in c.AnimController.Limbs) { if (limb.IsSevered) { continue; } - if (limb.LinearVelocity.LengthSquared() <= MinimumVelocity * MinimumVelocity) { continue; } + if (limb.LinearVelocity.LengthSquared() < MinimumVelocity * MinimumVelocity) { continue; } if (MathUtils.CircleIntersectsRectangle(limb.WorldPosition, ConvertUnits.ToDisplayUnits(limb.body.GetMaxExtent()), detectRect)) { MotionDetected = true; @@ -276,23 +287,12 @@ namespace Barotrauma.Items.Components } } } - } + } } - public override void FlipX(bool relativeToSub) - { - detectOffset.X = -detectOffset.X; - } - public override void FlipY(bool relativeToSub) - { - detectOffset.Y = -detectOffset.Y; - } public override XElement Save(XElement parentElement) { Vector2 prevDetectOffset = detectOffset; - //undo flipping before saving - if (item.FlippedX) { detectOffset.X = -detectOffset.X; } - if (item.FlippedY) { detectOffset.Y = -detectOffset.Y; } XElement element = base.Save(parentElement); detectOffset = prevDetectOffset; return element; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OrComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OrComponent.cs index d0661dc9c..abbd0b603 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OrComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OrComponent.cs @@ -15,14 +15,18 @@ namespace Barotrauma.Items.Components bool sendOutput = false; for (int i = 0; i < timeSinceReceived.Length; i++) { - if (timeSinceReceived[i] <= timeFrame) sendOutput = true; + if (timeSinceReceived[i] <= timeFrame) { sendOutput = true; } timeSinceReceived[i] += deltaTime; } string signalOut = sendOutput ? output : falseOutput; - if (string.IsNullOrEmpty(signalOut)) return; + if (string.IsNullOrEmpty(signalOut)) + { + IsActive = false; + return; + } - item.SendSignal(signalOut, "signal_out"); + item.SendSignal(new Signal(signalOut, sender: signalSender[0] ?? signalSender[1]), "signal_out"); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OxygenDetector.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OxygenDetector.cs index 4724dd56c..9cc1020e7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OxygenDetector.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OxygenDetector.cs @@ -4,6 +4,9 @@ namespace Barotrauma.Items.Components { class OxygenDetector : ItemComponent { + private int prevSentOxygenValue; + private string oxygenSignal; + public OxygenDetector(Item item, XElement element) : base (item, element) { @@ -12,9 +15,15 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - if (item.CurrentHull == null) return; + if (item.CurrentHull == null) { return; } - item.SendSignal(((int)item.CurrentHull.OxygenPercentage).ToString(), "signal_out"); + if (prevSentOxygenValue != (int)item.CurrentHull.OxygenPercentage || oxygenSignal == null) + { + prevSentOxygenValue = (int)item.CurrentHull.OxygenPercentage; + oxygenSignal = prevSentOxygenValue.ToString(); + } + + item.SendSignal(oxygenSignal, "signal_out"); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs index 629d07c37..6d78cea05 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs @@ -9,6 +9,9 @@ namespace Barotrauma.Items.Components //how often the detector can switch from state to another const float StateSwitchInterval = 1.0f; + private int prevSentWaterPercentageValue; + private string waterPercentageSignal; + private bool isInWater; private float stateSwitchDelay; @@ -106,7 +109,12 @@ namespace Barotrauma.Items.Components { waterPercentage = MathHelper.Clamp((int)Math.Ceiling(item.CurrentHull.WaterPercentage), 0, 100); } - item.SendSignal(waterPercentage.ToString(), "water_%"); + if (prevSentWaterPercentageValue != waterPercentage || waterPercentageSignal == null) + { + prevSentWaterPercentageValue = waterPercentage; + waterPercentageSignal = prevSentWaterPercentageValue.ToString(); + } + item.SendSignal(waterPercentageSignal, "water_%"); } string highPressureOut = (item.CurrentHull == null || item.CurrentHull.LethalPressure > 5.0f) ? "1" : "0"; item.SendSignal(highPressureOut, "high_pressure"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs index 4f5c1366f..706d20ba4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs @@ -8,12 +8,15 @@ using System.Xml.Linq; namespace Barotrauma.Items.Components { - partial class WifiComponent : ItemComponent + partial class WifiComponent : ItemComponent, IServerSerializable { private static readonly List list = new List(); const int ChannelMemorySize = 10; + private const int MinChannel = 0; + private const int MaxChannel = 10000; + private float range; private int channel; @@ -24,6 +27,7 @@ namespace Barotrauma.Items.Components private readonly int[] channelMemory = new int[ChannelMemorySize]; + private Connection signalInConnection; private Connection signalOutConnection; [Serialize(CharacterTeamType.None, true, description: "WiFi components can only communicate with components that have the same Team ID.", alwaysUseInstanceValues: true)] @@ -48,7 +52,7 @@ namespace Barotrauma.Items.Components get { return channel; } set { - channel = MathHelper.Clamp(value, 0, 10000); + channel = MathHelper.Clamp(value, MinChannel, MaxChannel); } } @@ -113,6 +117,7 @@ namespace Barotrauma.Items.Components if (item.Connections != null) { signalOutConnection = item.Connections.Find(c => c.Name == "signal_out"); + signalInConnection = item.Connections.Find(c => c.Name == "signal_in"); } if (channelMemory.All(m => m == 0)) { @@ -227,6 +232,18 @@ namespace Barotrauma.Items.Components if (wifiComp.signalOutConnection != null) { + if (signal.source != null && wifiComp.signalInConnection != null) + { + if (signal.source.LastSentSignalRecipients.Contains(wifiComp.signalInConnection)) + { + //signal already passed through this wifi component -> stop here to prevent an infinite loop + continue; + } + else + { + signal.source.LastSentSignalRecipients.Add(wifiComp.signalInConnection); + } + } wifiComp.item.SendSignal(s, wifiComp.signalOutConnection); } @@ -301,7 +318,14 @@ namespace Barotrauma.Items.Components case "set_channel": if (int.TryParse(signal.value, out int newChannel)) { + int prevChannel = Channel; Channel = newChannel; + if (prevChannel != Channel) + { +#if SERVER + item.CreateServerEvent(this); +#endif + } } break; case "set_range": diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs index a3dae763f..ec7a0db91 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs @@ -503,13 +503,6 @@ namespace Barotrauma.Items.Components return true; } - public override void Move(Vector2 amount) - { -#if CLIENT - if (item.IsSelected) MoveNodes(amount); -#endif - } - public List GetNodes() { return new List(nodes); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/XorComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/XorComponent.cs index 134b53130..61d782ffb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/XorComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/XorComponent.cs @@ -15,14 +15,18 @@ namespace Barotrauma.Items.Components int sendOutput = 0; for (int i = 0; i < timeSinceReceived.Length; i++) { - if (timeSinceReceived[i] <= timeFrame) sendOutput += 1; + if (timeSinceReceived[i] <= timeFrame) { sendOutput += 1; } timeSinceReceived[i] += deltaTime; } string signalOut = sendOutput == 1 ? output : falseOutput; - if (string.IsNullOrEmpty(signalOut)) return; + if (string.IsNullOrEmpty(signalOut)) + { + IsActive = false; + return; + } - item.SendSignal(signalOut, "signal_out"); + item.SendSignal(new Signal(signalOut, sender: signalSender[0] ?? signalSender[1]), "signal_out"); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs index b7ff7d8bb..a748459c7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs @@ -141,8 +141,7 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - triggerers.RemoveWhere(t => t.Removed); - LevelTrigger.RemoveDistantTriggerers(PhysicsBody, triggerers, item.WorldPosition); + LevelTrigger.RemoveInActiveTriggerers(PhysicsBody, triggerers); if (triggerOnce) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index fdaca5d18..0de5ab645 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -333,6 +333,7 @@ namespace Barotrauma.Items.Components FindLightComponent(); if (loadedRotationLimits.HasValue) { RotationLimits = loadedRotationLimits.Value; } if (loadedBaseRotation.HasValue) { BaseRotation = loadedBaseRotation.Value; } + targetRotation = rotation; UpdateTransformedBarrelPos(); } @@ -541,7 +542,7 @@ namespace Barotrauma.Items.Components public bool HasPowerToShoot() { - return GetAvailableBatteryPower() >= GetPowerRequiredToShoot(); + return GetAvailableInstantaneousBatteryPower() >= GetPowerRequiredToShoot(); } private bool TryLaunch(float deltaTime, Character character = null, bool ignorePower = false) @@ -1441,6 +1442,11 @@ namespace Barotrauma.Items.Components crosshairPointerSprite?.Remove(); crosshairPointerSprite = null; moveSoundChannel?.Dispose(); moveSoundChannel = null; WeaponIndicatorSprite?.Remove(); WeaponIndicatorSprite = null; + if (powerIndicator != null) + { + powerIndicator.RectTransform.Parent = null; + powerIndicator = null; + } #endif } @@ -1516,7 +1522,7 @@ namespace Barotrauma.Items.Components minRotation += MathHelper.TwoPi; maxRotation += MathHelper.TwoPi; } - rotation = (minRotation + maxRotation) / 2; + targetRotation = rotation = (minRotation + maxRotation) / 2; UpdateTransformedBarrelPos(); } @@ -1537,7 +1543,7 @@ namespace Barotrauma.Items.Components minRotation += MathHelper.TwoPi; maxRotation += MathHelper.TwoPi; } - rotation = (minRotation + maxRotation) / 2; + targetRotation = rotation = (minRotation + maxRotation) / 2; UpdateTransformedBarrelPos(); } @@ -1607,6 +1613,7 @@ namespace Barotrauma.Items.Components { base.OnItemLoaded(); FindLightComponent(); + targetRotation = rotation; if (!loadedBaseRotation.HasValue) { if (item.FlippedX) { FlipX(relativeToSub: false); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index 2b4190bb4..93ada4ac4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -365,7 +365,16 @@ namespace Barotrauma.Items.Components { foreach (var allowedSlot in allowedSlots) { - if (allowedSlot != InvSlotType.Any && !character.Inventory.IsInLimbSlot(item, allowedSlot)) { return; } + if (allowedSlot == InvSlotType.Any) { continue; } + foreach (Enum value in Enum.GetValues(typeof(InvSlotType))) + { + var slotType = (InvSlotType)value; + if (slotType == InvSlotType.Any || slotType == InvSlotType.None) { continue; } + if (allowedSlot.HasFlag(slotType) && !character.Inventory.IsInLimbSlot(item, slotType)) + { + return; + } + } } picker = character; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index f1806e40a..80d270cac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -479,8 +479,21 @@ namespace Barotrauma { if (i < 0 || i >= slots.Length) { - string errorMsg = "Inventory.TryPutItem failed: index was out of range(" + i + ").\n" + Environment.StackTrace.CleanupStackTrace(); + string thisItemStr = item?.prefab.Identifier ?? "null"; + string ownerStr = "null"; + if (Owner is Item ownerItem) + { + ownerStr = ownerItem.prefab.Identifier; + } + else if (Owner is Character ownerCharacter) + { + ownerStr = ownerCharacter.SpeciesName; + } + string errorMsg = $"Inventory.TryPutItem failed: index was out of range (item: {thisItemStr}, inventory: {ownerStr})."; GameAnalyticsManager.AddErrorEventOnce("Inventory.TryPutItem:IndexOutOfRange", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); +#if DEBUG + DebugConsole.ThrowError(errorMsg); +#endif return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index ee5c5c712..fd6dd2836 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -99,6 +99,14 @@ namespace Barotrauma private readonly ItemInventory ownInventory; private Rectangle defaultRect; + /// + /// Unscaled rect + /// + public Rectangle DefaultRect + { + get { return defaultRect; } + set { defaultRect = value; } + } private Dictionary connections; @@ -236,6 +244,13 @@ namespace Barotrauma /// public bool IsInteractable(Character character) { +#if CLIENT + if (Screen.Selected is EditorScreen) + { + return true; + } +#endif + if (character != null && character.IsOnPlayerTeam) { return IsPlayerTeamInteractable; @@ -263,6 +278,7 @@ namespace Barotrauma if (Screen.Selected == GameMain.SubEditorScreen) { SetContainedItemPositions(); + GetComponent()?.SetLightSourceTransform(); } #endif } @@ -733,12 +749,12 @@ namespace Barotrauma "Disclaimer: It's possible or even likely that the views block each other, if they were not designed to be viewed together!")] public bool DisplaySideBySideWhenLinked { get; set; } - public IEnumerable Repairables + public List Repairables { get { return repairables; } } - public IEnumerable Components + public List Components { get { return components; } } @@ -756,7 +772,7 @@ namespace Barotrauma get { return Position.X; } private set { - Move(new Vector2((value - Position.X) * Scale, 0.0f)); + Move(new Vector2(value * Scale, 0.0f)); } } /// @@ -767,7 +783,7 @@ namespace Barotrauma get { return Position.Y; } private set { - Move(new Vector2(0.0f, (value - Position.Y) * Scale)); + Move(new Vector2(0.0f, value * Scale)); } } @@ -783,7 +799,7 @@ namespace Barotrauma } private readonly List allPropertyObjects = new List(); - public IEnumerable AllPropertyObjects + public IReadOnlyList AllPropertyObjects { get { return allPropertyObjects; } } @@ -840,7 +856,16 @@ namespace Barotrauma switch (subElement.Name.ToString().ToLowerInvariant()) { case "body": - body = new PhysicsBody(subElement, ConvertUnits.ToSimUnits(Position), Scale); + float density = subElement.GetAttributeFloat("density", 10.0f); + float minDensity = subElement.GetAttributeFloat("mindensity", density); + float maxDensity = subElement.GetAttributeFloat("maxdensity", density); + if (minDensity < maxDensity) + { + var rand = new Random(ID); + density = MathHelper.Lerp(minDensity, maxDensity, (float)rand.NextDouble()); + } + body = new PhysicsBody(subElement, ConvertUnits.ToSimUnits(Position), Scale, density); + string collisionCategory = subElement.GetAttributeString("collisioncategory", null); if ((Prefab.DamagedByProjectiles || Prefab.DamagedByMeleeWeapons) && Condition > 0) { @@ -866,9 +891,8 @@ namespace Barotrauma } } } - - body.FarseerBody.AngularDamping = element.GetAttributeFloat("angulardamping", 0.2f); - body.FarseerBody.LinearDamping = element.GetAttributeFloat("lineardamping", 0.1f); + body.FarseerBody.AngularDamping = subElement.GetAttributeFloat("angulardamping", 0.2f); + body.FarseerBody.LinearDamping = subElement.GetAttributeFloat("lineardamping", 0.1f); body.UserData = this; break; case "trigger": @@ -1080,7 +1104,7 @@ namespace Barotrauma allPropertyObjects.Add(component); components.Add(component); - if (component.IsActive || component.Parent != null || (component.IsActiveConditionals != null && component.IsActiveConditionals.Any())) + if (component.IsActive || component.UpdateWhenInactive || component.Parent != null || (component.IsActiveConditionals != null && component.IsActiveConditionals.Any())) { updateableComponents.Add(component); } @@ -1093,7 +1117,7 @@ namespace Barotrauma #endif //component doesn't need to be updated if it isn't active, doesn't have a parent that could activate it, //nor status effects, sounds or conditionals that would need to run - if (!isActive && + if (!isActive && !component.UpdateWhenInactive && !hasSounds && component.Parent == null && (component.IsActiveConditionals == null || !component.IsActiveConditionals.Any()) && @@ -1587,7 +1611,7 @@ namespace Barotrauma float damageAmount = attack.GetItemDamage(deltaTime); Condition -= damageAmount; - if (damageAmount > 0) + if (damageAmount >= Prefab.OnDamagedThreshold) { ApplyStatusEffects(ActionType.OnDamaged, 1.0f); } @@ -1673,7 +1697,7 @@ namespace Barotrauma ic.WasUsed = false; ic.WasSecondaryUsed = false; - if (ic.IsActive) + if (ic.IsActive || ic.UpdateWhenInactive) { if (condition <= 0.0f) { @@ -1741,7 +1765,7 @@ namespace Barotrauma } else { - if (updateableComponents.Count == 0 && aiTarget == null && !hasStatusEffectsOfType[(int)ActionType.Always] && (body == null || !body.Enabled)) + if (updateableComponents.Count == 0 && !hasStatusEffectsOfType[(int)ActionType.Always] && (body == null || !body.Enabled)) { #if CLIENT positionBuffer.Clear(); @@ -1810,7 +1834,7 @@ namespace Barotrauma /// private void ApplyWaterForces() { - if (body.Mass <= 0.0f) + if (body.Mass <= 0.0f || body.Density <= 0.0f) { return; } @@ -1845,9 +1869,17 @@ namespace Barotrauma if (transformDirty) { return false; } var projectile = GetComponent(); - if (projectile?.IgnoredBodies != null) + if (projectile != null) { - if (projectile.IgnoredBodies.Contains(f2.Body)) { return false; } + //ignore character colliders (a projectile only hits limbs) + if (f2.CollisionCategories == Physics.CollisionCharacter && f2.Body.UserData is Character) + { + return false; + } + if (projectile.IgnoredBodies != null) + { + if (projectile.IgnoredBodies.Contains(f2.Body)) { return false; } + } } contact.GetWorldManifold(out Vector2 normal, out _); @@ -1971,7 +2003,7 @@ namespace Barotrauma return connectedComponents; } - private void GetConnectedComponentsRecursive(HashSet alreadySearched, List connectedComponents) where T : ItemComponent + private void GetConnectedComponentsRecursive(HashSet alreadySearched, List connectedComponents, bool ignoreInactiveRelays = false) where T : ItemComponent { ConnectionPanel connectionPanel = GetComponent(); if (connectionPanel == null) { return; } @@ -1980,18 +2012,18 @@ namespace Barotrauma { if (alreadySearched.Contains(c)) { continue; } alreadySearched.Add(c); - GetConnectedComponentsRecursive(c, alreadySearched, connectedComponents); + GetConnectedComponentsRecursive(c, alreadySearched, connectedComponents, ignoreInactiveRelays); } } /// /// Note: This function generates garbage and might be a bit too heavy to be used once per frame. /// - public List GetConnectedComponentsRecursive(Connection c) where T : ItemComponent + public List GetConnectedComponentsRecursive(Connection c, bool ignoreInactiveRelays = false) where T : ItemComponent { List connectedComponents = new List(); HashSet alreadySearched = new HashSet(); - GetConnectedComponentsRecursive(c, alreadySearched, connectedComponents); + GetConnectedComponentsRecursive(c, alreadySearched, connectedComponents, ignoreInactiveRelays); return connectedComponents; } @@ -2008,7 +2040,7 @@ namespace Barotrauma ("signal_in2", "signal_out") }; - private void GetConnectedComponentsRecursive(Connection c, HashSet alreadySearched, List connectedComponents) where T : ItemComponent + private void GetConnectedComponentsRecursive(Connection c, HashSet alreadySearched, List connectedComponents, bool ignoreInactiveRelays) where T : ItemComponent { alreadySearched.Add(c); @@ -2033,12 +2065,18 @@ namespace Barotrauma foreach (Connection wifiOutput in receiverConnections) { if ((wifiOutput.IsOutput == recipient.IsOutput) || alreadySearched.Contains(wifiOutput)) { continue; } - GetConnectedComponentsRecursive(wifiOutput, alreadySearched, connectedComponents); + GetConnectedComponentsRecursive(wifiOutput, alreadySearched, connectedComponents, ignoreInactiveRelays); } } } - recipient.Item.GetConnectedComponentsRecursive(recipient, alreadySearched, connectedComponents); + recipient.Item.GetConnectedComponentsRecursive(recipient, alreadySearched, connectedComponents, ignoreInactiveRelays); + } + + if (ignoreInactiveRelays) + { + var relay = GetComponent(); + if (relay != null && !relay.IsOn) { return; } } foreach ((string input, string output) in connectionPairs) @@ -2049,7 +2087,7 @@ namespace Barotrauma if (pairedConnection != null) { if (alreadySearched.Contains(pairedConnection)) { continue; } - GetConnectedComponentsRecursive(pairedConnection, alreadySearched, connectedComponents); + GetConnectedComponentsRecursive(pairedConnection, alreadySearched, connectedComponents, ignoreInactiveRelays); } } else if (output == c.Name) @@ -2058,7 +2096,7 @@ namespace Barotrauma if (pairedConnection != null) { if (alreadySearched.Contains(pairedConnection)) { continue; } - GetConnectedComponentsRecursive(pairedConnection, alreadySearched, connectedComponents); + GetConnectedComponentsRecursive(pairedConnection, alreadySearched, connectedComponents, ignoreInactiveRelays); } } } @@ -2114,9 +2152,14 @@ namespace Barotrauma //if the signal has been passed through this item multiple times already, interrupt it to prevent infinite loops if (signal.stepsTaken > 5 && signal.source != null) { - if (signal.source.LastSentSignalRecipients.AtLeast(3, recipient => recipient == connection)) + int duplicateRecipients = 0; + foreach (var recipient in signal.source.LastSentSignalRecipients) { - return; + if (recipient == connection) + { + duplicateRecipients++; + if (duplicateRecipients > 2) { return; } + } } } @@ -2127,7 +2170,16 @@ namespace Barotrauma //if there's an equal signal waiting to be sent //to the same connection, don't add a new one signal.stepsTaken = 0; - if (!delayedSignals.Any(s => s.Connection == connection && s.Signal.source == signal.source && s.Signal.value == signal.value && s.Signal.sender == signal.sender)) + bool duplicateFound = false; + foreach (var s in delayedSignals) + { + if (s.Connection == connection && s.Signal.source == signal.source && s.Signal.value == signal.value && s.Signal.sender == signal.sender) + { + duplicateFound = true; + break; + } + } + if (!duplicateFound) { delayedSignals.Add((signal, connection)); CoroutineManager.StartCoroutine(DelaySignal(signal, connection)); @@ -2135,16 +2187,17 @@ namespace Barotrauma } else { - foreach (StatusEffect effect in connection.Effects) + if (connection.Effects != null && signal.value != "0" && !string.IsNullOrEmpty(signal.value)) { - if (condition <= 0.0f && effect.type != ActionType.OnBroken) { continue; } - if (signal.value != "0" && !string.IsNullOrEmpty(signal.value)) { ApplyStatusEffect(effect, ActionType.OnUse, (float)Timing.Step); } + foreach (StatusEffect effect in connection.Effects) + { + if (condition <= 0.0f && effect.type != ActionType.OnBroken) { continue; } + ApplyStatusEffect(effect, ActionType.OnUse, (float)Timing.Step); + } } - signal.source ??= this; connection.SendSignal(signal); } - } private IEnumerable DelaySignal(Signal signal, Connection connection) @@ -2171,7 +2224,7 @@ namespace Barotrauma foreach (Rectangle trigger in Prefab.Triggers) { transformedTrigger = TransformTrigger(trigger, true); - if (Submarine.RectContains(transformedTrigger, worldPosition)) return true; + if (Submarine.RectContains(transformedTrigger, worldPosition)) { return true; } } transformedTrigger = Rectangle.Empty; @@ -2185,7 +2238,10 @@ namespace Barotrauma public bool TryInteract(Character user, bool ignoreRequiredItems = false, bool forceSelectKey = false, bool forceUseKey = false) { - if (CampaignInteractionType != CampaignMode.InteractionType.None) { return false; } + if (CampaignMode.BlocksInteraction(CampaignInteractionType)) + { + return false; + } bool picked = false, selected = false; #if CLIENT @@ -2475,8 +2531,6 @@ namespace Barotrauma } - - if (remove) { Spawner?.AddToRemoveQueue(this); } } @@ -2866,6 +2920,14 @@ namespace Barotrauma string name = element.Attribute("name").Value; string identifier = element.GetAttributeString("identifier", ""); + if (string.IsNullOrWhiteSpace(name) && string.IsNullOrWhiteSpace(identifier)) + { + string errorMessage = "Failed to load an item (both name and identifier were null):\n"+element.ToString(); + DebugConsole.ThrowError(errorMessage); + GameAnalyticsManager.AddErrorEventOnce("Item.Load:NameAndIdentifierNull", GameAnalyticsManager.ErrorSeverity.Error, errorMessage); + return null; + } + string pendingSwap = element.GetAttributeString("pendingswap", ""); ItemPrefab appliedSwap = null; ItemPrefab oldPrefab = null; @@ -3241,6 +3303,9 @@ namespace Barotrauma foreach (ItemComponent ic in components) { ic.Remove(); +#if CLIENT + ic.GuiFrame = null; +#endif } ItemList.Remove(this); @@ -3290,4 +3355,17 @@ namespace Barotrauma } } } + class AbilityApplyTreatment : AbilityObject, IAbilityCharacter, IAbilityItem + { + public Character Character { get; set; } + public Character User { get; set; } + public Item Item { get; set; } + + public AbilityApplyTreatment(Character user, Character target, Item item) + { + Character = target; + User = user; + Item = item; + } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index dfec46927..40cfa040f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -105,6 +105,10 @@ namespace Barotrauma RequiredSkills = new List(); RequiredTime = element.GetAttributeFloat("requiredtime", 1.0f); OutCondition = element.GetAttributeFloat("outcondition", 1.0f); + if (OutCondition > 1.0f) + { + DebugConsole.AddWarning($"Error in \"{itemPrefab.Name}\"'s fabrication recipe: out condition is above 100% ({OutCondition * 100})."); + } RequiredItems = new List(); RequiresRecipe = element.GetAttributeBool("requiresrecipe", false); Amount = element.GetAttributeInt("amount", 1); @@ -500,6 +504,9 @@ namespace Barotrauma set { impactTolerance = Math.Max(value, 0.0f); } } + [Serialize(0.0f, false)] + public float OnDamagedThreshold { get; set; } + [Serialize(0.0f, false)] public float SonarSize { @@ -1256,13 +1263,18 @@ namespace Barotrauma public static ItemPrefab Find(string name, string identifier) { + if (string.IsNullOrEmpty(name) && string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("Both name and identifier cannot be null."); + } + ItemPrefab prefab; if (string.IsNullOrEmpty(identifier)) { //legacy support identifier = GenerateLegacyIdentifier(name); } - prefab = Find(p => p is ItemPrefab && p.Identifier==identifier) as ItemPrefab; + prefab = Find(p => p is ItemPrefab && p.Identifier == identifier) as ItemPrefab; //not found, see if we can find a prefab with a matching alias if (prefab == null && !string.IsNullOrEmpty(name)) @@ -1478,5 +1490,10 @@ namespace Barotrauma return newElement; } + + public override string ToString() + { + return $"{Name} (identifier: {Identifier})"; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs index 710b21946..de0f565e8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs @@ -18,8 +18,8 @@ namespace Barotrauma.MapCreatures.Behavior public readonly BallastFloraBehavior? ParentBallastFlora; public int ID = -1; - public ushort ClaimedItem; - public bool HasClaimedItem; + public Item ClaimedItem; + public int ClaimedItemId = -1; public float MaxHealth = 100f; public float Health = 100f; @@ -271,10 +271,29 @@ namespace Barotrauma.MapCreatures.Behavior { ClaimTarget(item, Branches.FirstOrDefault(b => b.ID == branchid), true); } + else + { + string errorMsg = $"Error in BallastFloraBehavior.OnMapLoaded: could not find the item claimed by the ballast flora."; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("BallastFloraBehavior.OnMapLoaded:ClaimedItemNotFound", GameAnalyticsManager.ErrorSeverity.Warning, errorMsg); + } } foreach (BallastFloraBranch branch in Branches) { + if (branch.ClaimedItemId > -1) + { + if (Entity.FindEntityByID((ushort)branch.ClaimedItemId) is Item item) + { + branch.ClaimedItem = item; + } + else + { + string errorMsg = $"Error in BallastFloraBehavior.OnMapLoaded: could not find the item claimed by a branch."; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("BallastFloraBehavior.OnMapLoaded:BranchClaimedItemNotFound", GameAnalyticsManager.ErrorSeverity.Warning, errorMsg); + } + } UpdateConnections(branch); CreateBody(branch); } @@ -335,9 +354,9 @@ namespace Barotrauma.MapCreatures.Behavior new XAttribute("sides", (int)branch.Sides), new XAttribute("blockedsides", (int)branch.BlockedSides)); - if (branch.HasClaimedItem) + if (branch.ClaimedItem != null) { - be.Add(new XAttribute("claimed", (int)branch.ClaimedItem)); + be.Add(new XAttribute("claimed", (int)(branch.ClaimedItem?.ID ?? -1))); } saveElement.Add(be); @@ -345,6 +364,13 @@ namespace Barotrauma.MapCreatures.Behavior foreach (Item target in ClaimedTargets) { + if (target.Infector == null) + { + string errorMsg = $"Error in BallastFloraBehavior.Save: claimed target \"{target.Prefab.Identifier}\" had no infector set."; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("BallastFloraBehavior.Save:InfectorNull", GameAnalyticsManager.ErrorSeverity.Warning, errorMsg); + continue; + } XElement te = new XElement("ClaimedTarget", new XAttribute("id", target.ID), new XAttribute("branchId", target.Infector.ID)); saveElement.Add(te); } @@ -352,7 +378,7 @@ namespace Barotrauma.MapCreatures.Behavior element.Add(saveElement); } - public void LoadSave(XElement element) + public void LoadSave(XElement element, IdRemap idRemap) { SerializableProperties = SerializableProperty.DeserializeProperties(this, element); Offset = element.GetAttributeVector2("offset", Vector2.Zero); @@ -361,21 +387,20 @@ namespace Barotrauma.MapCreatures.Behavior switch (subElement.Name.ToString().ToLowerInvariant()) { case "branch": - LoadBranch(subElement); + LoadBranch(subElement, idRemap); break; - case "claimedtarget": int id = subElement.GetAttributeInt("id", -1); int branchId = subElement.GetAttributeInt("branchId", -1); if (id > 0) { - tempClaimedTargets.Add(Tuple.Create((UInt16)id, branchId)); + tempClaimedTargets.Add(Tuple.Create(idRemap.GetOffsetId(id), branchId)); } break; } } - void LoadBranch(XElement branchElement) + void LoadBranch(XElement branchElement, IdRemap idRemap) { Vector2 pos = branchElement.GetAttributeVector2("pos", Vector2.Zero); bool isRoot = branchElement.GetAttributeBool("isroot", false); @@ -400,8 +425,7 @@ namespace Barotrauma.MapCreatures.Behavior if (claimedId > -1) { - newBranch.HasClaimedItem = true; - newBranch.ClaimedItem = (ushort) claimedId; + newBranch.ClaimedItemId = idRemap.GetOffsetId((ushort)claimedId); } Branches.Add(newBranch); @@ -767,8 +791,7 @@ namespace Barotrauma.MapCreatures.Behavior if (branch != null) { - branch.ClaimedItem = target.ID; - branch.HasClaimedItem = true; + branch.ClaimedItem = target; } #if SERVER @@ -794,7 +817,7 @@ namespace Barotrauma.MapCreatures.Behavior if (parent != null) { - if (otherBranch.BlockedSides.IsBitSet(connectingSide)) + if (otherBranch.BlockedSides.HasFlag(connectingSide)) { branch.BlockedSides |= oppositeSide; continue; @@ -977,7 +1000,7 @@ namespace Barotrauma.MapCreatures.Behavior if (isClient) { return; } - if (branch.HasClaimedItem) + if (branch.ClaimedItem != null) { RemoveClaim(branch.ClaimedItem); } @@ -995,41 +1018,34 @@ namespace Barotrauma.MapCreatures.Behavior #endif } - public void RemoveClaim(ushort id) + public void RemoveClaim(Item item) { - ClaimedTargets.ForEachMod(item => + if (!IgnoredTargets.ContainsKey(item)) { - if (item.ID == id) + IgnoredTargets.Add(item, 10); + } + + ClaimedTargets.Remove(item); + item.Infector = null; + + ClaimedJunctionBoxes.ForEachMod(jb => + { + if (jb.Item == item) { - if (!IgnoredTargets.ContainsKey(item)) - { - IgnoredTargets.Add(item, 10); - } - - ClaimedTargets.Remove(item); - item.Infector = null; - - ClaimedJunctionBoxes.ForEachMod(jb => - { - if (jb.Item == item) - { - ClaimedJunctionBoxes.Remove(jb); - } - }); - - ClaimedBatteries.ForEachMod(bat => - { - if (bat.Item == item) - { - ClaimedBatteries.Remove(bat); - } - }); - -#if SERVER - SendNetworkMessage(this, NetworkHeader.Infect, item.ID, false); -#endif + ClaimedJunctionBoxes.Remove(jb); } }); + + ClaimedBatteries.ForEachMod(bat => + { + if (bat.Item == item) + { + ClaimedBatteries.Remove(bat); + } + }); +#if SERVER + SendNetworkMessage(this, NetworkHeader.Infect, item.ID, false); +#endif } public void Kill() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index e91c25f7b..1c0e18576 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -68,10 +68,9 @@ namespace Barotrauma force = element.GetAttributeFloat("force", 0.0f); - abilityExplosion = element.GetAttributeBool("abilityexplosion", false); applyToSelf = element.GetAttributeBool("applytoself", true); - bool showEffects = !abilityExplosion; + bool showEffects = !element.GetAttributeBool("abilityexplosion", false) && element.GetAttributeBool("showeffects", true); sparks = element.GetAttributeBool("sparks", showEffects); shockwave = element.GetAttributeBool("shockwave", showEffects); flames = element.GetAttributeBool("flames", showEffects); @@ -151,7 +150,7 @@ namespace Barotrauma if (!MathUtils.NearlyEqual(Attack.GetStructureDamage(1.0f), 0.0f) || !MathUtils.NearlyEqual(Attack.GetLevelWallDamage(1.0f), 0.0f)) { - RangedStructureDamage(worldPosition, displayRange, Attack.GetStructureDamage(1.0f), Attack.GetLevelWallDamage(1.0f), attacker, IgnoredSubmarines); + RangedStructureDamage(worldPosition, displayRange, Attack.GetStructureDamage(1.0f), Attack.GetLevelWallDamage(1.0f), attacker, IgnoredSubmarines, Attack.EmitStructureDamageParticles); } if (BallastFloraDamage > 0.0f) @@ -388,7 +387,7 @@ namespace Barotrauma { if (damages.TryGetValue(limb, out float damage)) { - c.TrySeverLimbJoints(limb, attack.SeverLimbsProbability * distFactor, damage, allowBeheading: true); + c.TrySeverLimbJoints(limb, attack.SeverLimbsProbability * distFactor, damage, allowBeheading: true, attacker: attacker); } } } @@ -396,13 +395,15 @@ namespace Barotrauma } } + private static readonly List damagedStructureList = new List(); + private static readonly Dictionary damagedStructures = new Dictionary(); /// /// Returns a dictionary where the keys are the structures that took damage and the values are the amount of damage taken /// - public static Dictionary RangedStructureDamage(Vector2 worldPosition, float worldRange, float damage, float levelWallDamage, Character attacker = null, IEnumerable ignoredSubmarines = null) + public static Dictionary RangedStructureDamage(Vector2 worldPosition, float worldRange, float damage, float levelWallDamage, Character attacker = null, IEnumerable ignoredSubmarines = null, bool emitWallDamageParticles = true) { - List structureList = new List(); float dist = 600.0f; + damagedStructureList.Clear(); foreach (MapEntity entity in MapEntity.mapEntityList) { if (!(entity is Structure structure)) { continue; } @@ -412,19 +413,19 @@ namespace Barotrauma !structure.IsPlatform && Vector2.Distance(structure.WorldPosition, worldPosition) < dist * 3.0f) { - structureList.Add(structure); + damagedStructureList.Add(structure); } } - Dictionary damagedStructures = new Dictionary(); - foreach (Structure structure in structureList) + damagedStructures.Clear(); + foreach (Structure structure in damagedStructureList) { for (int i = 0; i < structure.SectionCount; i++) { float distFactor = 1.0f - (Vector2.Distance(structure.SectionPosition(i, true), worldPosition) / worldRange); if (distFactor <= 0.0f) { continue; } - structure.AddDamage(i, damage * distFactor, attacker); + structure.AddDamage(i, damage * distFactor, attacker, emitParticles: emitWallDamageParticles); if (damagedStructures.ContainsKey(structure)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index 348bc996d..71858ef77 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -302,7 +302,7 @@ namespace Barotrauma Hull hull2 = linkedTo.Count < 2 ? null : (Hull)linkedTo[1]; if (hull1 == hull2) { return; } - UpdateOxygen(hull1, hull2); + UpdateOxygen(hull1, hull2, deltaTime); if (linkedTo.Count == 1) { @@ -317,7 +317,7 @@ namespace Barotrauma flowForce.X = MathHelper.Clamp(flowForce.X, -MaxFlowForce, MaxFlowForce); flowForce.Y = MathHelper.Clamp(flowForce.Y, -MaxFlowForce, MaxFlowForce); - if (openedTimer > 0.0f && flowForce.Length() > lerpedFlowForce.Length()) + if (openedTimer > 0.0f && flowForce.LengthSquared() > lerpedFlowForce.LengthSquared()) { //if the gap has just been opened/created, allow it to exert a large force instantly without any smoothing lerpedFlowForce = flowForce; @@ -326,6 +326,15 @@ namespace Barotrauma { lerpedFlowForce = Vector2.Lerp(lerpedFlowForce, flowForce, deltaTime * 5.0f); } + if (FlowTargetHull != null && IsRoomToRoom) + { + var otherRoom = linkedTo[1] == FlowTargetHull ? linkedTo[0] : linkedTo[1]; + if ((otherRoom as Hull).Volume < FlowTargetHull.Volume) + { + lerpedFlowForce = Vector2.Zero; + } + } + openedTimer -= deltaTime; EmitParticles(deltaTime); @@ -345,7 +354,7 @@ namespace Barotrauma subOffset = hull2.Submarine.Position - Submarine.Position; } - if (hull1.WaterVolume <= 0.0 && hull2.WaterVolume <= 0.0) return; + if (hull1.WaterVolume <= 0.0 && hull2.WaterVolume <= 0.0) { return; } float size = IsHorizontal ? rect.Height : rect.Width; @@ -367,7 +376,7 @@ namespace Barotrauma //water flowing from the righthand room to the lefthand room if (dir == -1) { - if (!(hull2.WaterVolume > 0.0f)) return; + if (!(hull2.WaterVolume > 0.0f)) { return; } lowerSurface = hull1.Surface - hull1.WaveY[hull1.WaveY.Length - 1]; //delta = Math.Min((room2.water.pressure - room1.water.pressure) * sizeModifier, Math.Min(room2.water.Volume, room2.Volume)); //delta = Math.Min(delta, room1.Volume - room1.water.Volume + Water.MaxCompress); @@ -375,10 +384,10 @@ namespace Barotrauma flowTargetHull = hull1; //make sure not to move more than what the room contains - delta = Math.Min(((hull2.Pressure + subOffset.Y) - hull1.Pressure) * 5.0f * sizeModifier, Math.Min(hull2.WaterVolume, hull2.Volume)); + delta = Math.Min(((hull2.Pressure + subOffset.Y) - hull1.Pressure) * 300.0f * sizeModifier * deltaTime, Math.Min(hull2.WaterVolume, hull2.Volume)); //make sure not to place more water to the target room than it can hold - delta = Math.Min(delta, hull1.Volume * Hull.MaxCompress - (hull1.WaterVolume)); + delta = Math.Min(delta, hull1.Volume * Hull.MaxCompress - hull1.WaterVolume); hull1.WaterVolume += delta; hull2.WaterVolume -= delta; if (hull1.WaterVolume > hull1.Volume) @@ -386,20 +395,20 @@ namespace Barotrauma hull1.Pressure = Math.Max(hull1.Pressure, (hull1.Pressure + hull2.Pressure+subOffset.Y) / 2); } - flowForce = new Vector2(-delta, 0.0f); + flowForce = new Vector2(-delta * (float)(Timing.Step / deltaTime), 0.0f); } else if (dir == 1) { - if (!(hull1.WaterVolume > 0.0f)) return; + if (!(hull1.WaterVolume > 0.0f)) { return; } lowerSurface = hull2.Surface - hull2.WaveY[hull2.WaveY.Length - 1]; flowTargetHull = hull2; //make sure not to move more than what the room contains - delta = Math.Min((hull1.Pressure - (hull2.Pressure + subOffset.Y)) * 5.0f * sizeModifier, Math.Min(hull1.WaterVolume, hull1.Volume)); + delta = Math.Min((hull1.Pressure - (hull2.Pressure + subOffset.Y)) * 300.0f * sizeModifier * deltaTime, Math.Min(hull1.WaterVolume, hull1.Volume)); //make sure not to place more water to the target room than it can hold - delta = Math.Min(delta, hull2.Volume * Hull.MaxCompress - (hull2.WaterVolume)); + delta = Math.Min(delta, hull2.Volume * Hull.MaxCompress - hull2.WaterVolume); hull1.WaterVolume -= delta; hull2.WaterVolume += delta; if (hull2.WaterVolume > hull2.Volume) @@ -407,10 +416,10 @@ namespace Barotrauma hull2.Pressure = Math.Max(hull2.Pressure, ((hull1.Pressure-subOffset.Y) + hull2.Pressure) / 2); } - flowForce = new Vector2(delta, 0.0f); + flowForce = new Vector2(delta * (float)(Timing.Step / deltaTime), 0.0f); } - if (delta > 100.0f && subOffset == Vector2.Zero) + if (delta > 1.5f && subOffset == Vector2.Zero) { float avg = (hull1.Surface + hull2.Surface) / 2.0f; @@ -450,7 +459,7 @@ namespace Barotrauma flowForce = new Vector2( 0.0f, - Math.Min(Math.Min((hull2.Pressure + subOffset.Y) - hull1.Pressure, 200.0f), delta)); + Math.Min(Math.Min((hull2.Pressure + subOffset.Y) - hull1.Pressure, 200.0f), delta * (float)(Timing.Step / deltaTime))); flowTargetHull = hull1; @@ -478,7 +487,7 @@ namespace Barotrauma flowForce = new Vector2( hull1.WaveY[hull1.GetWaveIndex(rect.X)] - hull1.WaveY[hull1.GetWaveIndex(rect.Right)], - MathHelper.Clamp(-delta, -200.0f, 0.0f)); + MathHelper.Clamp(-delta * (float)(Timing.Step / deltaTime), -200.0f, 0.0f)); if (hull2.WaterVolume > hull2.Volume) { @@ -517,7 +526,7 @@ namespace Barotrauma delta = Math.Min(delta, hull1.Volume * Hull.MaxCompress - hull1.WaterVolume); hull1.WaterVolume += delta; - if (hull1.WaterVolume > hull1.Volume) hull1.Pressure += 0.5f; + if (hull1.WaterVolume > hull1.Volume) { hull1.Pressure += 30.0f * deltaTime; } flowTargetHull = hull1; @@ -526,12 +535,12 @@ namespace Barotrauma //water flowing from right to left if (rect.X > hull1.Rect.X + hull1.Rect.Width / 2.0f) { - flowForce = new Vector2(-delta, 0.0f); + flowForce = new Vector2(-delta * (float)(Timing.Step / deltaTime), 0.0f); } else { - flowForce = new Vector2(delta, 0.0f); + flowForce = new Vector2(delta * (float)(Timing.Step / deltaTime), 0.0f); } higherSurface = hull1.Surface; @@ -542,39 +551,39 @@ namespace Barotrauma { if (rect.X > hull1.Rect.X + hull1.Rect.Width / 2.0f) { - float vel = ((rect.Y - rect.Height / 2) - (hull1.Surface + hull1.WaveY[hull1.WaveY.Length - 1])) * 0.1f; + float vel = ((rect.Y - rect.Height / 2) - (hull1.Surface + hull1.WaveY[hull1.WaveY.Length - 1])) * 6.0f; vel *= Math.Min(Math.Abs(flowForce.X) / 200.0f, 1.0f); - hull1.WaveVel[hull1.WaveY.Length - 1] += vel; - hull1.WaveVel[hull1.WaveY.Length - 2] += vel; + hull1.WaveVel[hull1.WaveY.Length - 1] += vel * deltaTime; + hull1.WaveVel[hull1.WaveY.Length - 2] += vel * deltaTime; } else { - float vel = ((rect.Y - rect.Height / 2) - (hull1.Surface + hull1.WaveY[0])) * 0.1f; + float vel = ((rect.Y - rect.Height / 2) - (hull1.Surface + hull1.WaveY[0])) * 6.0f; vel *= Math.Min(Math.Abs(flowForce.X) / 200.0f, 1.0f); - hull1.WaveVel[0] += vel; - hull1.WaveVel[1] += vel; + hull1.WaveVel[0] += vel * deltaTime; + hull1.WaveVel[1] += vel * deltaTime; } } else { - hull1.LethalPressure += (Submarine != null && Submarine.AtDamageDepth) ? 100.0f * deltaTime : 10.0f * deltaTime; + hull1.LethalPressure += ((Submarine != null && Submarine.AtDamageDepth) ? 100.0f : 10.0f) * deltaTime; } } else { if (rect.Y > hull1.Rect.Y - hull1.Rect.Height / 2.0f) { - flowForce = new Vector2(0.0f, -delta); + flowForce = new Vector2(0.0f, -delta * (float)(Timing.Step / deltaTime)); } else { - flowForce = new Vector2(0.0f, delta); + flowForce = new Vector2(0.0f, delta * (float)(Timing.Step / deltaTime)); } if (hull1.WaterVolume >= hull1.Volume / Hull.MaxCompress) { - hull1.LethalPressure += (Submarine != null && Submarine.AtDamageDepth) ? 100.0f * deltaTime : 10.0f * deltaTime; + hull1.LethalPressure += ((Submarine != null && Submarine.AtDamageDepth) ? 100.0f : 10.0f) * deltaTime; } } } @@ -640,7 +649,7 @@ namespace Barotrauma } } - private void UpdateOxygen(Hull hull1, Hull hull2) + private void UpdateOxygen(Hull hull1, Hull hull2, float deltaTime) { if (hull1 == null || hull2 == null) { return; } @@ -656,10 +665,10 @@ namespace Barotrauma return; float totalOxygen = hull1.Oxygen + hull2.Oxygen; - float totalVolume = (hull1.Volume + hull2.Volume); + float totalVolume = hull1.Volume + hull2.Volume; float deltaOxygen = (totalOxygen * hull1.Volume / totalVolume) - hull1.Oxygen; - deltaOxygen = MathHelper.Clamp(deltaOxygen, -Hull.OxygenDistributionSpeed, Hull.OxygenDistributionSpeed); + deltaOxygen = MathHelper.Clamp(deltaOxygen, -Hull.OxygenDistributionSpeed * deltaTime, Hull.OxygenDistributionSpeed * deltaTime); hull1.Oxygen += deltaOxygen; hull2.Oxygen -= deltaOxygen; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 6993379c2..869d1fe35 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -107,7 +107,7 @@ namespace Barotrauma public static bool ShowHulls = true; public static bool EditWater, EditFire; - public const float OxygenDistributionSpeed = 500.0f; + public const float OxygenDistributionSpeed = 30000.0f; public const float OxygenDeteriorationSpeed = 0.3f; public const float OxygenConsumptionSpeed = 700.0f; @@ -132,7 +132,7 @@ namespace Barotrauma private float lethalPressure; - private float surface, drawSurface; + private float surface; private float waterVolume; private float pressure; @@ -241,7 +241,10 @@ namespace Barotrauma } OxygenPercentage = prevOxygenPercentage; - surface = drawSurface = rect.Y - rect.Height + WaterVolume / rect.Width; + surface = rect.Y - rect.Height + WaterVolume / rect.Width; +#if CLIENT + drawSurface = surface; +#endif Pressure = surface; CreateBackgroundSections(); @@ -275,17 +278,6 @@ namespace Barotrauma get { return surface; } } - public float DrawSurface - { - get { return drawSurface; } - set - { - if (Math.Abs(drawSurface - value) < 0.00001f) return; - drawSurface = MathHelper.Clamp(value, rect.Y - rect.Height, rect.Y); - update = true; - } - } - public float WorldSurface { get { return Submarine == null ? surface : surface + Submarine.Position.Y; } @@ -628,7 +620,10 @@ namespace Barotrauma Gap.UpdateHulls(); } - surface = drawSurface = rect.Y - rect.Height + WaterVolume / rect.Width; + surface = rect.Y - rect.Height + WaterVolume / rect.Width; +#if CLIENT + drawSurface = surface; +#endif Pressure = surface; } @@ -753,8 +748,6 @@ namespace Barotrauma public override void Update(float deltaTime, Camera cam) { - base.Update(deltaTime, cam); - BallastFlora?.Update(deltaTime); UpdateProjSpecific(deltaTime, cam); @@ -808,11 +801,6 @@ namespace Barotrauma surface, rect.Y - rect.Height + waterDepth, deltaTime * 10.0f), rect.Y - rect.Height); - //interpolate the position of the rendered surface towards the "target surface" - drawSurface = Math.Max(MathHelper.Lerp( - drawSurface, - rect.Y - rect.Height + waterDepth, - deltaTime * 10.0f), rect.Y - rect.Height); for (int i = 0; i < waveY.Length; i++) { @@ -900,10 +888,10 @@ namespace Barotrauma } } - //0.01 increase every ~1000 frames = reaches full dirtiness in ~27 minutes - if (submergedSections.Count > 0 && Submarine != null && Submarine.Info.Type == SubmarineType.Player && Rand.Int(1000) == 1) + //0.016 increase every ~2000 frames = reaches full dirtiness in ~35 minutes + if (submergedSections.Count > 0 && Submarine != null && Submarine.Info.Type == SubmarineType.Player && Rand.Int(2000) == 1) { - DirtySections(submergedSections, 0.01f); + DirtySections(submergedSections, deltaTime); } if (waterVolume < Volume) @@ -911,11 +899,13 @@ namespace Barotrauma LethalPressure -= 10.0f * deltaTime; if (WaterVolume <= 0.0f) { +#if CLIENT //wait for the surface to be lerped back to bottom and the waves to settle until disabling update - if (drawSurface > rect.Y - rect.Height + 1) return; + if (drawSurface > rect.Y - rect.Height + 1) { return; } +#endif for (int i = 1; i < waveY.Length - 1; i++) { - if (waveY[i] > 0.1f) return; + if (waveY[i] > 0.1f) { return; } } update = false; @@ -1233,7 +1223,7 @@ namespace Barotrauma } } - Rectangle subRect = Submarine.CalculateDimensions(); + Rectangle subRect = Submarine.Borders; Alignment roomPos; if (rect.Y - rect.Height / 2 > subRect.Y + subRect.Height * 0.66f) @@ -1540,7 +1530,7 @@ namespace Barotrauma if (prefab != null) { hull.BallastFlora = new BallastFloraBehavior(hull, prefab, Vector2.Zero); - hull.BallastFlora.LoadSave(subElement); + hull.BallastFlora.LoadSave(subElement, idRemap); } break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs index a9fb2d699..f513fb078 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs @@ -205,6 +205,8 @@ namespace Barotrauma foreach (var cell in Cells) { cell.CellType = CellType.Removed; + cell.OnDestroyed?.Invoke(); + cell.OnDestroyed = null; } GameMain.World.Remove(Body); Dispose(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 94b486cc2..bd22ea0ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -25,7 +25,17 @@ namespace Barotrauma /// public const int MaxSubmarineWidth = 16000; - public static Level Loaded { get; private set; } + private static Level loaded; + public static Level Loaded + { + get { return loaded; } + private set + { + if (loaded == value) { return; } + loaded = value; + GameAnalyticsManager.SetCurrentLevel(loaded?.LevelData); + } + } [Flags] public enum PositionType @@ -574,12 +584,12 @@ namespace Barotrauma siteCoordsX = new List((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y)); siteCoordsY = new List((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y)); int caveSiteInterval = 500; - for (int x = siteInterval.X / 2; x < borders.Width; x += siteInterval.X) + for (int x = siteInterval.X / 2; x < borders.Width - siteInterval.X / 2; x += siteInterval.X) { - for (int y = siteInterval.Y / 2; y < borders.Height; y += siteInterval.Y) + for (int y = siteInterval.Y / 2; y < borders.Height - siteInterval.Y / 2; y += siteInterval.Y) { - int siteX = x + Rand.Range(-siteVariance.X, siteVariance.X, Rand.RandSync.Server); - int siteY = y + Rand.Range(-siteVariance.Y, siteVariance.Y, Rand.RandSync.Server); + int siteX = x + Rand.Range(-siteVariance.X, siteVariance.X + 1, Rand.RandSync.Server); + int siteY = y + Rand.Range(-siteVariance.Y, siteVariance.Y + 1, Rand.RandSync.Server); bool closeToTunnel = false; bool closeToCave = false; @@ -613,8 +623,11 @@ namespace Barotrauma if (Rand.Range(0, 10, Rand.RandSync.Server) != 0) { continue; } } - siteCoordsX.Add(siteX); - siteCoordsY.Add(siteY); + if (!TooClose(siteX, siteY)) + { + siteCoordsX.Add(siteX); + siteCoordsY.Add(siteY); + } if (closeToCave) { @@ -625,24 +638,45 @@ namespace Barotrauma int caveSiteX = x2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.Server); int caveSiteY = y2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.Server); - bool tooClose = false; - for (int i = 0; i < siteCoordsX.Count; i++) + if (!TooClose(caveSiteX, caveSiteY)) { - if (MathUtils.DistanceSquared(caveSiteX, caveSiteY, siteCoordsX[i], siteCoordsY[i]) < 10.0f * 10.0f) - { - tooClose = true; - break; - } + siteCoordsX.Add(caveSiteX); + siteCoordsY.Add(caveSiteY); } - if (tooClose) { continue; } - siteCoordsX.Add(caveSiteX); - siteCoordsY.Add(caveSiteY); } } } } } + bool TooClose(double siteX, double siteY) + { + for (int i = 0; i < siteCoordsX.Count; i++) + { + if (MathUtils.DistanceSquared(siteCoordsX[i], siteCoordsY[i], siteX, siteY) < 10.0f * 10.0f) + { + return true; + } + } + return false; + } + + for (int i = 0; i < siteCoordsX.Count; i++) + { + Debug.Assert( + siteCoordsX[i] > 0 || siteCoordsY[i] > 0, + $"Potential error in level generation: a voronoi site was outside the bounds of the level ({siteCoordsX[i]}, {siteCoordsY[i]})"); + Debug.Assert( + siteCoordsX[i] < borders.Width || siteCoordsY[i] < borders.Height, + $"Potential error in level generation: a voronoi site was outside the bounds of the level ({siteCoordsX[i]}, {siteCoordsY[i]})"); + for (int j = i + 1; j < siteCoordsX.Count; j++) + { + Debug.Assert( + MathUtils.DistanceSquared(siteCoordsX[i], siteCoordsY[i], siteCoordsX[j], siteCoordsY[j]) > 1.0f, + "Potential error in level generation: two voronoi sites are extremely close to each other."); + } + } + EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); //---------------------------------------------------------------------------------- @@ -1011,7 +1045,7 @@ namespace Barotrauma foreach (InterestingPosition pos in PositionsOfInterest) { if (pos.PositionType != PositionType.MainPath && pos.PositionType != PositionType.SidePath) { continue; } - if (pos.Position.X < 5000 || pos.Position.X > Size.X - 5000) { continue; } + if (pos.Position.X < pathBorders.X + minMainPathWidth || pos.Position.X > pathBorders.Right - minMainPathWidth) { continue; } if (Math.Abs(pos.Position.X - startPosition.X) < minMainPathWidth * 2 || Math.Abs(pos.Position.X - endPosition.X) < minMainPathWidth * 2) { continue; } if (GetTooCloseCells(pos.Position.ToVector2(), minMainPathWidth * 0.7f).Count > 0) { continue; } iceChunkPositions.Add(pos.Position); @@ -1689,7 +1723,7 @@ namespace Barotrauma { vertices[j] += position; } - var newChunk = new LevelWall(vertices, GenerationParams.WallColor, this); + var newChunk = new LevelWall(vertices, GenerationParams.WallColor, this, createBody: false); AbyssIslands.Add(new AbyssIsland(islandArea, newChunk.Cells)); continue; } @@ -1752,12 +1786,12 @@ namespace Barotrauma new Point(0, BottomPos) }; - int mountainCount = Rand.Range(GenerationParams.MountainCountMin, GenerationParams.MountainCountMax, Rand.RandSync.Server); + int mountainCount = Rand.Range(GenerationParams.MountainCountMin, GenerationParams.MountainCountMax + 1, Rand.RandSync.Server); for (int i = 0; i < mountainCount; i++) { bottomPositions.Add( new Point(Size.X / (mountainCount + 1) * (i + 1), - BottomPos + Rand.Range(GenerationParams.MountainHeightMin, GenerationParams.MountainHeightMax, Rand.RandSync.Server))); + BottomPos + Rand.Range(GenerationParams.MountainHeightMin, GenerationParams.MountainHeightMax + 1, Rand.RandSync.Server))); } bottomPositions.Add(new Point(Size.X, BottomPos)); @@ -1770,7 +1804,7 @@ namespace Barotrauma bottomPositions.Insert(i + 1, new Point( (bottomPositions[i].X + bottomPositions[i + 1].X) / 2, - (bottomPositions[i].Y + bottomPositions[i + 1].Y) / 2 + Rand.Range(0, GenerationParams.SeaFloorVariance, Rand.RandSync.Server))); + (bottomPositions[i].Y + bottomPositions[i + 1].Y) / 2 + Rand.Range(0, GenerationParams.SeaFloorVariance + 1, Rand.RandSync.Server))); i++; } @@ -1808,7 +1842,7 @@ namespace Barotrauma Rectangle allowedArea = new Rectangle(padding, padding, Size.X - padding * 2, Size.Y - padding * 2); int radius = Math.Max(caveSize.X, caveSize.Y) / 2; - var cavePos = FindPosAwayFromMainPath((parentTunnel.MinWidth + radius) * 1.5f, asCloseAsPossible: true, allowedArea); + var cavePos = FindPosAwayFromMainPath((parentTunnel.MinWidth + radius) * 1.25f, asCloseAsPossible: true, allowedArea); GenerateCave(caveParams, parentTunnel, cavePos, caveSize); @@ -1857,7 +1891,7 @@ namespace Barotrauma Tunnels.Add(tunnel); caveBranches.Add(tunnel); - int branches = Rand.Range(caveParams.MinBranchCount, caveParams.MaxBranchCount, Rand.RandSync.Server); + int branches = Rand.Range(caveParams.MinBranchCount, caveParams.MaxBranchCount + 1, Rand.RandSync.Server); for (int j = 0; j < branches; j++) { Tunnel parentBranch = caveBranches.GetRandom(Rand.RandSync.Server); @@ -2073,12 +2107,42 @@ namespace Barotrauma private Point FindPosAwayFromMainPath(double minDistance, bool asCloseAsPossible, Rectangle? limits = null) { - var validPoints = distanceField.FindAll(d => d.distance >= minDistance && (limits == null || limits.Value.Contains(d.point))); - validPoints.RemoveAll(d => d.point.Y < GetBottomPosition(d.point.X).Y + minDistance); - if (asCloseAsPossible || !validPoints.Any()) + var pointsAboveBottom = distanceField.FindAll(d => d.point.Y > GetBottomPosition(d.point.X).Y + minDistance); + if (pointsAboveBottom.Count == 0) + { + DebugConsole.ThrowError("Error in FindPosAwayFromMainPath: no valid positions above the bottom of the sea floor. Has the position of the sea floor been set too high up?"); + return distanceField[Rand.Int(distanceField.Count, Rand.RandSync.Server)].point; + } + + var validPoints = pointsAboveBottom.FindAll(d => d.distance >= minDistance && (limits == null || limits.Value.Contains(d.point))); + if (!validPoints.Any()) + { + DebugConsole.AddWarning("Failed to find a valid position far enough from the main path. Choosing the furthest possible position.\n" + Environment.StackTrace); + if (limits != null) + { + //try choosing something within the specified limits + validPoints = pointsAboveBottom.FindAll(d => limits.Value.Contains(d.point)); + } + if (!validPoints.Any()) + { + //couldn't find anything, let's just go with the furthest one + validPoints = pointsAboveBottom; + } + (Point position, double distance) furthestPoint = validPoints.First(); + foreach (var point in validPoints) + { + if (point.distance > furthestPoint.distance) + { + furthestPoint = point; + } + } + return furthestPoint.position; + } + + if (asCloseAsPossible) { if (!validPoints.Any()) { validPoints = distanceField; } - (Point position, double distance) closestPoint = validPoints.First(); + (Point position, double distance) closestPoint = validPoints.First(); foreach (var point in validPoints) { if (point.distance < closestPoint.distance) @@ -2138,7 +2202,7 @@ namespace Barotrauma { double xDiff = Math.Abs(point.X - ruinPos.X); double yDiff = Math.Abs(point.Y - ruinPos.Y); - if (xDiff < ruinSize || yDiff < ruinSize) + if (xDiff < ruinSize && yDiff < ruinSize) { shortestDistSqr = 0.0f; } @@ -2417,7 +2481,7 @@ namespace Barotrauma }, randSync: Rand.RandSync.Server); if (location.Cell == null || location.Edge == null) { break; } - int clusterSize = Rand.Range(GenerationParams.ResourceClusterSizeRange.X, GenerationParams.ResourceClusterSizeRange.Y, Rand.RandSync.Server); + int clusterSize = Rand.Range(GenerationParams.ResourceClusterSizeRange.X, GenerationParams.ResourceClusterSizeRange.Y + 1, Rand.RandSync.Server); PlaceResources(itemPrefab, clusterSize, location, out var abyssResources); var abyssClusterLocation = new ClusterLocation(location.Cell, location.Edge, initializeResourceList: true); abyssClusterLocation.Resources.AddRange(abyssResources); @@ -2897,6 +2961,7 @@ namespace Barotrauma float? edgeLength = null, float maxResourceOverlap = 0.4f) { edgeLength ??= Vector2.Distance(location.Edge.Point1, location.Edge.Point2); + Vector2 edgeDir = (location.Edge.Point2 - location.Edge.Point1) / edgeLength.Value; var minResourceOverlap = -((edgeLength.Value - (resourceCount * resourcePrefab.Size.X)) / (resourceCount * resourcePrefab.Size.X)); minResourceOverlap = Math.Max(minResourceOverlap, 0.0f); var lerpAmounts = new float[resourceCount]; @@ -2912,7 +2977,7 @@ namespace Barotrauma placedResources = new List(); for (int i = 0; i < resourceCount; i++) { - Vector2 selectedPos = Vector2.Lerp(location.Edge.Point1, location.Edge.Point2, startOffset + lerpAmounts[i]); + Vector2 selectedPos = Vector2.Lerp(location.Edge.Point1 + edgeDir * resourcePrefab.Size.X / 2, location.Edge.Point2 - edgeDir * resourcePrefab.Size.X / 2, startOffset + lerpAmounts[i]); var item = new Item(resourcePrefab, selectedPos, submarine: null); Vector2 edgeNormal = location.Edge.GetNormal(location.Cell); float moveAmount = (item.body == null ? item.Rect.Height / 2 : ConvertUnits.ToDisplayUnits(item.body.GetMaxExtent() * 0.7f)); @@ -3285,13 +3350,6 @@ namespace Barotrauma return pathCells; } - public string GetWreckIDTag(string originalTag, Submarine wreck) - { - string shortSeed = ToolBox.StringToInt(LevelData.Seed + wreck?.Info.Name).ToString(); - if (shortSeed.Length > 6) { shortSeed = shortSeed.Substring(0, 6); } - return originalTag + "_" + shortSeed; - } - public bool IsCloseToStart(Vector2 position, float minDist) => IsCloseToStart(position.ToPoint(), minDist); public bool IsCloseToEnd(Vector2 position, float minDist) => IsCloseToEnd(position.ToPoint(), minDist); @@ -3627,10 +3685,23 @@ namespace Barotrauma Wrecks = new List(wreckCount); for (int i = 0; i < wreckCount; i++) { - ContentFile contentFile = wreckFiles[i]; - if (contentFile == null) { continue; } - string wreckName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path); - SpawnSubOnPath(wreckName, contentFile, SubmarineType.Wreck); + //how many times we'll try placing another sub before giving up + const int MaxSubsToTry = 2; + int attempts = 0; + while (wreckFiles.Any() && attempts < MaxSubsToTry) + { + ContentFile contentFile = wreckFiles.First(); + wreckFiles.RemoveAt(0); + if (contentFile == null) { continue; } + string wreckName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path); + if (SpawnSubOnPath(wreckName, contentFile, SubmarineType.Wreck) != null) + { + //placed successfully + break; + } + attempts++; + } + } totalSW.Stop(); Debug.WriteLine($"{Wrecks.Count} wrecks created in { totalSW.ElapsedMilliseconds} (ms)"); @@ -4020,7 +4091,7 @@ namespace Barotrauma foreach (Submarine wreck in Wrecks) { - int corpseCount = Rand.Range(Loaded.GenerationParams.MinCorpseCount, Loaded.GenerationParams.MaxCorpseCount); + int corpseCount = Rand.Range(Loaded.GenerationParams.MinCorpseCount, Loaded.GenerationParams.MaxCorpseCount + 1); var allSpawnPoints = WayPoint.WayPointList.FindAll(wp => wp.Submarine == wreck && wp.CurrentHull != null); var pathPoints = allSpawnPoints.FindAll(wp => wp.SpawnType == SpawnType.Path); pathPoints.Shuffle(Rand.RandSync.Unsynced); @@ -4077,6 +4148,7 @@ namespace Barotrauma corpse.EnableDespawn = false; selectedPrefab.GiveItems(corpse, wreck); corpse.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null, log: false); + corpse.GiveIdCardTags(sp); spawnCounter++; static CorpsePrefab GetCorpsePrefab(Func predicate) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs index 1d6643992..dfd89203d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -304,7 +304,7 @@ namespace Barotrauma foreach (LevelObjectPrefab.ChildObject child in prefab.ChildObjects) { - int childCount = Rand.Range(child.MinCount, child.MaxCount, Rand.RandSync.Server); + int childCount = Rand.Range(child.MinCount, child.MaxCount + 1, Rand.RandSync.Server); for (int j = 0; j < childCount; j++) { var matchingPrefabs = LevelObjectPrefab.List.Where(p => child.AllowedNames.Contains(p.Name)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs index 87d9e6887..3ba23fb92 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs @@ -433,6 +433,32 @@ namespace Barotrauma return false; } + /// + /// Are there any active contacts between the physics body and the target entity + /// + public static bool CheckContactsForEntity(PhysicsBody triggerBody, Entity targetEntity) + { + foreach (Fixture fixture in triggerBody.FarseerBody.FixtureList) + { + ContactEdge contactEdge = fixture.Body.ContactList; + while (contactEdge != null) + { + if (contactEdge.Contact != null && + contactEdge.Contact.Enabled && + contactEdge.Contact.IsTouching) + { + if ((contactEdge.Contact.FixtureA.Body == triggerBody.FarseerBody && GetEntity(contactEdge.Contact.FixtureB) == targetEntity) || + (contactEdge.Contact.FixtureB.Body == triggerBody.FarseerBody && GetEntity(contactEdge.Contact.FixtureA) == targetEntity)) + { + return true; + } + } + contactEdge = contactEdge.Next; + } + } + return false; + } + public static Entity GetEntity(Fixture fixture) { if (fixture.Body == null || fixture.Body.UserData == null) { return null; } @@ -472,9 +498,6 @@ namespace Barotrauma { if (ParentTrigger != null && !ParentTrigger.IsTriggered) { return; } - triggerers.RemoveWhere(t => t.Removed); - - RemoveDistantTriggerers(PhysicsBody, triggerers, WorldPosition); bool isNotClient = true; #if CLIENT @@ -518,7 +541,9 @@ namespace Barotrauma } } } - + + RemoveInActiveTriggerers(PhysicsBody, triggerers); + if (stayTriggeredDelay > 0.0f) { if (triggerers.Count == 0) @@ -538,6 +563,8 @@ namespace Barotrauma foreach (Entity triggerer in triggerers) { + if (triggerer.Removed) { continue; } + ApplyStatusEffects(statusEffects, worldPosition, triggerer, deltaTime, targets); if (triggerer is IDamageable damageable) @@ -583,15 +610,27 @@ namespace Barotrauma } } - public static void RemoveDistantTriggerers(PhysicsBody physicsBody, HashSet triggerers, Vector2 calculateDistanceTo) + private static readonly List triggerersToRemove = new List(); + public static void RemoveInActiveTriggerers(PhysicsBody physicsBody, HashSet triggerers) { - //failsafe to ensure triggerers get removed when they're far from the trigger if (physicsBody == null) { return; } - float maxExtent = Math.Max(ConvertUnits.ToDisplayUnits(physicsBody.GetMaxExtent() * 5), 5000.0f); - triggerers.RemoveWhere(t => + + triggerersToRemove.Clear(); + foreach (var triggerer in triggerers) { - return Vector2.Distance(t.WorldPosition, calculateDistanceTo) > maxExtent; - }); + if (triggerer.Removed) + { + triggerersToRemove.Add(triggerer); + } + else if (!CheckContactsForEntity(physicsBody, triggerer)) + { + triggerersToRemove.Add(triggerer); + } + } + foreach (var triggerer in triggerersToRemove) + { + triggerers.Remove(triggerer); + } } public static void ApplyStatusEffects(List statusEffects, Vector2 worldPosition, Entity triggerer, float deltaTime, List targets) @@ -650,13 +689,15 @@ namespace Barotrauma float structureDamage = attack.GetStructureDamage(deltaTime); if (structureDamage > 0.0f) { - Explosion.RangedStructureDamage(worldPosition, attack.DamageRange, structureDamage, levelWallDamage: 0.0f); + Explosion.RangedStructureDamage(worldPosition, attack.DamageRange, structureDamage, levelWallDamage: 0.0f, emitWallDamageParticles: attack.EmitStructureDamageParticles); } } } private void ApplyForce(PhysicsBody body) { + if (body == null) { return; } + float distFactor = 1.0f; if (ForceFalloff) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelWall.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelWall.cs index 186c6bf20..67cb59004 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelWall.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelWall.cs @@ -57,7 +57,7 @@ namespace Barotrauma set { moveState = MathHelper.Clamp(value, 0.0f, MathHelper.TwoPi); } } - public LevelWall(List vertices, Color color, Level level, bool giftWrap = false) + public LevelWall(List vertices, Color color, Level level, bool giftWrap = false, bool createBody = true) { this.level = level; this.color = color; @@ -74,14 +74,17 @@ namespace Barotrauma wallCell.Edges[i].IsSolid = true; } Cells = new List() { wallCell }; - Body = CaveGenerator.GeneratePolygons(Cells, level, out triangles); - if (triangles.Count == 0) + if (createBody) { - throw new ArgumentException("Failed to generate a wall (not enough triangles). Original vertices: " + string.Join(", ", originalVertices.Select(v => v.ToString()))); - } + Body = CaveGenerator.GeneratePolygons(Cells, level, out triangles); + if (triangles.Count == 0) + { + throw new ArgumentException("Failed to generate a wall (not enough triangles). Original vertices: " + string.Join(", ", originalVertices.Select(v => v.ToString()))); + } #if CLIENT - GenerateVertices(); + GenerateVertices(); #endif + } } public LevelWall(List edgePositions, Vector2 extendAmount, Color color, Level level) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs index 771e43965..7c26aea92 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs @@ -147,8 +147,6 @@ namespace Barotrauma { List points = new List(); - var wallPrefabs = StructurePrefab.Prefabs.Where(mp => mp.Body); - foreach (XElement element in rootElement.Elements()) { if (element.Name != "Structure") { continue; } @@ -159,8 +157,12 @@ namespace Barotrauma StructurePrefab prefab = Structure.FindPrefab(name, identifier); if (prefab == null) { continue; } + float scale = element.GetAttributeFloat("scale", prefab.Scale); + var rect = element.GetAttributeVector4("rect", Vector4.Zero); - + rect.Z *= scale / prefab.Scale; + rect.W *= scale / prefab.Scale; + points.Add(new Vector2(rect.X, rect.Y)); points.Add(new Vector2(rect.X + rect.Z, rect.Y)); points.Add(new Vector2(rect.X, rect.Y - rect.W)); @@ -352,6 +354,7 @@ namespace Barotrauma if (hull.Submarine != sub) { continue; } hull.WaterVolume = 0.0f; hull.OxygenPercentage = 100.0f; + hull.BallastFlora?.Kill(); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 7a8e2f397..d3275a394 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -1,4 +1,5 @@ -using Barotrauma.Extensions; +using Barotrauma.Abilities; +using Barotrauma.Extensions; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -132,6 +133,7 @@ namespace Barotrauma #endregion private const float MechanicalMaxDiscountPercentage = 50.0f; + private const float HealMaxDiscountPercentage = 10.0f; private readonly List takenItems = new List(); public IEnumerable TakenItems @@ -780,7 +782,7 @@ namespace Barotrauma { if (priceInfo.MaxAvailableAmount > priceInfo.MinAvailableAmount) { - quantity = Rand.Range(priceInfo.MinAvailableAmount, priceInfo.MaxAvailableAmount); + quantity = Rand.Range(priceInfo.MinAvailableAmount, priceInfo.MaxAvailableAmount + 1); } else { @@ -908,6 +910,12 @@ namespace Barotrauma return (int) Math.Ceiling((1.0f - discount) * cost * MechanicalPriceMultiplier); } + public int GetAdjustedHealCost(int cost) + { + float discount = Reputation.Value / Reputation.MaxReputation * (HealMaxDiscountPercentage / 100.0f); + return (int) Math.Ceiling((1.0f - discount) * cost * PriceMultiplier); + } + /// If true, the store will be recreated if it already exists. public void CreateStore(bool force = false) { @@ -1002,7 +1010,7 @@ namespace Barotrauma private void GenerateRandomPriceModifier() { - StorePriceModifier = Rand.Range(-StorePriceModifierRange, StorePriceModifierRange); + StorePriceModifier = Rand.Range(-StorePriceModifierRange, StorePriceModifierRange + 1); } private void CreateStoreSpecials() @@ -1110,7 +1118,7 @@ namespace Barotrauma Discovered = true; if (checkTalents) { - GameSession.GetSessionCrewCharacters().ForEach(c => c.CheckTalents(AbilityEffectType.OnLocationDiscovered, new Abilities.AbilityLocation(this))); + GameSession.GetSessionCrewCharacters().ForEach(c => c.CheckTalents(AbilityEffectType.OnLocationDiscovered, new AbilityLocation(this))); } } @@ -1263,5 +1271,15 @@ namespace Barotrauma { HireManager?.Remove(); } + + class AbilityLocation : AbilityObject, IAbilityLocation + { + public AbilityLocation(Location location) + { + Location = location; + } + + public Location Location { get; set; } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index d4a2ba6e5..a5929ded1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -112,7 +112,7 @@ namespace Barotrauma System.Diagnostics.Debug.Assert(!Locations.Contains(null)); for (int i = 0; i < Locations.Count; i++) { - Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, $"location.{i}", -100, 100, Rand.Range(-10, 10, Rand.RandSync.Server)); + Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, Locations[i], $"location.{i}", -100, 100, Rand.Range(-10, 11, Rand.RandSync.Server)); } List connectionElements = new List(); @@ -214,7 +214,7 @@ namespace Barotrauma for (int i = 0; i < Locations.Count; i++) { - Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, $"location.{i}", -100, 100, Rand.Range(-10, 10, Rand.RandSync.Server)); + Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, Locations[i], $"location.{i}", -100, 100, Rand.Range(-10, 11, Rand.RandSync.Server)); } foreach (Location location in Locations) @@ -486,7 +486,8 @@ namespace Barotrauma { location.LevelData = new LevelData(location) { - Difficulty = MathHelper.Clamp(GetLevelDifficulty(location.MapPosition.X / Width), 0.0f, 100.0f) + Difficulty = MathHelper.Clamp(location.MapPosition.X / Width * 100, 0.0f, 100.0f) + //Difficulty = MathHelper.Clamp(GetLevelDifficulty(location.MapPosition.X / Width), 0.0f, 100.0f) }; location.UnlockInitialMissions(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index 8134430b4..e1abb2329 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -3,12 +3,10 @@ using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Xml.Linq; -using Barotrauma.Networking; namespace Barotrauma { @@ -20,14 +18,17 @@ namespace Barotrauma protected List linkedToID; public List unresolvedLinkedToID; - + + private const int GapUpdateInterval = 4; + private static int gapUpdateTimer; + /// /// List of upgrades this item has /// protected readonly List Upgrades = new List(); - + public HashSet disallowedUpgrades = new HashSet(); - + [Editable, Serialize("", true)] public string DisallowedUpgrades { @@ -101,7 +102,7 @@ namespace Barotrauma return !DrawBelowWater; } } - + public virtual bool Linkable { get { return false; } @@ -231,6 +232,9 @@ namespace Barotrauma protected set; } = true; + [Serialize("", true, "Submarine editor layer")] + public string Layer { get; set; } + /// /// The index of the outpost module this entity originally spawned in (-1 if not an outpost item) /// @@ -242,7 +246,7 @@ namespace Barotrauma { get { return ""; } } - + public MapEntity(MapEntityPrefab prefab, Submarine submarine, ushort id) : base(submarine, id) { this.prefab = prefab; @@ -303,7 +307,7 @@ namespace Barotrauma { return GetUpgrade(identifier) != null; } - + public Upgrade GetUpgrade(string identifier) { return Upgrades.Find(upgrade => upgrade.Identifier == identifier); @@ -329,7 +333,7 @@ namespace Barotrauma } DebugConsole.Log($"Set (ID: {ID} {prefab.Name})'s \"{upgrade.Prefab.Name}\" upgrade to level {upgrade.Level}"); } - + /// /// Adds a new upgrade to the item /// @@ -407,6 +411,7 @@ namespace Barotrauma } //connect clone wires to the clone items and refresh links between doors and gaps + List orphanedWires = new List(); for (int i = 0; i < clones.Count; i++) { if (!(clones[i] is Item cloneItem)) { continue; } @@ -435,11 +440,11 @@ namespace Barotrauma disconnectedFromClone.DisconnectedWires.Add(cloneWire); if (cloneWire.Item.body != null) { cloneWire.Item.body.Enabled = false; } cloneWire.IsActive = false; - continue; + continue; } var connectedItem = originalWire.Connections[n].Item; - if (connectedItem == null) { continue; } + if (connectedItem == null || !entitiesToClone.Contains(connectedItem)) { continue; } //index of the item the wire is connected to int itemIndex = entitiesToClone.IndexOf(connectedItem); @@ -466,6 +471,20 @@ namespace Barotrauma (clones[itemIndex] as Item).Connections[connectionIndex].TryAddLink(cloneWire); cloneWire.Connect((clones[itemIndex] as Item).Connections[connectionIndex], false); } + + if ((cloneWire.Connections[0] == null || cloneWire.Connections[1] == null) && cloneItem.GetComponent() == null) + { + if (!clones.Any(c => (c as Item)?.GetComponent()?.DisconnectedWires.Contains(cloneWire) ?? false)) + { + orphanedWires.Add(cloneWire); + } + } + } + + foreach (var orphanedWire in orphanedWires) + { + orphanedWire.Item.Remove(); + clones.Remove(orphanedWire.Item); } return clones; @@ -535,59 +554,48 @@ namespace Barotrauma linkedTo.Clear(); } } - static int tick = 0; + /// /// Call Update() on every object in Entity.list /// public static void UpdateAll(float deltaTime, Camera cam) { - tick++; - - if (tick % GameMain.Lua.game.mapEntityUpdateRate == 0) + foreach (Hull hull in Hull.hullList) { + hull.Update(deltaTime, cam); + } +#if CLIENT + Hull.UpdateCheats(deltaTime, cam); +#endif - foreach (Hull hull in Hull.hullList) - { - hull.Update(deltaTime * GameMain.Lua.game.mapEntityUpdateRate, cam); - } - - foreach (Structure structure in Structure.WallList) - { - structure.Update(deltaTime * GameMain.Lua.game.mapEntityUpdateRate, cam); - } - - - //update gaps in random order, because otherwise in rooms with multiple gaps - //the water/air will always tend to flow through the first gap in the list, - //which may lead to weird behavior like water draining down only through - //one gap in a room even if there are several - foreach (Gap gap in Gap.GapList.OrderBy(g => Rand.Int(int.MaxValue))) - { - gap.Update(deltaTime * GameMain.Lua.game.mapEntityUpdateRate, cam); - } - - Powered.UpdatePower(deltaTime * GameMain.Lua.game.mapEntityUpdateRate); - foreach (Item item in Item.ItemList) - { - if (GameMain.Lua.game.updatePriorityItems.Contains(item)) continue; - - item.Update(deltaTime * GameMain.Lua.game.mapEntityUpdateRate, cam); - } + foreach (Structure structure in Structure.WallList) + { + structure.Update(deltaTime, cam); } - foreach (var item in GameMain.Lua.game.updatePriorityItems) + //update gaps in random order, because otherwise in rooms with multiple gaps + //the water/air will always tend to flow through the first gap in the list, + //which may lead to weird behavior like water draining down only through + //one gap in a room even if there are several + gapUpdateTimer++; + if (gapUpdateTimer >= GapUpdateInterval) { - if (item.Removed) continue; + foreach (Gap gap in Gap.GapList.OrderBy(g => Rand.Int(int.MaxValue))) + { + gap.Update(deltaTime * GapUpdateInterval, cam); + } + gapUpdateTimer = 0; + } + Powered.UpdatePower(deltaTime); + foreach (Item item in Item.ItemList) + { item.Update(deltaTime, cam); } - if (tick % GameMain.Lua.game.mapEntityUpdateRate == 0) - { - UpdateAllProjSpecific(deltaTime * GameMain.Lua.game.mapEntityUpdateRate); + UpdateAllProjSpecific(deltaTime); - Spawner?.Update(); - } + Spawner?.Update(); } static partial void UpdateAllProjSpecific(float deltaTime); @@ -743,11 +751,11 @@ namespace Barotrauma foreach (ushort i in e.linkedToID) { - if (FindEntityByID(i) is MapEntity linked) + if (FindEntityByID(i) is MapEntity linked) { - e.linkedTo.Add(linked); - } - else + e.linkedTo.Add(linked); + } + else { #if DEBUG DebugConsole.ThrowError($"Linking the entity \"{e.Name}\" to another entity failed. Could not find an entity with the ID \"{i}\"."); @@ -788,7 +796,7 @@ namespace Barotrauma /// /// Gets all linked entities of specific type. /// - private static void GetLinkedEntitiesRecursive(MapEntity mapEntity, HashSet linkedTargets, ref int depth, int? maxDepth = null, Func filter = null) + private static void GetLinkedEntitiesRecursive(MapEntity mapEntity, HashSet linkedTargets, ref int depth, int? maxDepth = null, Func filter = null) where T : MapEntity { if (depth > maxDepth) { return; } @@ -806,4 +814,4 @@ namespace Barotrauma } } } -} +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs index 93eedb775..6596c4067 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; +using Barotrauma.Extensions; namespace Barotrauma { @@ -317,6 +318,11 @@ namespace Barotrauma return null; } + public static MapEntityPrefab GetRandom(Predicate predicate, Rand.RandSync sync) + { + return List.GetRandom(p => predicate(p), sync); + } + /// /// Find a matching map entity prefab /// @@ -338,6 +344,8 @@ namespace Barotrauma if (target == null) { return false; } if (target is StructurePrefab && AllowedLinks.Contains("structure")) { return true; } if (target is ItemPrefab && AllowedLinks.Contains("item")) { return true; } + if (target is LinkedSubmarinePrefab && Tags.Contains("dock")) { return true; } + if (this is LinkedSubmarinePrefab && target.Tags.Contains("dock")) { return true; } return AllowedLinks.Contains(target.Identifier) || target.AllowedLinks.Contains(identifier) || target.Tags.Any(t => AllowedLinks.Contains(t)) || Tags.Any(t => target.AllowedLinks.Contains(t)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index 9169f06e4..c68763505 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -115,7 +115,7 @@ namespace Barotrauma private float? maxHealth; - [Serialize(100.0f, true)] + [Serialize(100.0f, true), Editable(MinValueFloat = 0)] public float MaxHealth { get => maxHealth ?? Prefab.Health; @@ -704,7 +704,7 @@ namespace Barotrauma if (BodyWidth > 0.0f) { rectSize.X = BodyWidth; } if (BodyHeight > 0.0f) { rectSize.Y = BodyHeight; } - Vector2 bodyPos = WorldPosition + BodyOffset; + Vector2 bodyPos = WorldPosition + BodyOffset * Scale; Vector2 transformedMousePos = MathUtils.RotatePointAroundTarget(position, bodyPos, BodyRotation); @@ -876,7 +876,7 @@ namespace Barotrauma return true; } - public void AddDamage(int sectionIndex, float damage, Character attacker = null) + public void AddDamage(int sectionIndex, float damage, Character attacker = null, bool emitParticles = true) { if (!Prefab.Body || Prefab.Platform || Indestructible) { return; } @@ -885,7 +885,7 @@ namespace Barotrauma var section = Sections[sectionIndex]; #if CLIENT - if (damage > 0) + if (damage > 0 && emitParticles) { float dmg = Math.Min(MaxHealth - section.damage, damage); float particleAmount = MathHelper.Lerp(0, 25, MathUtils.InverseLerp(0, 100, dmg * Rand.Range(0.75f, 1.25f))); @@ -898,8 +898,8 @@ namespace Barotrauma { var worldRect = section.WorldRect; Vector2 particlePos = new Vector2( - Rand.Range(worldRect.X, worldRect.Right), - Rand.Range(worldRect.Y - worldRect.Height, worldRect.Y)); + Rand.Range(worldRect.X, worldRect.Right + 1), + Rand.Range(worldRect.Y - worldRect.Height, worldRect.Y + 1)); var particle = GameMain.ParticleManager.CreateParticle("shrapnel", particlePos, Rand.Vector(Rand.Range(1.0f, 50.0f)), collisionIgnoreTimer: 1f); if (particle == null) break; @@ -1016,7 +1016,10 @@ namespace Barotrauma damageAmount = attack.GetStructureDamage(deltaTime); AddDamage(i, damageAmount, attacker); #if CLIENT - GameMain.ParticleManager.CreateParticle("dustcloud", SectionPosition(i), 0.0f, 0.0f); + if (attack.EmitStructureDamageParticles) + { + GameMain.ParticleManager.CreateParticle("dustcloud", SectionPosition(i), 0.0f, 0.0f); + } #endif } } @@ -1034,7 +1037,7 @@ namespace Barotrauma if (Submarine != null && damageAmount > 0 && attacker != null) { - var abilityAttackerSubmarine = new AbilityCharacterSubmarine(attacker, Submarine); + var abilityAttackerSubmarine = new AbilityAttackerSubmarine(attacker, Submarine); foreach (Character character in Character.CharacterList) { character.CheckTalents(AbilityEffectType.AfterSubmarineAttacked, abilityAttackerSubmarine); @@ -1529,6 +1532,7 @@ namespace Barotrauma public virtual void Reset() { SerializableProperties = SerializableProperty.DeserializeProperties(this, Prefab.ConfigElement); + MaxHealth = Prefab.Health; Sprite.ReloadXML(); SpriteDepth = Sprite.Depth; NoAITarget = Prefab.NoAITarget; @@ -1542,4 +1546,15 @@ namespace Barotrauma } } } + + class AbilityAttackerSubmarine : AbilityObject, IAbilityCharacter, IAbilitySubmarine + { + public AbilityAttackerSubmarine(Character character, Submarine submarine) + { + Character = character; + Submarine = submarine; + } + public Character Character { get; set; } + public Submarine Submarine { get; set; } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index 9dad1fd8d..6901ddf25 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -1535,7 +1535,7 @@ namespace Barotrauma element.Add(new XAttribute("tags", Info.Tags.ToString())); element.Add(new XAttribute("gameversion", GameMain.Version.ToString())); - Rectangle dimensions = CalculateDimensions(); + Rectangle dimensions = VisibleBorders; element.Add(new XAttribute("dimensions", XMLExtensions.Vector2ToString(dimensions.Size.ToVector2()))); var cargoContainers = GetCargoContainers(); element.Add(new XAttribute("cargocapacity", cargoContainers.Sum(c => c.container.Capacity))); @@ -1615,7 +1615,7 @@ namespace Barotrauma Info.CheckSubsLeftBehind(element); } - public bool SaveAs(string filePath, System.IO.MemoryStream previewImage = null) + public bool TrySaveAs(string filePath, System.IO.MemoryStream previewImage = null) { var newInfo = new SubmarineInfo(this) { @@ -1628,8 +1628,19 @@ namespace Barotrauma //remove reference to the preview image from the old info, so we don't dispose it (the new info still uses the texture) Info.PreviewImage = null; #endif - Info.Dispose(); Info = newInfo; - return newInfo.SaveAs(filePath, previewImage); + Info.Dispose(); + Info = newInfo; + + try + { + newInfo.SaveAs(filePath, previewImage); + } + catch (Exception e) + { + DebugConsole.ThrowError($"Saving submarine \"{filePath}\" failed!", e); + return false; + } + return true; } public static bool Unloading @@ -1643,9 +1654,8 @@ namespace Barotrauma Unloading = true; #if CLIENT - RemoveAllRoundSounds(); //Sound.OnGameEnd(); - - if (GameMain.LightManager != null) GameMain.LightManager.ClearLights(); + RemoveAllRoundSounds(); + GameMain.LightManager?.ClearLights(); #endif var _loaded = new List(loaded); @@ -1766,7 +1776,7 @@ namespace Barotrauma if (connectedWp.isObstructed) { continue; } Vector2 start = ConvertUnits.ToSimUnits(wp.WorldPosition); Vector2 end = ConvertUnits.ToSimUnits(connectedWp.WorldPosition); - var body = Submarine.PickBody(start, end, null, Physics.CollisionLevel, allowInsideFixture: false); + var body = PickBody(start, end, null, Physics.CollisionLevel, allowInsideFixture: false); if (body != null) { connectedWp.isObstructed = true; @@ -1793,7 +1803,7 @@ namespace Barotrauma foreach (var connection in node.connections) { var connectedWp = connection.Waypoint; - if (connectedWp.isObstructed) { continue; } + if (connectedWp.isObstructed || connectedWp.Ladders != null) { continue; } Vector2 start = ConvertUnits.ToSimUnits(wp.WorldPosition) - otherSub.SimPosition; Vector2 end = ConvertUnits.ToSimUnits(connectedWp.WorldPosition) - otherSub.SimPosition; var body = PickBody(start, end, null, Physics.CollisionWall, allowInsideFixture: true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index 913d9e2cf..499acf25d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -115,6 +115,8 @@ namespace Barotrauma { this.submarine = sub; + Vector2 minExtents = Vector2.Zero, maxExtents = Vector2.Zero; + Vector2 visibleMinExtents = Vector2.Zero, visibleMaxExtents = Vector2.Zero; Body farseerBody = null; if (!Hull.hullList.Any(h => h.Submarine == sub)) { @@ -133,8 +135,6 @@ namespace Barotrauma } HullVertices = convexHull; - Vector2 minExtents = Vector2.Zero, maxExtents = Vector2.Zero; - Vector2 visibleMinExtents = Vector2.Zero, visibleMaxExtents = Vector2.Zero; farseerBody = GameMain.World.CreateBody(); farseerBody.UserData = this; @@ -142,25 +142,18 @@ namespace Barotrauma { if (mapEntity.Submarine != submarine || !(mapEntity is Structure wall)) { continue; } + bool hasCollider = wall.HasBody && !wall.IsPlatform && wall.StairDirection == Direction.None; Rectangle rect = wall.Rect; - visibleMinExtents.X = Math.Min(rect.X, visibleMinExtents.X); - visibleMinExtents.Y = Math.Min(rect.Y - rect.Height, visibleMinExtents.Y); - visibleMaxExtents.X = Math.Max(rect.Right, visibleMaxExtents.X); - visibleMaxExtents.Y = Math.Max(rect.Y, visibleMaxExtents.Y); - - if (!wall.HasBody || wall.IsPlatform || wall.StairDirection != Direction.None) { continue; } - - farseerBody.CreateRectangle( - ConvertUnits.ToSimUnits(wall.BodyWidth), - ConvertUnits.ToSimUnits(wall.BodyHeight), - 50.0f, - -wall.BodyRotation, - ConvertUnits.ToSimUnits(new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2) + wall.BodyOffset)).UserData = wall; - - minExtents.X = Math.Min(visibleMinExtents.X, minExtents.X); - minExtents.Y = Math.Min(visibleMinExtents.Y, minExtents.Y); - maxExtents.X = Math.Max(visibleMaxExtents.X, maxExtents.X); - maxExtents.Y = Math.Max(visibleMaxExtents.Y, maxExtents.Y); + SetExtents(new Vector2(rect.X, rect.Y - rect.Height), new Vector2(rect.Right, rect.Y), hasCollider); + if (hasCollider) + { + farseerBody.CreateRectangle( + ConvertUnits.ToSimUnits(wall.BodyWidth), + ConvertUnits.ToSimUnits(wall.BodyHeight), + 50.0f, + -wall.BodyRotation, + ConvertUnits.ToSimUnits(new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2) + wall.BodyOffset)).UserData = wall; + } } foreach (Hull hull in Hull.hullList) @@ -168,21 +161,13 @@ namespace Barotrauma if (hull.Submarine != submarine || hull.IdFreed) { continue; } Rectangle rect = hull.Rect; + SetExtents(new Vector2(rect.X, rect.Y - rect.Height), new Vector2(rect.Right, rect.Y), hasCollider: true); + farseerBody.CreateRectangle( ConvertUnits.ToSimUnits(rect.Width), ConvertUnits.ToSimUnits(rect.Height), 100.0f, ConvertUnits.ToSimUnits(new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2))).UserData = hull; - - visibleMinExtents.X = Math.Min(rect.X, visibleMinExtents.X); - visibleMinExtents.Y = Math.Min(rect.Y - rect.Height, visibleMinExtents.Y); - visibleMaxExtents.X = Math.Max(rect.Right, visibleMaxExtents.X); - visibleMaxExtents.Y = Math.Max(rect.Y, visibleMaxExtents.Y); - - minExtents.X = Math.Min(visibleMinExtents.X, minExtents.X); - minExtents.Y = Math.Min(visibleMinExtents.Y, minExtents.Y); - maxExtents.X = Math.Max(visibleMaxExtents.X, maxExtents.X); - maxExtents.Y = Math.Max(visibleMaxExtents.Y, maxExtents.Y); } foreach (Item item in Item.ItemList) @@ -207,31 +192,21 @@ namespace Barotrauma if (width > 0.0f && height > 0.0f) { item.StaticFixtures.Add(farseerBody.CreateRectangle(simWidth, simHeight, 5.0f, simPos)); - - visibleMinExtents.X = Math.Min(item.Position.X - width / 2, visibleMinExtents.X); - visibleMinExtents.Y = Math.Min(item.Position.Y - height / 2, visibleMinExtents.Y); - visibleMaxExtents.X = Math.Max(item.Position.X + width / 2, visibleMaxExtents.X); - visibleMaxExtents.Y = Math.Max(item.Position.Y + height / 2, visibleMaxExtents.Y); + SetExtents(item.Position - new Vector2(width, height) / 2, item.Position + new Vector2(width, height) / 2, hasCollider: true); } else if (radius > 0.0f && width > 0.0f) { item.StaticFixtures.Add(farseerBody.CreateRectangle(simWidth, simRadius * 2, 5.0f, simPos)); item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitX * simWidth / 2)); item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitX * simWidth / 2)); - visibleMinExtents.X = Math.Min(item.Position.X - width / 2 - radius, visibleMinExtents.X); - visibleMinExtents.Y = Math.Min(item.Position.Y - radius, visibleMinExtents.Y); - visibleMaxExtents.X = Math.Max(item.Position.X + width / 2 + radius, visibleMaxExtents.X); - visibleMaxExtents.Y = Math.Max(item.Position.Y + radius, visibleMaxExtents.Y); + SetExtents(item.Position - new Vector2(width / 2 + radius, height / 2), item.Position + new Vector2(width / 2 + radius, height / 2), hasCollider: true); } else if (radius > 0.0f && height > 0.0f) { item.StaticFixtures.Add(farseerBody.CreateRectangle(simRadius * 2, height, 5.0f, simPos)); item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitY * simHeight / 2)); item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitX * simHeight / 2)); - visibleMinExtents.X = Math.Min(item.Position.X - radius, visibleMinExtents.X); - visibleMinExtents.Y = Math.Min(item.Position.Y - height / 2 - radius, visibleMinExtents.Y); - visibleMaxExtents.X = Math.Max(item.Position.X + radius, visibleMaxExtents.X); - visibleMaxExtents.Y = Math.Max(item.Position.Y + height / 2 + radius, visibleMaxExtents.Y); + SetExtents(item.Position - new Vector2(width / 2, height / 2 + radius), item.Position + new Vector2(width / 2, height / 2 + radius), hasCollider: true); } else if (radius > 0.0f) { @@ -240,12 +215,8 @@ namespace Barotrauma visibleMinExtents.Y = Math.Min(item.Position.Y - radius, visibleMinExtents.Y); visibleMaxExtents.X = Math.Max(item.Position.X + radius, visibleMaxExtents.X); visibleMaxExtents.Y = Math.Max(item.Position.Y + radius, visibleMaxExtents.Y); + SetExtents(item.Position - new Vector2(radius, radius), item.Position + new Vector2(radius, radius), hasCollider: true); } - item.StaticFixtures.ForEach(f => f.UserData = item); - minExtents.X = Math.Min(visibleMinExtents.X, minExtents.X); - minExtents.Y = Math.Min(visibleMinExtents.Y, minExtents.Y); - maxExtents.X = Math.Max(visibleMaxExtents.X, maxExtents.X); - maxExtents.Y = Math.Max(visibleMaxExtents.Y, maxExtents.Y); } Borders = new Rectangle((int)minExtents.X, (int)maxExtents.Y, (int)(maxExtents.X - minExtents.X), (int)(maxExtents.Y - minExtents.Y)); @@ -271,6 +242,21 @@ namespace Barotrauma farseerBody.UserData = submarine; Body = new PhysicsBody(farseerBody); + + void SetExtents(Vector2 min, Vector2 max, bool hasCollider) + { + visibleMinExtents.X = Math.Min(min.X, visibleMinExtents.X); + visibleMinExtents.Y = Math.Min(min.Y, visibleMinExtents.Y); + visibleMaxExtents.X = Math.Max(max.X, visibleMaxExtents.X); + visibleMaxExtents.Y = Math.Max(max.Y, visibleMaxExtents.Y); + if (hasCollider) + { + minExtents.X = Math.Min(min.X, minExtents.X); + minExtents.Y = Math.Min(min.Y, minExtents.Y); + maxExtents.X = Math.Max(max.X, maxExtents.X); + maxExtents.Y = Math.Max(max.Y, maxExtents.Y); + } + } } private List GenerateConvexHull() @@ -853,6 +839,8 @@ namespace Barotrauma Vector2 impulse = direction * impact * 0.5f; impulse = impulse.ClampLength(MaxCollisionImpact); + float impulseMagnitude = impulse.Length(); + if (!MathUtils.IsValid(impulse)) { string errorMsg = @@ -919,8 +907,9 @@ namespace Barotrauma foreach (Item item in Item.ItemList) { if (item.Submarine != submarine || item.CurrentHull == null || item.body == null || !item.body.Enabled) { continue; } + if (item.body.Mass > impulseMagnitude) { continue; } - item.body.ApplyLinearImpulse(item.body.Mass * impulse, 10.0f); + item.body.ApplyLinearImpulse(impulse, 10.0f); item.PositionUpdateInterval = 0.0f; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index d8dc6665f..1ec236c0a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -401,6 +401,7 @@ namespace Barotrauma public bool IsVanillaSubmarine() { + if (FilePath == null) { return false; } var vanilla = GameMain.VanillaContent; if (vanilla != null) { @@ -521,7 +522,7 @@ namespace Barotrauma } //saving/loading ---------------------------------------------------- - public bool SaveAs(string filePath, System.IO.MemoryStream previewImage = null) + public void SaveAs(string filePath, System.IO.MemoryStream previewImage = null) { var newElement = new XElement( SubmarineElement.Name, @@ -543,18 +544,9 @@ namespace Barotrauma { doc.Root.Add(new XAttribute("previewimage", Convert.ToBase64String(previewImage.ToArray()))); } - try - { - SaveUtil.CompressStringToFile(filePath, doc.ToString()); - Md5Hash.RemoveFromCache(filePath); - } - catch (Exception e) - { - DebugConsole.ThrowError("Saving submarine \"" + filePath + "\" failed!", e); - return false; - } - return true; + SaveUtil.CompressStringToFile(filePath, doc.ToString()); + Md5Hash.RemoveFromCache(filePath); } public static void AddToSavedSubs(SubmarineInfo subInfo) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs index 6d282715f..36dbbb1b9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs @@ -112,7 +112,7 @@ namespace Barotrauma.Networking set; } - protected ChatMessage(string senderName, string text, ChatMessageType type, Character sender, Client client, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None) + protected ChatMessage(string senderName, string text, ChatMessageType type, Character sender, Client client, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None, Color? textColor = null) { Text = text; Type = type; @@ -122,11 +122,13 @@ namespace Barotrauma.Networking SenderName = senderName; ChangeType = changeType; - } - public static ChatMessage Create(string senderName, string text, ChatMessageType type, Character sender, Client client = null, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None) + customTextColor = textColor; + } + + public static ChatMessage Create(string senderName, string text, ChatMessageType type, Character sender, Client client = null, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None, Color? textColor = null) { - return new ChatMessage(senderName, text, type, sender, client ?? GameMain.NetworkMember?.ConnectedClients?.Find(c => c.Character != null && c.Character == sender), changeType); + return new ChatMessage(senderName, text, type, sender, client ?? GameMain.NetworkMember?.ConnectedClients?.Find(c => c.Character != null && c.Character == sender), changeType, textColor); } public static string GetChatMessageCommand(string message, out string messageWithoutCommand) @@ -225,27 +227,30 @@ namespace Barotrauma.Networking break; case ChatMessageType.Radio: case ChatMessageType.Order: - if (receiver != null && !receiver.IsDead) + if (receiver?.Inventory != null && !receiver.IsDead) { - var receiverItem = receiver.Inventory?.AllItems.FirstOrDefault(i => i.GetComponent() != null); - //character doesn't have a radio -> don't send - if (receiverItem == null || !receiver.HasEquippedItem(receiverItem)) { return spokenMsg; } - - var senderItem = sender.Inventory?.AllItems.FirstOrDefault(i => i.GetComponent() != null); - if (senderItem == null || !sender.HasEquippedItem(senderItem)) { return spokenMsg; } - - var receiverRadio = receiverItem.GetComponent(); - var senderRadio = senderItem.GetComponent(); - - if (!receiverRadio.CanReceive(senderRadio)) { return spokenMsg; } - - string msg = ApplyDistanceEffect(receiverItem, senderItem, message, senderRadio.Range); - if (sender.SpeechImpediment > 0.0f) + foreach (Item receiverItem in receiver.Inventory.AllItems.Where(i => i.GetComponent()?.LinkToChat ?? false)) { - //speech impediment doesn't reduce the range when using a radio, but adds extra garbling - msg = ApplyDistanceEffect(msg, sender.SpeechImpediment / 100.0f); + if (sender.Inventory == null || !receiver.HasEquippedItem(receiverItem)) { continue; } + + foreach (Item senderItem in sender.Inventory.AllItems.Where(i => i.GetComponent()?.LinkToChat ?? false)) + { + if (!sender.HasEquippedItem(senderItem)) { continue; } + + var receiverRadio = receiverItem.GetComponent(); + var senderRadio = senderItem.GetComponent(); + if (!receiverRadio.CanReceive(senderRadio)) { continue; } + + string msg = ApplyDistanceEffect(receiverItem, senderItem, message, senderRadio.Range); + if (sender.SpeechImpediment > 0.0f) + { + //speech impediment doesn't reduce the range when using a radio, but adds extra garbling + msg = ApplyDistanceEffect(msg, sender.SpeechImpediment / 100.0f); + } + return msg; + } } - return msg; + return spokenMsg; } break; } @@ -275,7 +280,7 @@ namespace Barotrauma.Networking foreach (Item item in sender.Inventory.AllItems) { var wifiComponent = item.GetComponent(); - if (wifiComponent == null || !wifiComponent.CanTransmit() || !sender.HasEquippedItem(item)) { continue; } + if (wifiComponent == null || !wifiComponent.LinkToChat || !wifiComponent.CanTransmit() || !sender.HasEquippedItem(item)) { continue; } if (radio == null || wifiComponent.Range > radio.Range) { radio = wifiComponent; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs index d77dc356a..01ef58ee2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs @@ -22,7 +22,11 @@ namespace Barotrauma.Networking ManageSettings = 0x200, ManagePermissions = 0x400, KarmaImmunity = 0x800, - All = 0xFFF + BuyItems = 0x1000, + SellInventoryItems = 0x2000, + SellSubItems = 0x4000, + CampaignStore = 0x8000, + All = 0xFFFF } class PermissionPreset diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs new file mode 100644 index 000000000..a30e4476c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs @@ -0,0 +1,547 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + /// + /// Marks fields and properties as to be serialized and deserialized by . + /// Also contains settings for some types like maximum and minimum values for numbers to reduce bits used. + /// + /// + /// + /// struct NetPurchasedItem : INetSerializableStruct + /// { + /// [NetworkSerialize] + /// public string Identifier; + /// + /// [NetworkSerialize(ArrayMaxSize = 16)] + /// public string[] Tags; + /// + /// [NetworkSerialize(MinValueInt = 0, MaxValueInt = 8)] + /// public int Amount; + /// } + /// + /// + /// + /// Using the attribute on the struct will make all fields and properties serialized + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Struct | AttributeTargets.Property)] + public class NetworkSerialize : Attribute + { + public int MaxValueInt = int.MaxValue; + public int MinValueInt = int.MinValue; + public float MaxValueFloat = float.MaxValue; + public float MinValueFloat = float.MinValue; + public int NumberOfBits = 8; + public bool IncludeColorAlpha = false; + public int ArrayMaxSize = ushort.MaxValue; + } + + /// + /// Static class that contains serialize and deserialize functions for different types used in + /// + public static class NetSerializableProperties + { + 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; + public readonly WriteDelegate WriteAction; + + public ReadWriteBehavior(ReadDelegate readAction, WriteDelegate writeAction) + { + ReadAction = readAction; + WriteAction = writeAction; + } + } + + private static readonly ImmutableDictionary TypeBehaviors = new Dictionary + { + { typeof(Boolean), new ReadWriteBehavior(ReadBoolean, WriteDynamic) }, + { typeof(Byte), new ReadWriteBehavior(ReadByte, WriteDynamic) }, + { typeof(UInt16), new ReadWriteBehavior(ReadUInt16, WriteDynamic) }, + { typeof(Int16), new ReadWriteBehavior(ReadInt16, WriteDynamic) }, + { typeof(UInt32), new ReadWriteBehavior(ReadUInt32, WriteDynamic) }, + { typeof(Int32), new ReadWriteBehavior(ReadInt32, WriteInt32) }, + { typeof(UInt64), new ReadWriteBehavior(ReadUInt64, WriteDynamic) }, + { typeof(Int64), new ReadWriteBehavior(ReadInt64, WriteDynamic) }, + { typeof(Single), new ReadWriteBehavior(ReadSingle, WriteSingle) }, + { typeof(Double), new ReadWriteBehavior(ReadDouble, WriteDynamic) }, + { typeof(String), new ReadWriteBehavior(ReadString, WriteDynamic) }, + { typeof(Color), new ReadWriteBehavior(ReadColor, WriteColor) }, + { 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 + { type => type.BaseType?.IsAssignableFrom(typeof(Array)) ?? false, new ReadWriteBehavior(ReadArray, WriteArray) }, + + // Nested INetSerializableStructs + { type => typeof(INetSerializableStruct).IsAssignableFrom(type), new ReadWriteBehavior(ReadINetSerializableStruct, WriteINetSerializableStruct) }, + + // Enums + { type => type.IsEnum, new ReadWriteBehavior(ReadEnum, WriteEnum) }, + + // Nullable / Optional types + { type => Nullable.GetUnderlyingType(type) != null, new ReadWriteBehavior(ReadNullable, WriteNullable) } + }.ToImmutableDictionary(); + + private static void WriteInvalid(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) => throw new InvalidOperationException($"Type {obj?.GetType()} cannot be serialized. Did you forget to implement INetSerializableStruct?"); + + private static dynamic ReadInvalid(IReadMessage inc, Type type, NetworkSerialize attribute) => throw new InvalidOperationException($"Type {type} cannot be deserialized. Did you forget to implement INetSerializableStruct?"); + + private static void WriteNullable(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) + { + if (obj is { } notNull) + { + msg.Write(true); + + if (TryFindBehavior(notNull.GetType(), out ReadWriteBehavior behavior)) + { + // uh oh, something terrible has happened! + if (behavior.WriteAction == WriteNullable) { behavior = InvalidReadWriteBehavior; } + + behavior.WriteAction(notNull, attribute, msg); + return; + } + } + + msg.Write(false); + } + + private static dynamic? ReadNullable(IReadMessage inc, Type type, NetworkSerialize attribute) + { + if (!inc.ReadBoolean()) { return null; } + + Type? underlyingType = Nullable.GetUnderlyingType(type); + if (underlyingType is null) { throw new InvalidOperationException($"Could not get the underlying type of {type} in {nameof(ReadNullable)}"); } + + if (TryFindBehavior(underlyingType, out ReadWriteBehavior behavior)) + { + // uh oh, something terrible has happened! + if (behavior.ReadAction == ReadNullable) { behavior = InvalidReadWriteBehavior; } + + return behavior.ReadAction(inc, underlyingType, attribute); + } + + throw new InvalidOperationException($"Could not find suitable behavior for type {underlyingType} in {nameof(ReadNullable)}"); + } + + private static void WriteEnum(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) + { + if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); } + + Range range = GetEnumRange(obj.GetType()); + msg.WriteRangedInteger(Convert.ChangeType(obj, obj.GetTypeCode()), range.Start, range.End); + } + + private static dynamic ReadEnum(IReadMessage inc, Type type, NetworkSerialize attribute) + { + Range range = GetEnumRange(type); + int enumIndex = inc.ReadRangedInteger(range.Start, range.End); + + foreach (dynamic e in Enum.GetValues(type)) + { + if (Convert.ChangeType(e, e.GetTypeCode()) == enumIndex) { return e; } + } + + throw new InvalidOperationException($"An enum {type} with value {enumIndex} could not be found in {nameof(ReadEnum)}"); + } + + private static void WriteINetSerializableStruct(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) + { + if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); } + + if (!(obj is INetSerializableStruct serializableStruct)) { throw new InvalidOperationException($"Object in {nameof(WriteINetSerializableStruct)} was {obj.GetType()} but expected {nameof(INetSerializableStruct)}"); } + + serializableStruct.Write(msg); + } + + private static dynamic ReadINetSerializableStruct(IReadMessage inc, Type type, NetworkSerialize attribute) + { + return INetSerializableStruct.ReadDynamic(type, inc); + } + + private static void WriteDynamic(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) + { + if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); } + + msg.Write(obj); + } + + private static dynamic ReadArray(IReadMessage inc, Type type, NetworkSerialize attribute) + { + Type? elementType = type.GetElementType(); + if (elementType is null) { throw new InvalidOperationException($"Could not get the element type of {type} in {nameof(ReadArray)}"); } + + int length = inc.ReadRangedInteger(0, attribute.ArrayMaxSize); + + Array list = Array.CreateInstance(elementType, length); + + for (int i = 0; i < length; i++) + { + if (TryFindBehavior(elementType, out ReadWriteBehavior behavior)) + { + list.SetValue(behavior.ReadAction(inc, elementType, attribute), i); + } + else + { + throw new InvalidOperationException($"Could not find suitable behavior for type {elementType} in {nameof(ReadArray)}"); + } + } + + return list; + } + + private static void WriteArray(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) + { + if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); } + + if (!(obj is Array array)) { throw new InvalidOperationException($"Object in {nameof(WriteArray)} was {obj.GetType()} but expected {nameof(Array)}"); } + + msg.WriteRangedInteger(array.Length, 0, attribute.ArrayMaxSize); + + foreach (dynamic o in array) + { + if (TryFindBehavior(o.GetType(), out ReadWriteBehavior behavior)) + { + behavior.WriteAction(o, attribute, msg); + } + } + } + + private static dynamic ReadBoolean(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadBoolean(); + + private static dynamic ReadByte(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadByte(); + + private static dynamic ReadUInt16(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadUInt16(); + + private static dynamic ReadInt16(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadInt16(); + + private static dynamic ReadUInt32(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadUInt32(); + + private static dynamic ReadInt32(IReadMessage inc, Type type, NetworkSerialize attribute) + { + if (IsRanged(attribute.MinValueInt, attribute.MaxValueInt)) + { + return inc.ReadRangedInteger(attribute.MinValueInt, attribute.MaxValueInt); + } + + return inc.ReadInt32(); + } + + private static void WriteInt32(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) + { + if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); } + + if (IsRanged(attribute.MinValueInt, attribute.MaxValueInt)) + { + msg.WriteRangedInteger(obj, attribute.MinValueInt, attribute.MaxValueInt); + return; + } + + msg.Write(obj); + } + + private static dynamic ReadUInt64(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadUInt64(); + + private static dynamic ReadInt64(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadInt64(); + + private static dynamic ReadSingle(IReadMessage inc, Type type, NetworkSerialize attribute) + { + if (IsRanged(attribute.MinValueFloat, attribute.MaxValueFloat)) + { + return inc.ReadRangedSingle(attribute.MinValueFloat, attribute.MaxValueFloat, attribute.NumberOfBits); + } + + return inc.ReadSingle(); + } + + private static void WriteSingle(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) + { + if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); } + + if (IsRanged(attribute.MinValueFloat, attribute.MaxValueFloat)) + { + msg.WriteRangedSingle(obj, attribute.MinValueFloat, attribute.MaxValueFloat, attribute.NumberOfBits); + return; + } + + msg.Write(obj); + } + + 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 ReadColor(IReadMessage inc, Type type, NetworkSerialize attribute) => attribute.IncludeColorAlpha ? inc.ReadColorR8G8B8A8() : inc.ReadColorR8G8B8(); + + private static void WriteColor(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) + { + if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); } + + if (attribute.IncludeColorAlpha) + { + msg.WriteColorR8G8B8A8(obj); + return; + } + + msg.WriteColorR8G8B8(obj); + } + + private static dynamic ReadVector2(IReadMessage inc, Type type, NetworkSerialize attribute) + { + float x; + float y; + + if (IsRanged(attribute.MinValueFloat, attribute.MaxValueFloat)) + { + x = inc.ReadRangedSingle(attribute.MinValueFloat, attribute.MaxValueFloat, attribute.NumberOfBits); + y = inc.ReadRangedSingle(attribute.MinValueFloat, attribute.MaxValueFloat, attribute.NumberOfBits); + } + else + { + x = inc.ReadSingle(); + y = inc.ReadSingle(); + } + + return new Vector2(x, y); + } + + private static void WriteVector2(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) + { + if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); } + + var (x, y) = (Vector2)obj; + if (IsRanged(attribute.MinValueFloat, attribute.MaxValueFloat)) + { + msg.WriteRangedSingle(x, attribute.MinValueFloat, attribute.MaxValueFloat, attribute.NumberOfBits); + msg.WriteRangedSingle(y, attribute.MinValueFloat, attribute.MaxValueFloat, attribute.NumberOfBits); + return; + } + + msg.Write(x); + msg.Write(y); + } + + private static bool IsRanged(float minValue, float maxValue) => minValue > float.MinValue || maxValue < float.MaxValue; + private static bool IsRanged(int minValue, int maxValue) => minValue > int.MinValue || maxValue < int.MaxValue; + + private static Range GetEnumRange(Type type) + { + ImmutableArray values = Enum.GetValues(type).Cast().ToImmutableArray(); + return new Range(values.Min(), values.Max()); + } + + public static bool TryFindBehavior(Type type, out ReadWriteBehavior behavior) + { + if (TypeBehaviors.TryGetValue(type, out behavior)) { return true; } + + foreach (var (predicate, behavior2) in TypePredicates) + { + if (predicate(type)) + { + behavior = behavior2; + return true; + } + } + + behavior = InvalidReadWriteBehavior; + return false; + } + } + + /// + /// Interface that allows the creation of automatically serializable and deserializable structs. + ///

+ ///
+ /// + /// + /// public enum PurchaseResult + /// { + /// Unknown, + /// Completed, + /// Declined + /// } + /// + /// [NetworkSerialize] + /// struct NetStoreTransaction : INetSerializableStruct + /// { + /// public long Timestamp { get; set; } + /// public PurchaseResult Result { get; set; } + /// public NetPurchasedItem? PurchasedItem { get; set; } + /// } + /// + /// [NetworkSerialize] + /// struct NetPurchasedItem : INetSerializableStruct + /// { + /// public string Identifier; + /// public string[] Tags; + /// public int Amount; + /// } + /// + /// + /// + /// Supported types are:
+ /// bool
+ /// byte
+ /// ushort
+ /// short
+ /// uint
+ /// int
+ /// ulong
+ /// long
+ /// float
+ /// double
+ /// string
+ ///
+ ///
+ /// In addition arrays, enums and are supported.
+ /// Using will make the field or property optional + ///
+ /// + public interface INetSerializableStruct + { + /// + /// Deserializes a network message into a struct. + /// + /// + /// + /// public void ClientRead(IReadMessage inc) + /// { + /// NetStoreTransaction transaction = INetSerializableStruct.Read<NetStoreTransaction>(inc); + /// if (transaction.Result == PurchaseResult.Declined) + /// { + /// Console.WriteLine("Purchase declined!"); + /// return; + /// } + /// + /// if (transaction.PurchasedItem is { } item) + /// { + /// // Purchased 3x Wrench with tags: smallitem, mechanical, tool + /// Console.WriteLine($"Purchased {item.Amount}x {item.Identifier} with tags: {string.Join(", ", item.Tags)}"); + /// } + /// } + /// + /// + /// Incoming network message + /// Type of the struct that implements + /// A new struct of type T with fields and properties deserialized + public static T Read(IReadMessage inc) where T : INetSerializableStruct => (T)ReadDynamic(typeof(T), inc); + + public static dynamic ReadDynamic(Type type, IReadMessage inc) + { + object? newObject = Activator.CreateInstance(type); + if (newObject is null) { return default!; } + + PropertyInfo[] properties = type.GetProperties(); + foreach (PropertyInfo info 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}!"); + } + } + + return newObject; + } + + /// + /// Serializes the struct into a network message + /// + /// + /// public void ServerWrite(IWriteMessage msg) + /// { + /// INetSerializableStruct transaction = new NetStoreTransaction + /// { + /// Result = PurchaseResult.Completed, + /// Timestamp = DateTimeOffset.Now.ToUnixTimeSeconds(), + /// PurchasedItem = new NetPurchasedItem + /// { + /// Identifier = "Wrench", + /// Amount = 3, + /// Tags = new []{ "smallitem", "mechanical", "tool" } + /// } + /// }; + /// + /// transaction.Write(msg); + /// } + /// + /// + /// + /// Outgoing network message + public void Write(IWriteMessage msg) + { + PropertyInfo[] properties = GetType().GetProperties(); + foreach (PropertyInfo info 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}"); + } + } + } + + private static NetworkSerialize? GetAttribute(MemberInfo info, object baseClass) => info.GetCustomAttribute() ?? baseClass.GetType().GetCustomAttribute(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index cfd0e8039..6f7e45888 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -29,7 +29,8 @@ namespace Barotrauma.Networking REQUEST_STARTGAMEFINALIZE, //tell the server you're ready to finalize round initialization ERROR, //tell the server that an error occurred - CREW, + CREW, //hiring UI + MEDICAL, //medical clinic READY_CHECK, READY_TO_SPAWN, LUA_NET_MESSAGE @@ -81,6 +82,8 @@ namespace Barotrauma.Networking EVENTACTION, CREW, //anything related to managing bots in multiplayer READY_CHECK, //start, end and update a ready check + MEDICAL, //medical clinic + LUA_NET_MESSAGE } enum ServerNetObject @@ -145,7 +148,9 @@ namespace Barotrauma.Networking NotOnWhitelist, ExcessiveDesyncOldEvent, ExcessiveDesyncRemovedEvent, - SyncTimeout + SyncTimeout, + SteamP2PError, + SteamP2PTimeOut, } abstract partial class NetworkMember @@ -231,24 +236,24 @@ namespace Barotrauma.Networking var radio = sender.Inventory.AllItems.FirstOrDefault(i => i.GetComponent() != null); if (radio == null || !sender.HasEquippedItem(radio)) { return false; } - + var radioComponent = radio.GetComponent(); if (radioComponent == null) { return false; } return radioComponent.HasRequiredContainedItems(sender, addMessage: false); } - public void AddChatMessage(string message, ChatMessageType type, string senderName = "", Client senderClient = null, Character senderCharacter = null, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None) + public void AddChatMessage(string message, ChatMessageType type, string senderName = "", Client senderClient = null, Character senderCharacter = null, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None, Color? textColor = null) { - AddChatMessage(ChatMessage.Create(senderName, message, type, senderCharacter, senderClient, changeType: changeType)); + AddChatMessage(ChatMessage.Create(senderName, message, type, senderCharacter, senderClient, changeType: changeType, textColor: textColor)); } public virtual void AddChatMessage(ChatMessage message) { if (string.IsNullOrEmpty(message.Text)) { return; } - + if (message.Sender != null && !message.Sender.IsDead) { - message.Sender.ShowSpeechBubble(2.0f, ChatMessage.MessageColor[(int)message.Type]); + message.Sender.ShowSpeechBubble(2.0f, message.Color); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs index dc79afcd9..81170bc0c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs @@ -23,18 +23,22 @@ namespace Barotrauma.Networking ///
public int? WallSectionIndex { get; set; } + public bool IsNewOrder { get; } + /// - /// Same as calling , but the text parameter is set using + /// Same as calling , + /// but the text parameter is set using /// - public OrderChatMessage(Order order, string orderOption, int priority, ISpatialEntity targetEntity, Character targetCharacter, Character sender) + public OrderChatMessage(Order order, string orderOption, int priority, ISpatialEntity targetEntity, Character targetCharacter, Character sender, bool isNewOrder = true) : this(order, orderOption, priority, - order?.GetChatMessage(targetCharacter?.Name, sender?.CurrentHull?.DisplayName, givingOrderToSelf: targetCharacter == sender, orderOption: orderOption, priority: priority), - targetEntity, targetCharacter, sender) + order?.GetChatMessage(targetCharacter?.Name, sender?.CurrentHull?.DisplayName, targetCharacter == sender, orderOption, isNewOrder), + targetEntity, targetCharacter, sender, isNewOrder) { } - public OrderChatMessage(Order order, string orderOption, int priority, string text, ISpatialEntity targetEntity, Character targetCharacter, Character sender) + public OrderChatMessage(Order order, string orderOption, int priority, string text, ISpatialEntity targetEntity, + Character targetCharacter, Character sender, bool isNewOrder = true) : base(sender?.Name, text, ChatMessageType.Order, sender, GameMain.NetworkMember.ConnectedClients.Find(c => c.Character == sender)) { Order = order; @@ -42,9 +46,11 @@ namespace Barotrauma.Networking OrderPriority = priority; TargetCharacter = targetCharacter; TargetEntity = targetEntity; + IsNewOrder = isNewOrder; } - public static void WriteOrder(IWriteMessage msg, Order order, Character targetCharacter, ISpatialEntity targetEntity, string orderOption, int orderPriority, int? wallSectionIndex) + public static void WriteOrder(IWriteMessage msg, Order order, Character targetCharacter, ISpatialEntity targetEntity, + string orderOption, int orderPriority, int? wallSectionIndex, bool isNewOrder) { msg.Write((byte)Order.PrefabList.IndexOf(order.Prefab)); msg.Write(targetCharacter == null ? (UInt16)0 : targetCharacter.ID); @@ -100,11 +106,13 @@ namespace Barotrauma.Networking msg.Write((byte)(wallSectionIndex ?? order.WallSectionIndex ?? 0)); } } + + msg.Write(isNewOrder); } private void WriteOrder(IWriteMessage msg) { - WriteOrder(msg, Order, TargetCharacter, TargetEntity, OrderOption, OrderPriority, WallSectionIndex); + WriteOrder(msg, Order, TargetCharacter, TargetEntity, OrderOption, OrderPriority, WallSectionIndex, IsNewOrder); } public struct OrderMessageInfo @@ -119,8 +127,10 @@ namespace Barotrauma.Networking public OrderTarget TargetPosition { get; } public int? WallSectionIndex { get; } public int Priority { get; } + public bool IsNewOrder { get; } - public OrderMessageInfo(int orderIndex, Order orderPrefab, string orderOption, int? orderOptionIndex, Character targetCharacter, Order.OrderTargetType targetType, Entity targetEntity, OrderTarget targetPosition, int? wallSectionIndex, int orderPriority) + public OrderMessageInfo(int orderIndex, Order orderPrefab, string orderOption, int? orderOptionIndex, Character targetCharacter, + Order.OrderTargetType targetType, Entity targetEntity, OrderTarget targetPosition, int? wallSectionIndex, int orderPriority, bool isNewOrder) { OrderIndex = orderIndex; OrderPrefab = orderPrefab; @@ -132,6 +142,7 @@ namespace Barotrauma.Networking TargetPosition = targetPosition; WallSectionIndex = wallSectionIndex; Priority = orderPriority; + IsNewOrder = isNewOrder; } } @@ -205,7 +216,10 @@ namespace Barotrauma.Networking wallSectionIndex = msg.ReadByte(); } - return new OrderMessageInfo(orderIndex, orderPrefab, orderOption, optionIndex, targetCharacter, orderTargetType, targetEntity, orderTargetPosition, wallSectionIndex, orderPriority); + bool isNewOrder = msg.ReadBoolean(); + + return new OrderMessageInfo(orderIndex, orderPrefab, orderOption, optionIndex, targetCharacter, + orderTargetType, targetEntity, orderTargetPosition, wallSectionIndex, orderPriority, isNewOrder); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Enums.cs index e3ccaab6c..8f8e62d55 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Enums.cs @@ -38,19 +38,19 @@ namespace Barotrauma.Networking public static class NetworkEnumExtensions { public static bool IsCompressed(this PacketHeader h) - => h.IsBitSet(PacketHeader.IsCompressed); + => h.HasFlag(PacketHeader.IsCompressed); public static bool IsConnectionInitializationStep(this PacketHeader h) - => h.IsBitSet(PacketHeader.IsConnectionInitializationStep); + => h.HasFlag(PacketHeader.IsConnectionInitializationStep); public static bool IsDisconnectMessage(this PacketHeader h) - => h.IsBitSet(PacketHeader.IsDisconnectMessage); + => h.HasFlag(PacketHeader.IsDisconnectMessage); public static bool IsServerMessage(this PacketHeader h) - => h.IsBitSet(PacketHeader.IsServerMessage); + => h.HasFlag(PacketHeader.IsServerMessage); public static bool IsHeartbeatMessage(this PacketHeader h) - => h.IsBitSet(PacketHeader.IsHeartbeatMessage); + => h.HasFlag(PacketHeader.IsHeartbeatMessage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs index 8d27d37ac..8367fdce9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs @@ -81,8 +81,7 @@ namespace Barotrauma.Networking public const string SavePath = "ServerLogs"; private readonly Queue lines; - - private int unsavedLineCount; + private readonly Queue unsavedLines; private readonly bool[] msgTypeHidden = new bool[Enum.GetValues(typeof(MessageType)).Length]; @@ -98,6 +97,7 @@ namespace Barotrauma.Networking { ServerName = serverName; lines = new Queue(); + unsavedLines = new Queue(); foreach (MessageType messageType in Enum.GetValues(typeof(MessageType))) { @@ -117,22 +117,19 @@ namespace Barotrauma.Networking #endif lines.Enqueue(newText); + unsavedLines.Enqueue(newText); #if CLIENT if (listBox != null) { AddLine(newText); - listBox.UpdateScrollBarSize(); } #endif - - unsavedLineCount++; - - if (unsavedLineCount >= LinesPerFile) + if (unsavedLines.Count() >= LinesPerFile) { Save(); - unsavedLineCount = 0; + unsavedLines.Clear(); } while (lines.Count > LinesPerFile) @@ -176,7 +173,7 @@ namespace Barotrauma.Networking try { - File.WriteAllLines(filePath, lines.Select(l => l.SanitizedText)); + File.WriteAllLines(filePath, unsavedLines.Select(l => l.SanitizedText)); } catch (Exception e) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index 8bb1e4f26..2b6686c4f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -97,7 +97,7 @@ namespace Barotrauma.Networking private readonly SerializableProperty property; private readonly string typeString; private readonly object parentObject; - + public string Name { get { return property.Name; } @@ -214,7 +214,7 @@ namespace Barotrauma.Networking public void Write(IWriteMessage msg, object overrideValue = null) { - if (overrideValue == null) overrideValue = property.GetValue(parentObject); + if (overrideValue == null) { overrideValue = Value; } switch (typeString) { case "float": @@ -315,6 +315,7 @@ namespace Barotrauma.Networking { NetPropertyData netPropertyData = new NetPropertyData(this, property, typeName); UInt32 key = ToolBox.StringToUInt32Hash(property.Name, md5); + if (key == 0) { key++; } //0 is reserved to indicate the end of the netproperties section of a message if (netProperties.ContainsKey(key)){ throw new Exception("Hashing collision in ServerSettings.netProperties: " + netProperties[key] + " has same key as " + property.Name + " (" + key.ToString() + ")"); } netProperties.Add(key, netPropertyData); } @@ -720,13 +721,6 @@ namespace Barotrauma.Networking set; } - [Serialize("", true)] - public string CampaignSubmarines - { - get; - set; - } - private YesNoMaybe traitorsEnabled; [Serialize(YesNoMaybe.No, true)] public YesNoMaybe TraitorsEnabled @@ -1070,7 +1064,6 @@ namespace Barotrauma.Networking } #if SERVER - MultiPlayerCampaign.UpdateCampaignSubs(); SelectNonHiddenSubmarine(); #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs index 98e6022d1..347af34f7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs @@ -124,7 +124,6 @@ namespace Barotrauma.Steam return unlocked; } - public static bool IncrementStat(string statName, int increment) { if (!isInitialized || !Steamworks.SteamClient.IsValid) { return false; } @@ -136,6 +135,10 @@ namespace Barotrauma.Steam DebugConsole.NewMessage("Failed to increment stat \"" + statName + "\"."); #endif } + else + { + StoreStats(); + } return success; } @@ -150,9 +153,19 @@ namespace Barotrauma.Steam DebugConsole.NewMessage("Failed to increment stat \"" + statName + "\"."); #endif } + else + { + StoreStats(); + } return success; } + public static int GetStatInt(string statName) + { + if (!isInitialized || !Steamworks.SteamClient.IsValid) { return 0; } + return Steamworks.SteamUserStats.GetStatInt(statName); + } + public static bool StoreStats() { if (!isInitialized || !Steamworks.SteamClient.IsValid) { return false; } @@ -167,6 +180,17 @@ namespace Barotrauma.Steam return success; } + public static bool TryGetUnlockedAchievements(out List achievements) + { + if (!isInitialized || !Steamworks.SteamClient.IsValid) + { + achievements = null; + return false; + } + achievements = Steamworks.SteamUserStats.Achievements.Where(a => a.State).ToList(); + return true; + } + public static void Update(float deltaTime) { if (!isInitialized) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/FrameCounter.cs b/Barotrauma/BarotraumaShared/SharedSource/PerformanceCounter.cs similarity index 68% rename from Barotrauma/BarotraumaShared/SharedSource/FrameCounter.cs rename to Barotrauma/BarotraumaShared/SharedSource/PerformanceCounter.cs index 0b4d5c016..6915d1dfc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/FrameCounter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/PerformanceCounter.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -7,20 +6,18 @@ namespace Barotrauma { public class PerformanceCounter { - public long TotalFrames { get; private set; } - public double TotalSeconds { get; private set; } public double AverageFramesPerSecond { get; private set; } public double CurrentFramesPerSecond { get; private set; } public const int MaximumSamples = 10; - private Queue sampleBuffer = new Queue(); + private readonly Queue sampleBuffer = new Queue(); - private Dictionary> elapsedTicks = new Dictionary>(); - private Dictionary avgTicksPerFrame = new Dictionary(); + private readonly Dictionary> elapsedTicks = new Dictionary>(); + private readonly Dictionary avgTicksPerFrame = new Dictionary(); #if CLIENT - internal Graph UpdateTimeGraph = new Graph(500), UpdateIterationsGraph = new Graph(500), DrawTimeGraph = new Graph(500); + internal Graph UpdateTimeGraph = new Graph(500), DrawTimeGraph = new Graph(500); #endif public IEnumerable GetSavedIdentifiers @@ -50,7 +47,7 @@ namespace Barotrauma { if (deltaTime == 0.0f) { return false; } - CurrentFramesPerSecond = (1.0 / deltaTime); + CurrentFramesPerSecond = 1.0 / deltaTime; sampleBuffer.Enqueue(CurrentFramesPerSecond); @@ -64,12 +61,7 @@ namespace Barotrauma AverageFramesPerSecond = CurrentFramesPerSecond; } - if (AverageFramesPerSecond < 0 || AverageFramesPerSecond > 500) { } - - TotalFrames++; - TotalSeconds += deltaTime; return true; } - } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index 5b4fd2ac9..8baf4ad07 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -88,6 +88,7 @@ namespace Barotrauma Circle, Rectangle, Capsule, HorizontalCapsule }; + public const float MinDensity = 0.01f; public const float DefaultAngularDamping = 5.0f; private static readonly List list = new List(); @@ -314,14 +315,33 @@ namespace Barotrauma set { FarseerBody.BodyType = value; } } + private Category _collisionCategories; + public Category CollisionCategories { - set { FarseerBody.CollisionCategories = value; } + set + { + _collisionCategories = value; + FarseerBody.CollisionCategories = value; + } + get + { + return _collisionCategories; + } } + private Category _collidesWith; public Category CollidesWith { - set { FarseerBody.CollidesWith = value; } + set + { + _collidesWith = value; + FarseerBody.CollidesWith = value; + } + get + { + return _collidesWith; + } } public PhysicsBody(XElement element, float scale = 1.0f) : this(element, Vector2.Zero, scale) { } @@ -330,6 +350,7 @@ namespace Barotrauma public PhysicsBody(float width, float height, float radius, float density) { + density = Math.Max(density, MinDensity); CreateBody(width, height, radius, density); LastSentPosition = FarseerBody.Position; list.Add(this); @@ -367,7 +388,7 @@ namespace Barotrauma float radius = ConvertUnits.ToSimUnits(limbParams.Radius) * limbParams.Scale * limbParams.Ragdoll.LimbScale; float height = ConvertUnits.ToSimUnits(limbParams.Height) * limbParams.Scale * limbParams.Ragdoll.LimbScale; float width = ConvertUnits.ToSimUnits(limbParams.Width) * limbParams.Scale * limbParams.Ragdoll.LimbScale; - density = limbParams.Density; + density = Math.Max(limbParams.Density, MinDensity); CreateBody(width, height, radius, density); FarseerBody.BodyType = BodyType.Dynamic; FarseerBody.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel; @@ -381,18 +402,18 @@ namespace Barotrauma list.Add(this); } - public PhysicsBody(XElement element, Vector2 position, float scale = 1.0f) + public PhysicsBody(XElement element, Vector2 position, float scale = 1.0f, float? forceDensity = null) { float radius = ConvertUnits.ToSimUnits(element.GetAttributeFloat("radius", 0.0f)) * scale; float height = ConvertUnits.ToSimUnits(element.GetAttributeFloat("height", 0.0f)) * scale; float width = ConvertUnits.ToSimUnits(element.GetAttributeFloat("width", 0.0f)) * scale; - density = element.GetAttributeFloat("density", 10.0f); + density = Math.Max(forceDensity ?? element.GetAttributeFloat("density", 10.0f), MinDensity); CreateBody(width, height, radius, density); Enum.TryParse(element.GetAttributeString("bodytype", "Dynamic"), out BodyType bodyType); FarseerBody.BodyType = bodyType; FarseerBody.CollisionCategories = Physics.CollisionItem; FarseerBody.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform; - FarseerBody.Friction = element.GetAttributeFloat("friction", 0.3f); + FarseerBody.Friction = element.GetAttributeFloat("friction", 0.5f); FarseerBody.Restitution = element.GetAttributeFloat("restitution", 0.05f); FarseerBody.UserData = this; SetTransformIgnoreContacts(position, 0.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs b/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs index 8c461594f..62b8c8056 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs @@ -53,6 +53,7 @@ using Barotrauma; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; using System.Linq; @@ -157,6 +158,11 @@ namespace Voronoi2 public bool IsDestructible; public bool DoesDamage; + /// + /// Executed when the cell is destroyed (only applies to destructible level walls) + /// + public Action OnDestroyed; + public Vector2 Center { get { return new Vector2((float)Site.Coord.X, (float)Site.Coord.Y) + Translation; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs index 77bdc67bc..1f45c807a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs @@ -244,6 +244,8 @@ namespace Barotrauma } cam.MoveCamera((float)deltaTime, allowZoom: GUI.MouseOn == null && !Inventory.IsMouseOnInventory); + + Character.Controlled?.UpdateLocalCursor(cam); #endif foreach (Submarine sub in Submarine.Loaded) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index 1f4ff6489..f5cf90bc5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs @@ -228,13 +228,13 @@ namespace Barotrauma { case "bool": bool boolValue = value == "true" || value == "True"; - if (TrySetValueWithoutReflection(parentObject, boolValue)) { return true; } + if (TrySetBoolValueWithoutReflection(parentObject, boolValue)) { return true; } PropertyInfo.SetValue(parentObject, boolValue, null); break; case "int": if (int.TryParse(value, out int intVal)) { - if (TrySetValueWithoutReflection(parentObject, intVal)) { return true; } + if (TrySetFloatValueWithoutReflection(parentObject, intVal)) { return true; } PropertyInfo.SetValue(parentObject, intVal, null); } else @@ -245,7 +245,7 @@ namespace Barotrauma case "float": if (float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out float floatVal)) { - if (TrySetValueWithoutReflection(parentObject, floatVal)) { return true; } + if (TrySetFloatValueWithoutReflection(parentObject, floatVal)) { return true; } PropertyInfo.SetValue(parentObject, floatVal, null); } else @@ -387,7 +387,7 @@ namespace Barotrauma { try { - if (TrySetValueWithoutReflection(parentObject, value)) { return true; } + if (TrySetFloatValueWithoutReflection(parentObject, value)) { return true; } PropertyInfo.SetValue(parentObject, value, null); } catch (TargetInvocationException e) @@ -408,7 +408,7 @@ namespace Barotrauma { try { - if (TrySetValueWithoutReflection(parentObject, value)) { return true; } + if (TrySetBoolValueWithoutReflection(parentObject, value)) { return true; } PropertyInfo.SetValue(parentObject, value, null); } catch (TargetInvocationException e) @@ -428,6 +428,7 @@ namespace Barotrauma { try { + if (TrySetFloatValueWithoutReflection(parentObject, value)) { return true; } PropertyInfo.SetValue(parentObject, value, null); } catch (TargetInvocationException e) @@ -466,6 +467,56 @@ namespace Barotrauma } } + public float GetFloatValue(object parentObject) + { + if (parentObject == null || PropertyInfo == null) { return 0.0f; } + + if (TryGetFloatValueWithoutReflection(parentObject, out float value)) + { + return value; + } + + try + { + return (float)PropertyInfo.GetValue(parentObject, null); + } + catch (TargetInvocationException e) + { + DebugConsole.ThrowError("Exception thrown by the target of SerializableProperty.GetValue", e.InnerException); + return 0.0f; + } + catch (Exception e) + { + DebugConsole.ThrowError("Error in SerializableProperty.GetValue", e); + return 0.0f; + } + } + + public bool GetBoolValue(object parentObject) + { + if (parentObject == null || PropertyInfo == null) { return false; } + + if (TryGetBoolValueWithoutReflection(parentObject, out bool value)) + { + return value; + } + + try + { + return (bool)PropertyInfo.GetValue(parentObject, null); + } + catch (TargetInvocationException e) + { + DebugConsole.ThrowError("Exception thrown by the target of SerializableProperty.GetValue", e.InnerException); + return false; + } + catch (Exception e) + { + DebugConsole.ThrowError("Error in SerializableProperty.GetValue", e); + return false; + } + } + public static string GetSupportedTypeName(Type type) { if (type.IsEnum) return "Enum"; @@ -481,125 +532,221 @@ namespace Barotrauma ///
private object TryGetValueWithoutReflection(object parentObject) { + if (PropertyType == typeof(float)) + { + if (TryGetFloatValueWithoutReflection(parentObject, out float value)) { return value; } + } + else if (PropertyType == typeof(bool)) + { + if (TryGetBoolValueWithoutReflection(parentObject, out bool value)) { return value; } + } + else if (PropertyType == typeof(string)) + { + if (TryGetStringValueWithoutReflection(parentObject, out string value)) { return value; } + } + return null; + } + + /// + /// Try getting the values of some commonly used properties directly without reflection + /// + private bool TryGetFloatValueWithoutReflection(object parentObject, out float value) + { + value = 0.0f; switch (Name) { - case "Voltage": - if (parentObject is Powered powered) { return powered.Voltage; } - break; - case "Charge": - if (parentObject is PowerContainer powerContainer) { return powerContainer.Charge; } - break; - case "Overload": - if (parentObject is PowerTransfer powerTransfer) { return powerTransfer.Overload; } - break; - case "AvailableFuel": - { if (parentObject is Reactor reactor) { return reactor.AvailableFuel; } } - break; - case "FissionRate": - { if (parentObject is Reactor reactor) { return reactor.FissionRate; } } - break; - case "OxygenFlow": - if (parentObject is Vent vent) { return vent.OxygenFlow; } - break; - case "CurrFlow": - if (parentObject is Pump pump) { return pump.CurrFlow; } - if (parentObject is OxygenGenerator oxygenGenerator) { return oxygenGenerator.CurrFlow; } - break; - case "CurrentVolume": - if (parentObject is Engine engine) { return engine.CurrentVolume; } - break; - case "MotionDetected": - if (parentObject is MotionSensor motionSensor) { return motionSensor.MotionDetected; } - break; - case "Oxygen": - { if (parentObject is Character character) { return character.Oxygen; } } - break; - case "Health": - { if (parentObject is Character character) { return character.Health; } } - break; - case "OxygenAvailable": - { if (parentObject is Character character) { return character.OxygenAvailable; } } - break; - case "PressureProtection": - { if (parentObject is Character character) { return character.PressureProtection; } } - break; - case "IsDead": - { if (parentObject is Character character) { return character.IsDead; } } - break; - case "IsHuman": - { if (parentObject is Character character) { return character.IsHuman; } } - break; - case "IsOn": - { if (parentObject is LightComponent lightComponent) { return lightComponent.IsOn; } } - break; - case "Condition": + case nameof(Powered.Voltage): { - if (parentObject is Item item) { return item.Condition; } + if (parentObject is Powered powered) { value = powered.Voltage; return true; } } break; - case "ContainerIdentifier": + case nameof(Powered.CurrPowerConsumption): { - if (parentObject is Item item) { return item.ContainerIdentifier; } + if (parentObject is Powered powered) { value = powered.CurrPowerConsumption; return true; } } break; - case "PhysicsBodyActive": + case nameof(PowerContainer.Charge): { - if (parentObject is Item item) { return item.PhysicsBodyActive; } + if (parentObject is PowerContainer powerContainer) { value = powerContainer.Charge; return true; } + } + break; + case nameof(PowerContainer.ChargePercentage): + { + if (parentObject is PowerContainer powerContainer) { value = powerContainer.ChargePercentage; return true; } + } + break; + case nameof(PowerContainer.RechargeRatio): + { + if (parentObject is PowerContainer powerContainer) { value = powerContainer.RechargeRatio; return true; } + } + break; + case nameof(Reactor.AvailableFuel): + { if (parentObject is Reactor reactor) { value = reactor.AvailableFuel; return true; } } + break; + case nameof(Reactor.FissionRate): + { if (parentObject is Reactor reactor) { value = reactor.FissionRate; return true; } } + break; + case nameof(Reactor.Temperature): + { if (parentObject is Reactor reactor) { value = reactor.Temperature; return true; } } + break; + case nameof(Vent.OxygenFlow): + if (parentObject is Vent vent) { value = vent.OxygenFlow; return true; } + break; + case nameof(Pump.CurrFlow): + { if (parentObject is Pump pump) { value = pump.CurrFlow; return true; } } + if (parentObject is OxygenGenerator oxygenGenerator) { value = oxygenGenerator.CurrFlow; return true; } + break; + case nameof(Engine.CurrentBrokenVolume): + { if (parentObject is Engine engine) { value = engine.CurrentBrokenVolume; return true; } } + { if (parentObject is Pump pump) { value = pump.CurrentBrokenVolume; return true; } } + break; + case nameof(Engine.CurrentVolume): + { if (parentObject is Engine engine) { value = engine.CurrentVolume; return true; } } + break; + case nameof(Character.Oxygen): + { if (parentObject is Character character) { value = character.Oxygen; return true; } } + { if (parentObject is Hull hull) { value = hull.Oxygen; return true; } } + break; + case nameof(Character.Health): + { if (parentObject is Character character) { value = character.Health; return true; } } + break; + case nameof(Character.OxygenAvailable): + { if (parentObject is Character character) { value = character.OxygenAvailable; return true; } } + break; + case nameof(Character.PressureProtection): + { if (parentObject is Character character) { value = character.PressureProtection; return true; } } + break; + case nameof(Item.Condition): + { if (parentObject is Item item) { value = item.Condition; return true; } } + break; + case nameof(Character.SpeedMultiplier): + { if (parentObject is Character character) { value = character.SpeedMultiplier; return true; } } + break; + } + return false; + } + + /// + /// Try getting the values of some commonly used properties directly without reflection + /// + private bool TryGetBoolValueWithoutReflection(object parentObject, out bool value) + { + value = false; + switch (Name) + { + case nameof(ItemComponent.IsActive): + if (parentObject is ItemComponent ic) { value = ic.IsActive; return true; } + break; + case nameof(PowerTransfer.Overload): + if (parentObject is PowerTransfer powerTransfer) { value = powerTransfer.Overload; return true; } + break; + case nameof(MotionSensor.MotionDetected): + if (parentObject is MotionSensor motionSensor) { value = motionSensor.MotionDetected; return true; } + break; + case nameof(Character.IsDead): + { if (parentObject is Character character) { value = character.IsDead; return true; } } + break; + case nameof(Character.IsHuman): + { if (parentObject is Character character) { value = character.IsHuman; return true; } } + break; + case nameof(LightComponent.IsOn): + { if (parentObject is LightComponent lightComponent) { value = lightComponent.IsOn; return true; } } + break; + case nameof(Item.PhysicsBodyActive): + { + if (parentObject is Item item) { value = item.PhysicsBodyActive; return true; } + } + break; + case nameof(DockingPort.Docked): + if (parentObject is DockingPort dockingPort) { value = dockingPort.Docked; return true; } + break; + case nameof(Reactor.TemperatureCritical): + if (parentObject is Reactor reactor) { value = reactor.TemperatureCritical; return true; } + break; + case nameof(TriggerComponent.TriggerActive): + if (parentObject is TriggerComponent trigger) { value = trigger.TriggerActive; return true; } + break; + case nameof(Controller.State): + if (parentObject is Controller controller) { value = controller.State; return true; } + break; + } + return false; + } + + /// + /// Try getting the values of some commonly used properties directly without reflection + /// + private bool TryGetStringValueWithoutReflection(object parentObject, out string value) + { + value = null; + switch (Name) + { + case nameof(Item.ContainerIdentifier): + { + if (parentObject is Item item) { value = item.ContainerIdentifier; return true; } } break; } - - return null; + return false; } /// /// Try setting the values of some commonly used properties directly without reflection /// - private bool TrySetValueWithoutReflection(object parentObject, object value) + private bool TrySetFloatValueWithoutReflection(object parentObject, float value) { switch (Name) { - case "Condition": - if (parentObject is Item item && value is float) { item.Condition = (float)value; return true; } + case nameof(Item.Condition): + if (parentObject is Item item) { item.Condition = value; return true; } break; - case "Voltage": - if (parentObject is Powered powered && value is float) { powered.Voltage = (float)value; return true; } + case nameof(Powered.Voltage): + if (parentObject is Powered powered) { powered.Voltage = value; return true; } break; - case "Charge": - if (parentObject is PowerContainer powerContainer && value is float) { powerContainer.Charge = (float)value; return true; } + case nameof(PowerContainer.Charge): + if (parentObject is PowerContainer powerContainer) { powerContainer.Charge = value; return true; } break; - case "AvailableFuel": - if (parentObject is Reactor reactor && value is float) { reactor.AvailableFuel = (float)value; return true; } + case nameof(Reactor.AvailableFuel): + if (parentObject is Reactor reactor) { reactor.AvailableFuel = value; return true; } break; - case "Oxygen": - { if (parentObject is Character character && value is float) { character.Oxygen = (float)value; return true; } } + case nameof(Character.Oxygen): + { if (parentObject is Character character) { character.Oxygen = value; return true; } } break; - case "HideFace": - { if (parentObject is Character character && value is bool) { character.HideFace = (bool)value; return true; } } + case nameof(Character.OxygenAvailable): + { if (parentObject is Character character) { character.OxygenAvailable = value; return true; } } break; - case "OxygenAvailable": - { if (parentObject is Character character && value is float) { character.OxygenAvailable = (float)value; return true; } } + case nameof(Character.PressureProtection): + { if (parentObject is Character character) { character.PressureProtection = value; return true; } } break; - case "ObstructVision": - { if (parentObject is Character character && value is bool) { character.ObstructVision = (bool)value; return true; } } + case nameof(Character.LowPassMultiplier): + { if (parentObject is Character character) { character.LowPassMultiplier = value; return true; } } break; - case "PressureProtection": - { if (parentObject is Character character && value is float) { character.PressureProtection = (float)value; return true; } } + case nameof(Character.SpeedMultiplier): + { if (parentObject is Character character) { character.StackSpeedMultiplier(value); return true; } } break; - case "LowPassMultiplier": - { if (parentObject is Character character && value is float) { character.LowPassMultiplier = (float)value; return true; } } - break; - case "SpeedMultiplier": - { if (parentObject is Character character && value is float) { character.StackSpeedMultiplier((float)value); return true; } } - break; - case "HealthMultiplier": - { if (parentObject is Character character && value is float) { character.StackHealthMultiplier((float)value); return true; } } - break; - case "IsOn": - { if (parentObject is LightComponent lightComponent && value is bool) { lightComponent.IsOn = (bool)value; return true; } } + case nameof(Character.HealthMultiplier): + { if (parentObject is Character character) { character.StackHealthMultiplier(value); return true; } } + break; + } + return false; + } + /// + /// Try setting the values of some commonly used properties directly without reflection + /// + private bool TrySetBoolValueWithoutReflection(object parentObject, bool value) + { + switch (Name) + { + case nameof(Character.ObstructVision): + { if (parentObject is Character character) { character.ObstructVision = value; return true; } } + break; + case nameof(LightComponent.IsOn): + { if (parentObject is LightComponent lightComponent) { lightComponent.IsOn = value; return true; } } + break; + case nameof(ItemComponent.IsActive): + { if (parentObject is ItemComponent ic) { ic.IsActive = value; return true; } } break; } - return false; } @@ -782,6 +929,21 @@ namespace Barotrauma (int)structure.Prefab.ScaledSize.Y); } } + else if (entity is Item item) + { + if (!item.ResizeHorizontal) + { + item.Rect = item.DefaultRect = new Rectangle(item.Rect.X, item.Rect.Y, + (int)(item.Prefab.Size.X * item.Prefab.Scale), + item.Rect.Height); + } + if (!item.ResizeVertical) + { + item.Rect = item.DefaultRect = new Rectangle(item.Rect.X, item.Rect.Y, + item.Rect.Width, + (int)(item.Prefab.Size.Y * item.Prefab.Scale)); + } + } } if (entity.SerializableProperties.TryGetValue(attributeName, out SerializableProperty property)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index c5ee6f977..91e9b82a5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -262,6 +262,28 @@ namespace Barotrauma return val; } + public static double GetAttributeDouble(this XElement element, string name, double defaultValue) + { + if (element?.Attribute(name) == null) { return defaultValue; } + + double val = defaultValue; + try + { + string strVal = element.Attribute(name).Value; + if (strVal.LastOrDefault() == 'f') + { + strVal = strVal.Substring(0, strVal.Length - 1); + } + val = double.Parse(strVal, CultureInfo.InvariantCulture); + } + catch (Exception e) + { + DebugConsole.ThrowError("Error in " + element + "!", e); + } + + return val; + } + public static float[] GetAttributeFloatArray(this XElement element, string name, float[] defaultValue) { if (element?.Attribute(name) == null) { return defaultValue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs index db53a6db6..4cbd51d6c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs @@ -34,7 +34,7 @@ namespace Barotrauma } } - private static List> list = new List>(); + private readonly static List> list = new List>(); /// /// Reference to the xml element from where the sprite was created. Can be null if the sprite was not defined in xml! diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs index 7bd12a932..e6d051abf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs @@ -55,14 +55,23 @@ namespace Barotrauma public override void Apply(ActionType type, float deltaTime, Entity entity, ISerializableEntity target, Vector2? worldPosition = null) { if (this.type != type || !HasRequiredItems(entity)) { return; } - if (!Stackable && DelayList.Any(d => d.Parent == this && d.Targets.FirstOrDefault() == target)) { return; } + if (!Stackable) + { + foreach (var existingEffect in DelayList) + { + if (existingEffect.Parent == this && existingEffect.Targets.FirstOrDefault() == target) { return; } + } + } if (!IsValidTarget(target)) { return; } - if (!HasRequiredConditions(target.ToEnumerable())) { return; } + + currentTargets.Clear(); + currentTargets.Add(target); + if (!HasRequiredConditions(currentTargets)) { return; } switch (delayType) { case DelayTypes.Timer: - DelayList.Add(new DelayedListElement(this, entity, target.ToEnumerable(), delay, worldPosition, null)); + DelayList.Add(new DelayedListElement(this, entity, currentTargets, delay, worldPosition, null)); break; case DelayTypes.ReachCursor: Projectile projectile = (entity as Item)?.GetComponent(); @@ -78,16 +87,22 @@ namespace Barotrauma return; } - DelayList.Add(new DelayedListElement(this, entity, target.ToEnumerable(), Vector2.Distance(entity.WorldPosition, projectile.User.CursorWorldPosition), worldPosition, entity.WorldPosition)); + DelayList.Add(new DelayedListElement(this, entity, currentTargets, Vector2.Distance(entity.WorldPosition, projectile.User.CursorWorldPosition), worldPosition, entity.WorldPosition)); break; } } - public override void Apply(ActionType type, float deltaTime, Entity entity, IEnumerable targets, Vector2? worldPosition = null) + public override void Apply(ActionType type, float deltaTime, Entity entity, IReadOnlyList targets, Vector2? worldPosition = null) { if (this.type != type || !HasRequiredItems(entity)) { return; } - if (!Stackable && DelayList.Any(d => d.Parent == this && d.Targets.SequenceEqual(targets))) { return; } if (delayType == DelayTypes.ReachCursor && Character.Controlled == null) { return; } + if (!Stackable) + { + foreach (var existingEffect in DelayList) + { + if (existingEffect.Parent == this && existingEffect.Targets.SequenceEqual(targets)) { return; } + } + } currentTargets.Clear(); foreach (ISerializableEntity target in targets) diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs index 258bd5466..b8ca1a030 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs @@ -178,15 +178,28 @@ namespace Barotrauma { if (target is Item item) { - return item.ContainedItems.Any(it => Matches(it)); + foreach (var containedItem in item.ContainedItems) + { + if (Matches(containedItem)) { return true; } + } + return false; } else if (target is Items.Components.ItemComponent ic) { - return ic.Item.ContainedItems.Any(it => Matches(it)); + foreach (var containedItem in ic.Item.ContainedItems) + { + if (Matches(containedItem)) { return true; } + } + return false; } else if (target is Character character) { - return character.Inventory != null && character.Inventory.AllItems.Any(it => Matches(it)); + if (character.Inventory == null) { return false; } + foreach (var containedItem in character.Inventory.AllItems) + { + if (Matches(containedItem)) { return true; } + } + return false; } } @@ -204,56 +217,34 @@ namespace Barotrauma if (target == null) { return Operator == OperatorType.NotEquals; } return (Operator == OperatorType.Equals) == (target.Name == AttributeValue); case ConditionType.HasTag: + if (target == null) { return Operator == OperatorType.NotEquals; } + return MatchesTagCondition(target); + case ConditionType.HasStatusTag: + if (target == null) { return Operator == OperatorType.NotEquals; } + int matches = 0; + foreach (DurationListElement durationEffect in StatusEffect.DurationList) { - if (target == null) { return Operator == OperatorType.NotEquals; } - int matches = 0; + if (!durationEffect.Targets.Contains(target)) { continue; } foreach (string tag in SplitAttributeValue) { - if (target is Item item && item.HasTag(tag)) + if (durationEffect.Parent.HasTag(tag)) { matches++; } } - //If operator is == then it needs to match everything, otherwise if its != there must be zero matches. - return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; } - case ConditionType.HasStatusTag: - if (target == null) { return Operator == OperatorType.NotEquals; } - bool success = false; - if (StatusEffect.DurationList.Any(d => d.Targets.Contains(target)) || DelayedEffect.DelayList.Any(d => d.Targets.Contains(target))) + foreach (DelayedListElement delayedEffect in DelayedEffect.DelayList) { - int matches = 0; - foreach (DurationListElement durationEffect in StatusEffect.DurationList) + if (!delayedEffect.Targets.Contains(target)) { continue; } + foreach (string tag in SplitAttributeValue) { - if (!durationEffect.Targets.Contains(target)) { continue; } - foreach (string tag in SplitAttributeValue) + if (delayedEffect.Parent.HasTag(tag)) { - if (durationEffect.Parent.HasTag(tag)) - { - matches++; - } - } - success = Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; - } - foreach (DelayedListElement delayedEffect in DelayedEffect.DelayList) - { - if (!delayedEffect.Targets.Contains(target)) { continue; } - foreach (string tag in SplitAttributeValue) - { - if (delayedEffect.Parent.HasTag(tag)) - { - matches++; - } + matches++; } } - return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; } - else if (Operator == OperatorType.NotEquals) - { - //no status effects, so the tags cannot be equal -> condition met - return true; - } - return success; + return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; case ConditionType.SpeciesName: { if (target == null) { return Operator == OperatorType.NotEquals; } @@ -335,96 +326,87 @@ namespace Barotrauma return false; } } + + private bool MatchesTagCondition(ISerializableEntity target) + { + if (!(target is Item item)) { return Operator == OperatorType.NotEquals; } + + int matches = 0; + foreach (string tag in SplitAttributeValue) + { + if (item.HasTag(tag)) + { + matches++; + } + } + //If operator is == then it needs to match everything, otherwise if its != there must be zero matches. + return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; + } + + public bool MatchesTagCondition(string targetTag) + { + if (string.IsNullOrEmpty(targetTag) || Type != ConditionType.HasTag) { return false; } + + int matches = 0; + foreach (string tag in SplitAttributeValue) + { + if (targetTag.Equals(tag, StringComparison.OrdinalIgnoreCase)) + { + matches++; + } + } + //If operator is == then it needs to match everything, otherwise if its != there must be zero matches. + return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; + } // TODO: refactor and add tests private bool Matches(ISerializableEntity target, SerializableProperty property) { - object propertyValue = property.GetValue(target); + Type type = property.PropertyType; - if (propertyValue == null) + if (type == typeof(float) || type == typeof(int)) { - DebugConsole.ThrowError("Couldn't compare " + AttributeValue.ToString() + " (" + AttributeValue.GetType() + ") to property \"" + property.Name + "\" - property.GetValue() returns null!"); + float floatValue = property.GetFloatValue(target); + switch (Operator) + { + case OperatorType.Equals: + return MathUtils.NearlyEqual(floatValue, FloatValue.Value); + case OperatorType.NotEquals: + return !MathUtils.NearlyEqual(floatValue, FloatValue.Value); + case OperatorType.GreaterThan: + return floatValue > FloatValue.Value; + case OperatorType.LessThan: + return floatValue < FloatValue.Value; + case OperatorType.GreaterThanEquals: + return floatValue >= FloatValue.Value; + case OperatorType.LessThanEquals: + return floatValue <= FloatValue.Value; + } return false; } - Type type = propertyValue.GetType(); - float? floatProperty = null; - if (type == typeof(float) || type == typeof(int)) - { - floatProperty = (float)propertyValue; - } switch (Operator) { case OperatorType.Equals: if (type == typeof(bool)) { - return ((bool)propertyValue) == (AttributeValue == "true" || AttributeValue == "True"); - } - else if (FloatValue == null) - { - return propertyValue.ToString().Equals(AttributeValue); - } - else - { - return propertyValue.Equals(FloatValue); + return property.GetBoolValue(target) == (AttributeValue == "true" || AttributeValue == "True"); } + return property.GetValue(target).ToString().Equals(AttributeValue); + case OperatorType.NotEquals: if (type == typeof(bool)) { - return ((bool)propertyValue) != (AttributeValue == "true" || AttributeValue == "True"); - } - else if (FloatValue == null) - { - return !propertyValue.ToString().Equals(AttributeValue); - } - else - { - return !propertyValue.Equals(FloatValue); + return property.GetBoolValue(target) != (AttributeValue == "true" || AttributeValue == "True"); } + return !property.GetValue(target).ToString().Equals(AttributeValue); case OperatorType.GreaterThan: - if (FloatValue == null) - { - DebugConsole.ThrowError("Couldn't compare " + AttributeValue.ToString() + " (" + AttributeValue.GetType() + ") to property \"" + property.Name + "\" (" + type + ")! " - + "Make sure the type of the value set in the config files matches the type of the property."); - } - else if (floatProperty > FloatValue) - { - return true; - } - break; - case OperatorType.LessThan: - if (FloatValue == null) - { - DebugConsole.ThrowError("Couldn't compare " + AttributeValue.ToString() + " (" + AttributeValue.GetType() + ") to property \"" + property.Name + "\" (" + type + ")! " - + "Make sure the type of the value set in the config files matches the type of the property."); - } - else if (floatProperty < FloatValue) - { - return true; - } - break; - case OperatorType.GreaterThanEquals: - if (FloatValue == null) - { - DebugConsole.ThrowError("Couldn't compare " + AttributeValue.ToString() + " (" + AttributeValue.GetType() + ") to property \"" + property.Name + "\" (" + type + ")! " - + "Make sure the type of the value set in the config files matches the type of the property."); - } - else if (floatProperty >= FloatValue) - { - return true; - } - break; case OperatorType.LessThanEquals: - if (FloatValue == null) - { - DebugConsole.ThrowError("Couldn't compare " + AttributeValue.ToString() + " (" + AttributeValue.GetType() + ") to property \"" + property.Name + "\" (" + type + ")! " - + "Make sure the type of the value set in the config files matches the type of the property."); - } - else if (floatProperty <= FloatValue) - { - return true; - } + case OperatorType.LessThan: + case OperatorType.GreaterThanEquals: + DebugConsole.ThrowError("Couldn't compare " + AttributeValue.ToString() + " (" + AttributeValue.GetType() + ") to property \"" + property.Name + "\" (" + type + ")! " + + "Make sure the type of the value set in the config files matches the type of the property."); break; } return false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 8426c68ac..0bb5c11b6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -133,7 +133,8 @@ namespace Barotrauma Target, Limb, MainLimb, - Collider + Collider, + Random } public readonly ItemPrefab ItemPrefab; @@ -217,7 +218,7 @@ namespace Barotrauma public string[] TalentIdentifiers; public bool GiveRandom; - public GiveTalentInfo(XElement element, string parentDebugName) + public GiveTalentInfo(XElement element, string _) { TalentIdentifiers = element.GetAttributeStringArray("talentidentifiers", new string[0], convertToLowerInvariant: true); GiveRandom = element.GetAttributeBool("giverandom", false); @@ -751,14 +752,41 @@ namespace Barotrauma for (int i = 0; i < propertyNames.Length; i++) { if (propertyNames[i] != "condition") { continue; } - if (propertyEffects[i].GetType() == typeof(float)) + object propertyEffect = propertyEffects[i]; + if (propertyEffect.GetType() == typeof(float)) { - return (float)propertyEffects[i] < 0.0f || (setValue && (float)propertyEffects[i] <= 0.0f); + return (float)propertyEffect < 0.0f || (setValue && (float)propertyEffect <= 0.0f); } } return false; } + public bool IncreasesItemCondition() + { + for (int i = 0; i < propertyNames.Length; i++) + { + if (propertyNames[i] != "condition") { continue; } + object propertyEffect = propertyEffects[i]; + if (propertyEffect.GetType() == typeof(float)) + { + return (float)propertyEffect > 0.0f || (setValue && (float)propertyEffect > 0.0f); + } + } + return false; + } + + public bool MatchesTagConditionals(ItemPrefab itemPrefab) + { + if (itemPrefab == null || !HasConditions) + { + return false; + } + else + { + return itemPrefab.Tags.Any(t => propertyConditionals.Any(pc => pc.MatchesTagCondition(t))); + } + } + public bool HasRequiredAfflictions(AttackResult attackResult) { if (requiredAfflictions == null) { return true; } @@ -787,7 +815,7 @@ namespace Barotrauma return true; } - public IEnumerable GetNearbyTargets(Vector2 worldPosition, List targets = null) + public IReadOnlyList GetNearbyTargets(Vector2 worldPosition, List targets = null) { targets ??= new List(); if (Range <= 0.0f) { return targets; } @@ -843,23 +871,24 @@ namespace Barotrauma } } - public bool HasRequiredConditions(IEnumerable targets) + public bool HasRequiredConditions(IReadOnlyList targets) { return HasRequiredConditions(targets, propertyConditionals); } - private bool HasRequiredConditions(IEnumerable targets, IEnumerable conditionals, bool targetingContainer = false) + private bool HasRequiredConditions(IReadOnlyList targets, IReadOnlyList conditionals, bool targetingContainer = false) { - if (conditionals.None()) { return true; } - if (requiredItems.Any() && requiredItems.All(ri => ri.MatchOnEmpty) && targets.None()) { return true; } + if (conditionals.Count == 0) { return true; } + if (targets.Count == 0 && requiredItems.Count > 0 && requiredItems.All(ri => ri.MatchOnEmpty)) { return true; } switch (conditionalComparison) { case PropertyConditional.Comparison.Or: - foreach (PropertyConditional pc in conditionals) + for (int i = 0; i < conditionals.Count; i++) { + var pc = conditionals[i]; if (pc.TargetContainer && !targetingContainer) { - var target = targets.FirstOrDefault(t => t is Item || t is ItemComponent); + var target = FindTargetItemOrComponent(targets); var targetItem = target as Item ?? (target as ItemComponent)?.Item; if (targetItem?.ParentInventory == null) { @@ -881,37 +910,28 @@ namespace Barotrauma if (pc.Type == PropertyConditional.ConditionType.HasTag) { //if we're checking for tags, just check the Item object, not the ItemComponents - if (HasRequiredConditions((container as ISerializableEntity).ToEnumerable(), pc.ToEnumerable(), targetingContainer: true)) { return true; } + if (pc.Matches(container)) { return true; } } else { - if (HasRequiredConditions(container.AllPropertyObjects, pc.ToEnumerable(), targetingContainer: true)) { return true; } + if (AnyTargetMatches(container.AllPropertyObjects, pc.TargetItemComponentName, pc)) { return true; } } } - if (owner is Character character && HasRequiredConditions(character.ToEnumerable(), pc.ToEnumerable(), targetingContainer: true)) { return true; } + if (owner is Character character && pc.Matches(character)) { return true; } } else { - foreach (ISerializableEntity target in targets) - { - if (!string.IsNullOrEmpty(pc.TargetItemComponentName)) - { - if (!(target is ItemComponent ic) || ic.Name != pc.TargetItemComponentName) - { - continue; - } - } - if (pc.Matches(target)) { return true; } - } + if (AnyTargetMatches(targets, pc.TargetItemComponentName, pc)) { return true; } } } return false; case PropertyConditional.Comparison.And: - foreach (PropertyConditional pc in conditionals) + for (int i = 0; i < conditionals.Count; i++) { + var pc = conditionals[i]; if (pc.TargetContainer && !targetingContainer) { - var target = targets.FirstOrDefault(t => t is Item || t is ItemComponent); + var target = FindTargetItemOrComponent(targets); var targetItem = target as Item ?? (target as ItemComponent)?.Item; if (targetItem?.ParentInventory == null) { @@ -933,29 +953,49 @@ namespace Barotrauma if (pc.Type == PropertyConditional.ConditionType.HasTag) { //if we're checking for tags, just check the Item object, not the ItemComponents - if (!HasRequiredConditions((container as ISerializableEntity).ToEnumerable(), pc.ToEnumerable(), targetingContainer: true)) { return false; } + if (!pc.Matches(container)) { return false; } } else { - if (!HasRequiredConditions(container.AllPropertyObjects, pc.ToEnumerable(), targetingContainer: true)) { return false; } + if (!AnyTargetMatches(container.AllPropertyObjects, pc.TargetItemComponentName, pc)) { return false; } } } - if (owner is Character character && !HasRequiredConditions(character.ToEnumerable(), pc.ToEnumerable(), targetingContainer: true)) { return false; } + if (owner is Character character && !pc.Matches(character)) { return false; } } else { - var validTargets = targets; - if (!string.IsNullOrEmpty(pc.TargetItemComponentName)) - { - validTargets = targets.Where(t => t is ItemComponent ic && ic.Name == pc.TargetItemComponentName); - } - if (targets.None(t => pc.Matches(t))) { return false; } + if (!AnyTargetMatches(targets, pc.TargetItemComponentName, pc)) { return false; } } } return true; default: throw new NotImplementedException(); } + + static bool AnyTargetMatches(IReadOnlyList targets, string targetItemComponentName, PropertyConditional conditional) + { + for (int i = 0; i < targets.Count; i++) + { + if (!string.IsNullOrEmpty(targetItemComponentName)) + { + if (!(targets[i] is ItemComponent ic) || ic.Name != targetItemComponentName) { continue; } + } + if (conditional.Matches(targets[i])) + { + return true; + } + } + return false; + } + + static ISerializableEntity FindTargetItemOrComponent(IReadOnlyList targets) + { + for (int i = 0; i < targets.Count; i++) + { + if (targets[i] is Item || targets[i] is ItemComponent) { return targets[i]; } + } + return null; + } } protected bool IsValidTarget(ISerializableEntity entity) @@ -972,14 +1012,27 @@ namespace Barotrauma { if (targetIdentifiers == null) { return true; } if (targetIdentifiers.Contains("structure")) { return true; } - if (targetIdentifiers.Any(id => id.Equals(structure.Prefab.Identifier, StringComparison.OrdinalIgnoreCase))) { return true; } + foreach (var id in targetIdentifiers) + { + if (id.Equals(structure.Prefab.Identifier, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } } else if (entity is Character character) { return IsValidTarget(character); } if (targetIdentifiers == null) { return true; } - return targetIdentifiers.Any(id => id.Equals(entity.Name, StringComparison.OrdinalIgnoreCase)); + foreach (var id in targetIdentifiers) + { + if (id.Equals(entity.Name, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + return false; } protected bool IsValidTarget(ItemComponent itemComponent) @@ -989,7 +1042,14 @@ namespace Barotrauma if (targetIdentifiers == null) { return true; } if (targetIdentifiers.Contains("itemcomponent")) { return true; } if (itemComponent.Item.HasTag(targetIdentifiers)) { return true; } - return targetIdentifiers.Any(id => id.Equals(itemComponent.Item.Prefab.Identifier, StringComparison.OrdinalIgnoreCase)); + foreach (var id in targetIdentifiers) + { + if (id.Equals(itemComponent.Item.Prefab.Identifier, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + return false; } protected bool IsValidTarget(Item item) @@ -999,7 +1059,14 @@ namespace Barotrauma if (targetIdentifiers == null) { return true; } if (targetIdentifiers.Contains("item")) { return true; } if (item.HasTag(targetIdentifiers)) { return true; } - return targetIdentifiers.Any(id => id.Equals(item.Prefab.Identifier, StringComparison.OrdinalIgnoreCase)); + foreach (var id in targetIdentifiers) + { + if (id.Equals(item.Prefab.Identifier, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + return false; } protected bool IsValidTarget(Character character) @@ -1008,7 +1075,14 @@ namespace Barotrauma if (OnlyOutside && character.CurrentHull != null) { return false; } if (targetIdentifiers == null) { return true; } if (targetIdentifiers.Contains("character")) { return true; } - return targetIdentifiers.Any(id => id.Equals(character.SpeciesName, StringComparison.OrdinalIgnoreCase)); + foreach (var id in targetIdentifiers) + { + if (id.Equals(character.SpeciesName, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + return false; } public void SetUser(Character user) @@ -1037,12 +1111,14 @@ namespace Barotrauma } } - if (!HasRequiredConditions(target.ToEnumerable())) { return; } - Apply(deltaTime, entity, target.ToEnumerable(), worldPosition); + currentTargets.Clear(); + currentTargets.Add(target); + if (!HasRequiredConditions(currentTargets)) { return; } + Apply(deltaTime, entity, currentTargets, worldPosition); } protected readonly List currentTargets = new List(); - public virtual void Apply(ActionType type, float deltaTime, Entity entity, IEnumerable targets, Vector2? worldPosition = null) + public virtual void Apply(ActionType type, float deltaTime, Entity entity, IReadOnlyList targets, Vector2? worldPosition = null) { if (this.type != type) { return; } @@ -1095,39 +1171,52 @@ namespace Barotrauma return hull; } - private Vector2 GetPosition(Entity entity, IEnumerable targets, Vector2? worldPosition = null) + private Vector2 GetPosition(Entity entity, IReadOnlyList targets, Vector2? worldPosition = null) { Vector2 position = worldPosition ?? (entity == null || entity.Removed ? Vector2.Zero : entity.WorldPosition); if (worldPosition == null) { - if (entity is Character character && !character.Removed && targetLimbs?.FirstOrDefault(l => l != LimbType.None) is LimbType limbType) + if (entity is Character character && !character.Removed && targetLimbs != null) { - Limb limb = character.AnimController.GetLimb(limbType); - if (limb != null && !limb.Removed) + foreach (var targetLimbType in targetLimbs) { - position = limb.WorldPosition; + Limb limb = character.AnimController.GetLimb(targetLimbType); + if (limb != null && !limb.Removed) + { + position = limb.WorldPosition; + break; + } + } + } + else if (HasTargetType(TargetType.Contained)) + { + for (int i = 0; i < targets.Count; i++) + { + if (targets[i] is Item targetItem) + { + position = targetItem.WorldPosition; + break; + } } } else { - if (targets.FirstOrDefault(t => t is Limb) is Limb targetLimb && !targetLimb.Removed) + for (int i = 0; i < targets.Count; i++) { - position = targetLimb.WorldPosition; - } - else if (HasTargetType(TargetType.Contained)) - { - if (targets.FirstOrDefault(t => t is Item) is Item targetItem) + if (targets[i] is Limb targetLimb && !targetLimb.Removed) { - position = targetItem.WorldPosition; + position = targetLimb.WorldPosition; + break; } } } + } position += Offset; return position; } - protected void Apply(float deltaTime, Entity entity, IEnumerable targets, Vector2? worldPosition = null) + protected void Apply(float deltaTime, Entity entity, IReadOnlyList targets, Vector2? worldPosition = null) { if (lifeTime > 0) { @@ -1147,58 +1236,68 @@ namespace Barotrauma Hull hull = GetHull(entity); Vector2 position = GetPosition(entity, targets, worldPosition); - foreach (ISerializableEntity serializableEntity in targets) + if (useItemCount > 0) { - if (!(serializableEntity is Item item)) { continue; } - - Character targetCharacter = targets.FirstOrDefault(t => t is Character character && !character.Removed) as Character; - if (targetCharacter == null) + Character useTargetCharacter = null; + Limb useTargetLimb = null; + for (int i = 0; i < targets.Count; i++) { - foreach (var target in targets) + if (targets[i] is Character character && !character.Removed) { - if (target is Limb limb && limb.character != null && !limb.character.Removed) - { - targetCharacter = ((Limb)target).character; - } + useTargetCharacter = character; + break; + } + else if (targets[i] is Limb limb && limb.character != null && !limb.character.Removed) + { + useTargetLimb = limb; + useTargetCharacter ??= limb.character; + break; } } - for (int i = 0; i < useItemCount; i++) + for (int i = 0; i < targets.Count; i++) { - if (item.Removed) { continue; } - item.Use(deltaTime, targetCharacter, targets.FirstOrDefault(t => t is Limb) as Limb); + if (!(targets[i] is Item item)) { continue; } + for (int j = 0; j < useItemCount; j++) + { + if (item.Removed) { continue; } + item.Use(deltaTime, useTargetCharacter, useTargetLimb); + } } } if (removeItem) { - foreach (var target in targets) + for (int i = 0; i < targets.Count; i++) { - if (target is Item item) { Entity.Spawner?.AddToRemoveQueue(item); } + if (targets[i] is Item item) { Entity.Spawner?.AddToRemoveQueue(item); } } } if (removeCharacter) { - foreach (var target in targets) + for (int i = 0; i < targets.Count; i++) { - if (target is Character character) { Entity.Spawner?.AddToRemoveQueue(character); } + if (targets[i] is Character character) { Entity.Spawner?.AddToRemoveQueue(character); } } } if (breakLimb || hideLimb) { - foreach (var target in targets) + for (int i = 0; i < targets.Count; i++) { - if (target is Character character) + if (targets[i] is Character character) { - var matchingLimb = character.AnimController.Limbs.FirstOrDefault(l => l.body == sourceBody); - if (matchingLimb != null) + foreach (Limb limb in character.AnimController.Limbs) { - if (breakLimb) + if (limb.body == sourceBody) { - character.TrySeverLimbJoints(matchingLimb, severLimbsProbability: 100, damage: 100, allowBeheading: true); - } - else - { - matchingLimb.HideAndDisable(hideLimbTimer); + if (breakLimb) + { + character.TrySeverLimbJoints(limb, severLimbsProbability: 100, damage: 100, allowBeheading: true, attacker: user); + } + else + { + limb.HideAndDisable(hideLimbTimer); + } + break; } } } @@ -1211,10 +1310,10 @@ namespace Barotrauma } else { - foreach (ISerializableEntity target in targets) + for (int i = 0; i < targets.Count; i++) { + var target = targets[i]; if (target == null) { continue; } - if (target is Entity targetEntity) { if (targetEntity.Removed) { continue; } @@ -1225,13 +1324,13 @@ namespace Barotrauma position = limb.WorldPosition + Offset; } - for (int i = 0; i < propertyNames.Length; i++) + for (int j = 0; j < propertyNames.Length; j++) { - if (target == null || target.SerializableProperties == null || !target.SerializableProperties.TryGetValue(propertyNames[i], out SerializableProperty property)) + if (target == null || target.SerializableProperties == null || !target.SerializableProperties.TryGetValue(propertyNames[j], out SerializableProperty property)) { continue; } - ApplyToProperty(target, property, propertyEffects[i], deltaTime); + ApplyToProperty(target, property, j, deltaTime); } } } @@ -1241,8 +1340,11 @@ namespace Barotrauma explosion.Explode(position, damageSource: entity, attacker: user); } - foreach (ISerializableEntity target in targets) + bool isNotClient = GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient; + + for (int i = 0; i < targets.Count; i++) { + var target = targets[i]; //if the effect has a duration, these will be done in the UpdateAll method if (duration > 0) { break; } if (target == null) { continue; } @@ -1261,7 +1363,7 @@ namespace Barotrauma if (limb.IsSevered) { continue; } if (targetLimbs != null && !targetLimbs.Contains(limb.type)) { continue; } AttackResult result = limb.character.DamageLimb(position, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source, allowStacking: !setValue); - limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, disableDeltaTime ? result.Damage : result.Damage / deltaTime, allowBeheading: true); + limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, disableDeltaTime ? result.Damage : result.Damage / deltaTime, allowBeheading: true, attacker: affliction.Source); RegisterTreatmentResults(entity, limb, affliction, result); //only apply non-limb-specific afflictions to the first limb if (!affliction.Prefab.LimbSpecific) { break; } @@ -1273,7 +1375,7 @@ namespace Barotrauma if (limb.character.Removed || limb.Removed) { continue; } newAffliction = GetMultipliedAffliction(affliction, entity, limb.character, deltaTime, multiplyAfflictionsByMaxVitality); AttackResult result = limb.character.DamageLimb(position, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source, allowStacking: !setValue); - limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, disableDeltaTime ? result.Damage : result.Damage / deltaTime, allowBeheading: true); + limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, disableDeltaTime ? result.Damage : result.Damage / deltaTime, allowBeheading: true, attacker: affliction.Source); RegisterTreatmentResults(entity, limb, affliction, result); } } @@ -1312,7 +1414,7 @@ namespace Barotrauma } } - if (aiTriggers.Any()) + if (aiTriggers.Count > 0) { Character targetCharacter = target as Character; if (targetCharacter == null) @@ -1338,7 +1440,7 @@ namespace Barotrauma } } - if (talentTriggers.Any()) + if (talentTriggers.Count > 0) { Character targetCharacter = CharacterFromTarget(target); if (targetCharacter != null && !targetCharacter.Removed) @@ -1347,14 +1449,12 @@ namespace Barotrauma { targetCharacter.CheckTalents(AbilityEffectType.OnStatusEffectIdentifier, new AbilityStatusEffectIdentifier(talentTrigger)); } - } } - if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) + if (isNotClient) { // these effects do not need to be run clientside, as they are replicated from server to clients anyway - foreach (int giveExperience in giveExperiences) { Character targetCharacter = CharacterFromTarget(target); @@ -1364,7 +1464,7 @@ namespace Barotrauma } } - if (giveSkills.Any()) + if (giveSkills.Count > 0) { foreach (GiveSkill giveSkill in giveSkills) { @@ -1383,7 +1483,7 @@ namespace Barotrauma } } - if (giveTalentInfos.Any()) + if (giveTalentInfos.Count > 0) { Character targetCharacter = CharacterFromTarget(target); if (targetCharacter?.Info == null) { continue; } @@ -1398,7 +1498,7 @@ namespace Barotrauma if (giveTalentInfo.GiveRandom) { - targetCharacter.GiveTalent(viableTalents.GetRandom(), true); + targetCharacter.GiveTalent(viableTalents.GetRandom(Rand.RandSync.Unsynced), true); } else { @@ -1418,7 +1518,6 @@ namespace Barotrauma fire.Size = new Vector2(FireSize, fire.Size.Y); } - bool isNotClient = GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient; if (isNotClient && GameMain.GameSession?.EventManager is { } eventManager) { foreach (EventPrefab eventPrefab in triggeredEvents) @@ -1433,7 +1532,7 @@ namespace Barotrauma { List eventTargets = targets.Where(t => t is Entity).Cast().ToList(); - if (eventTargets.Any()) + if (eventTargets.Count > 0) { scriptedEvent.Targets.Add(triggeredEventTargetTag, eventTargets); } @@ -1454,7 +1553,7 @@ namespace Barotrauma var characters = new List(); for (int i = 0; i < characterSpawnInfo.Count; i++) { - Entity.Spawner.AddToSpawnQueue(characterSpawnInfo.SpeciesName, position + Rand.Vector(characterSpawnInfo.Spread, Rand.RandSync.Server) + characterSpawnInfo.Offset, + Entity.Spawner.AddToSpawnQueue(characterSpawnInfo.SpeciesName, position + Rand.Vector(characterSpawnInfo.Spread, Rand.RandSync.Unsynced) + characterSpawnInfo.Offset, onSpawn: newCharacter => { if (newCharacter.AIController is EnemyAIController enemyAi && @@ -1475,7 +1574,7 @@ namespace Barotrauma if (spawnItemRandomly) { - SpawnItem(spawnItems.GetRandom()); + SpawnItem(spawnItems.GetRandom(Rand.RandSync.Unsynced)); } else { @@ -1494,7 +1593,7 @@ namespace Barotrauma switch (chosenItemSpawnInfo.SpawnPosition) { case ItemSpawnInfo.SpawnPositionType.This: - Entity.Spawner.AddToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, position + Rand.Vector(chosenItemSpawnInfo.Spread, Rand.RandSync.Server), onSpawned: newItem => + Entity.Spawner.AddToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, position + Rand.Vector(chosenItemSpawnInfo.Spread, Rand.RandSync.Unsynced), onSpawned: newItem => { Projectile projectile = newItem.GetComponent(); if (projectile != null && user != null && sourceBody != null && entity != null) @@ -1503,11 +1602,13 @@ namespace Barotrauma if (rope != null && sourceBody.UserData is Limb sourceLimb) { rope.Attach(sourceLimb, newItem); +#if SERVER + newItem.CreateServerEvent(rope); +#endif } - float spread = MathHelper.ToRadians(Rand.Range(-chosenItemSpawnInfo.AimSpread, chosenItemSpawnInfo.AimSpread)); var worldPos = sourceBody.Position; - float rotation = chosenItemSpawnInfo.Rotation; + float rotation = 0; if (user.Submarine != null) { worldPos += user.Submarine.Position; @@ -1524,21 +1625,63 @@ namespace Barotrauma rotation = sourceBody.TransformedRotation; break; case ItemSpawnInfo.SpawnRotationType.Collider: - rotation = user.AnimController.Collider.Rotation; + rotation = user.AnimController.Collider.Rotation + MathHelper.PiOver2; break; case ItemSpawnInfo.SpawnRotationType.MainLimb: rotation = user.AnimController.MainLimb.body.TransformedRotation; break; + case ItemSpawnInfo.SpawnRotationType.Random: + DebugConsole.ShowError("Random rotation is not supported for Projectiles."); + break; default: - throw new NotImplementedException("Not implemented: " + chosenItemSpawnInfo.RotationType); + throw new NotImplementedException("Projectile spawn rotation type not implemented: " + chosenItemSpawnInfo.RotationType); } rotation += MathHelper.ToRadians(chosenItemSpawnInfo.Rotation * user.AnimController.Dir); projectile.Shoot(user, ConvertUnits.ToSimUnits(worldPos), ConvertUnits.ToSimUnits(worldPos), rotation + spread, ignoredBodies: user.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(), createNetworkEvent: true); } else { - newItem.body?.ApplyLinearImpulse(Rand.Vector(1) * chosenItemSpawnInfo.Speed); - newItem.Rotation = chosenItemSpawnInfo.Rotation; + var body = newItem.body; + if (body != null) + { + float rotation = MathHelper.ToRadians(chosenItemSpawnInfo.Rotation); + switch (chosenItemSpawnInfo.RotationType) + { + case ItemSpawnInfo.SpawnRotationType.Fixed: + if (sourceBody != null) + { + rotation = sourceBody.TransformRotation(chosenItemSpawnInfo.Rotation); + } + break; + case ItemSpawnInfo.SpawnRotationType.Limb: + if (sourceBody != null) + { + rotation += sourceBody.Rotation; + } + break; + case ItemSpawnInfo.SpawnRotationType.Collider: + if (entity is Character character) + { + rotation += character.AnimController.Collider.Rotation + MathHelper.PiOver2; + } + break; + case ItemSpawnInfo.SpawnRotationType.MainLimb: + if (entity is Character c) + { + rotation = c.AnimController.MainLimb.body.TransformedRotation; + } + break; + case ItemSpawnInfo.SpawnRotationType.Random: + rotation = Rand.Range(0f, MathHelper.TwoPi, Rand.RandSync.Unsynced); + break; + case ItemSpawnInfo.SpawnRotationType.Target: + break; + default: + throw new NotImplementedException("Spawn rotation type not implemented: " + chosenItemSpawnInfo.RotationType); + } + body.SetTransform(newItem.SimPosition, rotation); + body.ApplyLinearImpulse(Rand.Vector(1) * chosenItemSpawnInfo.Speed); + } } }); break; @@ -1626,17 +1769,19 @@ namespace Barotrauma } } - partial void ApplyProjSpecific(float deltaTime, Entity entity, IEnumerable targets, Hull currentHull, Vector2 worldPosition, bool playSound); + partial void ApplyProjSpecific(float deltaTime, Entity entity, IReadOnlyList targets, Hull currentHull, Vector2 worldPosition, bool playSound); - private void ApplyToProperty(ISerializableEntity target, SerializableProperty property, object value, float deltaTime) + private void ApplyToProperty(ISerializableEntity target, SerializableProperty property, int effectIndex, float deltaTime) { if (disableDeltaTime || setValue) { deltaTime = 1.0f; } - if (value is int || value is float) + object propertyEffect = propertyEffects[effectIndex]; + if (propertyEffect is int || propertyEffect is float) { - var propertyValue = property.GetValue(target); - if (propertyValue is float propertyValueF) + float propertyValueF = property.GetFloatValue(target); + if (property.PropertyType == typeof(float)) { - float floatValue = Convert.ToSingle(value) * deltaTime; + float floatValue = propertyEffect is float single ? single : (int)propertyEffect; + floatValue *= deltaTime; if (!setValue) { floatValue += propertyValueF; @@ -1644,18 +1789,23 @@ namespace Barotrauma property.TrySetValue(target, floatValue); return; } - else if (propertyValue is int integer) + else if (property.PropertyType == typeof(int)) { - int intValue = (int)(Convert.ToInt32(value) * deltaTime); + int intValue = (int)(propertyEffect is float single ? single * deltaTime : (int)propertyEffect * deltaTime); if (!setValue) { - intValue += integer; + intValue += (int)propertyValueF; } property.TrySetValue(target, intValue); return; } } - property.TrySetValue(target, value); + else if (propertyEffect is bool propertyValueBool) + { + property.TrySetValue(target, propertyValueBool); + return; + } + property.TrySetValue(target, propertyEffect); } public static void UpdateAll(float deltaTime) @@ -1692,7 +1842,7 @@ namespace Barotrauma { continue; } - element.Parent.ApplyToProperty(target, property, element.Parent.propertyEffects[n], CoroutineManager.UnscaledDeltaTime); + element.Parent.ApplyToProperty(target, property, n, CoroutineManager.UnscaledDeltaTime); } foreach (Affliction affliction in element.Parent.Afflictions) diff --git a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs index f05045e14..c6b96185c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs @@ -224,9 +224,9 @@ namespace Barotrauma public static void OnItemRepaired(Item item, Character fixer) { #if CLIENT - if (GameMain.Client != null) return; + if (GameMain.Client != null) { return; } #endif - if (fixer == null) return; + if (fixer == null) { return; } UnlockAchievement(fixer, "repairdevice"); UnlockAchievement(fixer, "repair" + item.Prefab.Identifier); @@ -234,10 +234,10 @@ namespace Barotrauma public static void OnAfflictionRemoved(Affliction affliction, Character character) { - if (string.IsNullOrEmpty(affliction.Prefab.AchievementOnRemoved)) return; + if (string.IsNullOrEmpty(affliction.Prefab.AchievementOnRemoved)) { return; } #if CLIENT - if (GameMain.Client != null) return; + if (GameMain.Client != null) { return; } #endif UnlockAchievement(character, affliction.Prefab.AchievementOnRemoved); } @@ -245,26 +245,29 @@ namespace Barotrauma public static void OnCharacterRevived(Character character, Character reviver) { #if CLIENT - if (GameMain.Client != null) return; + if (GameMain.Client != null) { return; } #endif - if (reviver == null) return; + if (reviver == null) { return; } UnlockAchievement(reviver, "healcrit"); } public static void OnCharacterKilled(Character character, CauseOfDeath causeOfDeath) { #if CLIENT - if (GameMain.Client != null || GameMain.GameSession == null) return; -#endif + if (GameMain.Client != null || GameMain.GameSession == null) { return; } if (character != Character.Controlled && causeOfDeath.Killer != null && causeOfDeath.Killer == Character.Controlled) { - SteamManager.IncrementStat( - character.IsHuman ? "humanskilled" : "monsterskilled", - 1); + IncrementStat(causeOfDeath.Killer, character.IsHuman ? "humanskilled" : "monsterskilled", 1); } +#elif SERVER + if (character != causeOfDeath.Killer && causeOfDeath.Killer != null) + { + IncrementStat(causeOfDeath.Killer, character.IsHuman ? "humanskilled" : "monsterskilled", 1); + } +#endif roundData?.Casualties.Add(character); @@ -337,13 +340,15 @@ namespace Barotrauma public static void OnTraitorWin(Character character) { #if CLIENT - if (GameMain.Client != null || GameMain.GameSession == null) return; + if (GameMain.Client != null || GameMain.GameSession == null) { return; } #endif UnlockAchievement(character, "traitorwin"); } public static void OnRoundEnded(GameSession gameSession) { + if (CheatsEnabled) { return; } + //made it to the destination if (gameSession?.Submarine != null && Level.Loaded != null && gameSession.Submarine.AtEndExit) { @@ -358,14 +363,14 @@ namespace Barotrauma !myCharacter.IsDead && (myCharacter.Submarine == gameSession.Submarine || (Level.Loaded?.EndOutpost != null && myCharacter.Submarine == Level.Loaded.EndOutpost))) { - SteamManager.IncrementStat("kmstraveled", levelLengthKilometers); + IncrementStat("kmstraveled", levelLengthKilometers); } #endif } else { //in sp making it to the end is enough - SteamManager.IncrementStat("kmstraveled", levelLengthKilometers); + IncrementStat("kmstraveled", levelLengthKilometers); } } @@ -461,42 +466,63 @@ namespace Barotrauma private static void UnlockAchievement(Character recipient, string identifier) { - if (CheatsEnabled) return; - if (recipient == null) return; + if (CheatsEnabled || recipient == null) { return; } #if CLIENT if (recipient == Character.Controlled) { UnlockAchievement(identifier); } -#endif -#if SERVER +#elif SERVER GameMain.Server?.GiveAchievement(recipient, identifier); #endif } - + + private static void IncrementStat(Character recipient, string identifier, int amount) + { + if (CheatsEnabled || recipient == null) { return; } +#if CLIENT + if (recipient == Character.Controlled) + { + SteamManager.IncrementStat(identifier, amount); + } +#elif SERVER + GameMain.Server?.IncrementStat(recipient, identifier, amount); +#endif + } + + public static void IncrementStat(string identifier, int amount) + { + if (CheatsEnabled) { return; } + SteamManager.IncrementStat(identifier, amount); + } + + public static void IncrementStat(string identifier, float amount) + { + if (CheatsEnabled) { return; } + SteamManager.IncrementStat(identifier, amount); + } + public static void UnlockAchievement(string identifier, bool unlockClients = false, Func conditions = null) { - if (CheatsEnabled) return; + if (CheatsEnabled) { return; } identifier = identifier.ToLowerInvariant(); #if SERVER - if (unlockClients && GameMain.Server != null) { foreach (Client c in GameMain.Server.ConnectedClients) { - if (conditions != null && !conditions(c.Character)) continue; + if (conditions != null && !conditions(c.Character)) { continue; } GameMain.Server.GiveAchievement(c, identifier); } } #endif - //already unlocked, no need to do anything - if (unlockedAchievements.Contains(identifier)) return; + if (unlockedAchievements.Contains(identifier)) { return; } unlockedAchievements.Add(identifier); #if CLIENT - if (conditions != null && !conditions(Character.Controlled)) return; + if (conditions != null && !conditions(Character.Controlled)) { return; } #endif SteamManager.UnlockAchievement(identifier); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Timing.cs b/Barotrauma/BarotraumaShared/SharedSource/Timing.cs index 96916546f..6685423d4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Timing.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Timing.cs @@ -12,6 +12,7 @@ namespace Barotrauma public static double Accumulator; public const int FixedUpdateRate = 60; public const double Step = 1.0 / FixedUpdateRate; + public const double AccumulatorMax = 0.25f; private static int frameLimit; /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs index 038c69e55..81af9005f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs @@ -7,7 +7,7 @@ using System.Xml.Linq; namespace Barotrauma { - public class IdRemap + public sealed class IdRemap { public static readonly IdRemap DiscardId = new IdRemap(null, -1); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Rand.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Rand.cs index e9d8f3727..7f0193f6e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Rand.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Rand.cs @@ -62,6 +62,9 @@ namespace Barotrauma return (sync == RandSync.Unsynced ? localRandom : (syncedRandom[(int)sync])).NextDouble() * (maximum - minimum) + minimum; } + /// + /// Min inclusive, Max exclusive! + /// public static int Range(int minimum, int maximum, RandSync sync = RandSync.Unsynced) { CheckRandThreadSafety(sync); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs index 0621dd858..2fece0b4d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs @@ -58,21 +58,37 @@ namespace Barotrauma.IO public static class SafeXML { - public static void SaveSafe(this System.Xml.Linq.XDocument doc, string path) + public static void SaveSafe(this System.Xml.Linq.XDocument doc, string path, bool throwExceptions = false) { if (!Validation.CanWrite(path, false)) { - DebugConsole.ThrowError($"Cannot save XML document to \"{path}\": modifying the files in this folder/with this extension is not allowed."); + string errorMsg = $"Cannot save XML document to \"{path}\": modifying the files in this folder/with this extension is not allowed."; + if (throwExceptions) + { + throw new InvalidOperationException(errorMsg); + } + else + { + DebugConsole.ThrowError(errorMsg); + } return; } doc.Save(path); } - public static void SaveSafe(this System.Xml.Linq.XElement element, string path) + public static void SaveSafe(this System.Xml.Linq.XElement element, string path, bool throwExceptions = false) { if (!Validation.CanWrite(path, false)) { - DebugConsole.ThrowError($"Cannot save XML element to \"{path}\": modifying the files in this folder/with this extension is not allowed."); + string errorMsg = $"Cannot save XML element to \"{path}\": modifying the files in this folder/with this extension is not allowed."; + if (throwExceptions) + { + throw new InvalidOperationException(errorMsg); + } + else + { + DebugConsole.ThrowError(errorMsg); + } return; } element.Save(path); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs index a797bde55..6e427d5ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs @@ -7,7 +7,8 @@ using System.Linq; using System.Text; using System.Threading; using System.Xml.Linq; -using Microsoft.Xna.Framework; +using Steamworks.Data; +using Color = Microsoft.Xna.Framework.Color; namespace Barotrauma { @@ -66,6 +67,7 @@ namespace Barotrauma catch (Exception e) { DebugConsole.ThrowError("Failed to clear folder", e); + return; } try @@ -75,6 +77,7 @@ namespace Barotrauma catch (Exception e) { DebugConsole.ThrowError("Error saving gamesession", e); + return; } try @@ -107,6 +110,7 @@ namespace Barotrauma catch (Exception e) { DebugConsole.ThrowError("Error saving submarine", e); + return; } try @@ -356,10 +360,10 @@ namespace Barotrauma private static bool DecompressFile(bool writeFile, string sDir, GZipStream zipStream, ProgressDelegate progress, out string fileName) { fileName = null; - + //Decompress file name byte[] bytes = new byte[sizeof(int)]; - int Readed = zipStream.Read(bytes, 0, sizeof(int)); + int Readed = Read(zipStream, bytes, sizeof(int)); if (Readed < sizeof(int)) return false; @@ -373,29 +377,29 @@ namespace Barotrauma StringBuilder sb = new StringBuilder(); for (int i = 0; i < iNameLen; i++) { - zipStream.Read(bytes, 0, sizeof(char)); + Read(zipStream, bytes, sizeof(char)); char c = BitConverter.ToChar(bytes, 0); sb.Append(c); } string sFileName = sb.ToString(); - + fileName = sFileName; progress?.Invoke(sFileName); //Decompress file content bytes = new byte[sizeof(int)]; - zipStream.Read(bytes, 0, sizeof(int)); + Read(zipStream, bytes, sizeof(int)); int iFileLen = BitConverter.ToInt32(bytes, 0); bytes = new byte[iFileLen]; - zipStream.Read(bytes, 0, bytes.Length); + Read(zipStream, bytes, bytes.Length); string sFilePath = Path.Combine(sDir, sFileName); string sFinalDir = Path.GetDirectoryName(sFilePath); string sDirFull = (string.IsNullOrEmpty(sDir) ? Directory.GetCurrentDirectory() : Path.GetFullPath(sDir)).CleanUpPathCrossPlatform(correctFilenameCase: false); string sFinalDirFull = (string.IsNullOrEmpty(sFinalDir) ? Directory.GetCurrentDirectory() : Path.GetFullPath(sFinalDir)).CleanUpPathCrossPlatform(correctFilenameCase: false); - + if (!sFinalDirFull.StartsWith(sDirFull, StringComparison.OrdinalIgnoreCase)) { throw new InvalidOperationException( @@ -427,6 +431,26 @@ namespace Barotrauma return true; } + private static int Read(GZipStream zipStream, byte[] bytes, int amount) + { + int read = 0; + + // FIXME workaround for .NET6 causing save decompression to fail +#if NET6_0 && LINUX + for (int i = 0; i < amount; i++) + { + int result = zipStream.ReadByte(); + if (result < 0) { break; } + + bytes[i] = (byte) result; + read++; + } +#else + read = zipStream.Read(bytes, 0, amount); +#endif + return read; + } + public static void DecompressToDirectory(string sCompressedFile, string sDir, ProgressDelegate progress) { DebugConsole.Log("Decompressing " + sCompressedFile + " to " + sDir + "..."); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskExtensions.cs new file mode 100644 index 000000000..1b4d14dda --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskExtensions.cs @@ -0,0 +1,18 @@ +using System.Threading.Tasks; + +namespace Barotrauma +{ + static class TaskExtensions + { + public static bool TryGetResult(this Task task, out T result) + { + if (task is Task { IsCompletedSuccessfully: true } castTask) + { + result = castTask.Result; + return true; + } + result = default; + return false; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index 3c4e97c06..bc5aa7d5b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -535,16 +535,6 @@ namespace Barotrauma } } - // Enum.HasFlag() sucks - public static bool IsBitSet(this T self, T bit) where T : struct, Enum - { - // This uses Unsafe.As for performance reasons, as - // C# will otherwise not allow a T -> int cast - // without first casting to object, which would make - // this not any better than Enum.HasFlag - return (Unsafe.As(ref self) & Unsafe.As(ref bit)) != 0; - } - public static string ByteArrayToString(byte[] ba) { StringBuilder hex = new StringBuilder(ba.Length * 2); diff --git a/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub b/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub index e6b23855e..26e46ca4f 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub and b/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Barsuk.sub b/Barotrauma/BarotraumaShared/Submarines/Barsuk.sub new file mode 100644 index 000000000..1bb604f3e Binary files /dev/null and b/Barotrauma/BarotraumaShared/Submarines/Barsuk.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Berilia.sub b/Barotrauma/BarotraumaShared/Submarines/Berilia.sub index fc12f5627..b87771309 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Berilia.sub and b/Barotrauma/BarotraumaShared/Submarines/Berilia.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Dugong.sub b/Barotrauma/BarotraumaShared/Submarines/Dugong.sub index 3ef87f69f..930f93d70 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Dugong.sub and b/Barotrauma/BarotraumaShared/Submarines/Dugong.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub b/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub index c595276b0..13e491cf7 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub and b/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Herja.sub b/Barotrauma/BarotraumaShared/Submarines/Herja.sub new file mode 100644 index 000000000..246bbcdcd Binary files /dev/null and b/Barotrauma/BarotraumaShared/Submarines/Herja.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Humpback.sub b/Barotrauma/BarotraumaShared/Submarines/Humpback.sub index 1884c10ea..86acde003 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Humpback.sub and b/Barotrauma/BarotraumaShared/Submarines/Humpback.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub b/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub index 2de8f93e6..db4d8efad 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub and b/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub b/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub index c88e6472d..0696aa7ab 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub and b/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Orca.sub b/Barotrauma/BarotraumaShared/Submarines/Orca.sub index 803ea52cb..a770b9e08 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Orca.sub and b/Barotrauma/BarotraumaShared/Submarines/Orca.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Orca2.sub b/Barotrauma/BarotraumaShared/Submarines/Orca2.sub index 744b20f4a..0cf0024d3 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Orca2.sub and b/Barotrauma/BarotraumaShared/Submarines/Orca2.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/R-29.sub b/Barotrauma/BarotraumaShared/Submarines/R-29.sub index c35ec236e..dbe786c4b 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/R-29.sub and b/Barotrauma/BarotraumaShared/Submarines/R-29.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Remora.sub b/Barotrauma/BarotraumaShared/Submarines/Remora.sub index 119454742..0fed8bcee 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Remora.sub and b/Barotrauma/BarotraumaShared/Submarines/Remora.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub b/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub index f1401653e..6361caafd 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub and b/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Selkie.sub b/Barotrauma/BarotraumaShared/Submarines/Selkie.sub index 4075081a5..cec49a349 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Selkie.sub and b/Barotrauma/BarotraumaShared/Submarines/Selkie.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Typhon.sub b/Barotrauma/BarotraumaShared/Submarines/Typhon.sub index 8a614c4c1..0771f67c3 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Typhon.sub and b/Barotrauma/BarotraumaShared/Submarines/Typhon.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub b/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub index 956eafdbd..9f602a6e0 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub and b/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Venture.sub b/Barotrauma/BarotraumaShared/Submarines/Venture.sub index a5602a06c..f11f731da 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Venture.sub and b/Barotrauma/BarotraumaShared/Submarines/Venture.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Winterhalter.sub b/Barotrauma/BarotraumaShared/Submarines/Winterhalter.sub new file mode 100644 index 000000000..87b70f6e6 Binary files /dev/null and b/Barotrauma/BarotraumaShared/Submarines/Winterhalter.sub differ diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index cbb8356d3..e8d8ddb64 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,283 @@ +--------------------------------------------------------------------------------------------------------- +v0.16.6.1 +--------------------------------------------------------------------------------------------------------- + +- Fixed captain tutorial being impossible to complete due to the docking button not activating on the nav terminal. +- Fixed door lights appearing to be off in the tutorials. +- Fixed crashing when you try to use the hotkeys or double-clicking to put an item inside another item you're holding in your left hand, but there's an item with no inventory in your right hand. +- Fixed inability to repair research stations. +- Fixed status monitors crashing the game if the selected UI style doesn't define "IconOverflowIndicator". +- Fixed crashing when trying to view the specs window of a sub that hasn't been saved yet (= if you create a new sub, go to the test mode and open the submarine tab in the tab menu). +- Fixed some of Spineling's spikes getting hidden for 30 seconds instead of 15. + +--------------------------------------------------------------------------------------------------------- +v0.16.6.0 +--------------------------------------------------------------------------------------------------------- + +Changes and additions: +- Added a medical clinic to outposts that allows you to heal your crew for a price. +- Added three new sub: Barsuk, Herja and Winterhalter. +- Improvements and polish to the human sprites. +- Improvements and polish to clothing and headgear sprites. +- Added the Submarine tab to the multiplayer campaign store to allow selling items on the submarine. +- Prevented selling items from submarine containers tagged with "donttakeitems", e.g. constructors, deconstructors. +- Added new campaign store related client permissions: use campaign store, buy items, sell inventory items, and sell submarine items. +- Entities can be grouped together and the groups selectively hidden in the submarine editor. +- Balanced mission rewards, commonness and difficulties. +- Added some new decorative items and structures. +- Adjusted medical items' effects on bleeding and burns. Bandages, plastiseal and antibiotic glue are now much more effective at treating them, and morphine & fentanyl only heal them by a negligible amount. +- Heavily increased supercapacitor's power consumption and made the recharge speed increase exponentially when the recharge rate is increased. +- Optimizations to signal logic, status effects and property conditionals. +- Item update optimizations. +- Optimizations to gap logic. +- Optimized multiplayer store interface. +- Added "separator" property to Concat Component. +- Added "power_value_out" and "load_value_out" connections to junction boxes. +- Added "set_output" connection to Greater Than Component, Equals Component and Regex Component. +- Made alien materials spawn in abyss islands. +- Exposed wall healths in the sub editor. +- Modified Orca 2 and R-29 reactor values so they're more in line with other subs. +- Made ballast flora toxins more visible and made them emit a sound. +- Made impacts toss items around less effectively, especially when the item is heavy. +- "Allow rewiring" server setting doesn't affect wrecks, pirate subs or ruins. +- Added an option to disable all in-game hints to the hint message box. +- Option to make sonar displays center on the connected sonar transducer. +- Made the sizes of signal components consistent (32x32px, they also align to the grid now). +- Don't allow fabricators to take items from linked containers the user doesn't have access to. +- Connected diving suit lockers to oxygen in vanilla subs. +- Pulse laser ammo can be bought from outposts and cities. +- Chaingun tweaks: doubled turning speed when firing, reduced charging up time, reduced ammo consumption and made the ammo boxes more expensive. +- Simplified Remora drone docking system. +- Show notifications about reputation changes mid-round. +- Escorted NPCs drop the items they took from the sub (like suits) at the end of the round. +- Added warnings when the game fails to run the physics at the desired 60 updates per second (which would cause rubberbanding in multiplayer). The warnings are shown below the FPS when the client is running slowly, and in the debug console of the host/moderators/admins when the server is. +- Made turret icons on the minimap gray instead of red when not manned (easy to think there's something wrong with the turret when it's red). +- Abandoned outpost's oxygen generators now consume power. +- Made characters crouch a little lower (enough to make it possible to shoot while standing behind a crouching character). +- Added a "sendchatmessage" console command with an option to configure the color of the message. +- Added button to align selected items and wire nodes to grid to the sub editor. +- Allow using cheats in editors. +- Don't show "hidden in-game" docking ports on the sonar, option to disable the docking port's particle and sound effects. +- The prompt about forbidden words in the server's name is only shown when trying to start a public server. +- Show prices in the submarine specs window (previously there was no way to see a sub's price in the server lobby). +- Allow wiring non-interactable items and accessing non-interactable containers in the sub editor. +- Chaingun projectiles and canister shells cause lacerations instead of gunshot wounds. +- Added recycle recipe to SMG magazines. +- Significantly reduced the speed at which welding tools fix walls. +- Changed the small arms max stack sizes to either 6 or 12 when the clip size is 6. Makes it less tedious to use the extra ammunition. +- Added damage sounds to doors when they take 10 or more damage. +- Made outpost containers' sprite depths more consistent with other containers. +- Added tags to outpost medical compartments and made them linkable. +- Mission completion and failure icons are now displayed mid-round in the tab menu. + +Monsters: +- Added 3 new monster variants: Giant Spineling, Crawler Broodmother and Veteran Mudraptor. +- An overall balance and behavior fix pass on all monsters. Feedback is welcome. +- Monster mission reward and difficulty level adjustments. +- Adjustments to the random monster spawn events. +- Adjusted the loot dropped by Crawler Broodmother, Giant Spineling, and Bonethresher. +- Moloch Pupa and Hammerhead Matriarch now also drop some loot. +- Crawler Eggs now deconstruct into suplhuric acid (and adrenaline gland, if they are not the smallest variants). +- Fixed mudraptors being unable to hit the targets that are very near. +- Fixed monsters sometimes getting stuck near the Humpback's bottom railgun. +- Fixed monsters getting stuck on trying to reach open gaps that are on the other side of the sub. +- Fixed aggressive boarders not being aggressive enough inside the player sub, because they couldn't target things that were blocked by a wall. +- Fixed Molochs not doing anything when there's babies around. +- Fixed Mudraptors not staying together in swarms. +- Fixed monsters continuing to eat a characters who've been revived with console commands (leading to weird results, such as the character being able to run around after being dismembered).. +- Fixed some monsters, like crawlers, trying to target walls with lots of gaps even though there are better targets closer to them. +- Fixed monsters being unable to target inner walls when they are technically outside of the sub (= when there's no hull where they are). Such places, between the outer and the inner walls, can be found e.g. in Humpback. +- Tigerthreshers can now target doors. +- Monsters that try to get inside the sub, should now notice and priorize doors more overall. Affects e.g. Mudraptors, and to lesser extent Crawlers (and Tigerthreshers). +- Monsters' burns don't heal by themselves. +- Fixed hammerhead matriarchs sometimes spawning in low-difficulty levels. +- Fixed endworm not having a burn damage modifier in the right tooth. +- Changed Golden Hammerhead's behavior towards stronger monsters. + +AI: +- The order of the crew list is saved between rounds in single player. +- Indicate when a bot is following someone else than you on the crew list's order icons. +- When quick-assigning orders, prioritize the characters with the same Operate order only when they are targeting the same item. +- When quick-assigning orders, don't prioritize characters with the same Maintenance order. Otherwise, Maintenance orders will always be quick-assigned to characters who already have the samer order. This will prevent giving out multiple Maintenance orders of the same type to multiple characters using the quick-assignment logic. +- Allow quick-assigning the same kind of Operate order to multiple characters. Previously, it would always be given to the character that already had the same kind of order. +- Added a new "Assault Enemy" order: bots with the order will seek out and attack any hostile characters in any connected submarines or outposts. +- Made the order quick-assigning logic prefer characters who don't have the order yet (among the characters with the appropriate job). +- Made it possible to use the order quick-assignment to give the Fix Leaks order to all character (although mechanics, engineers, and assistants are still preferred). +- Fixed bots sometimes getting stuck while trying to fix a leak that's not in the same sub (e.g. bot in Remora and the leak in the drone). +- Reduced the range where the bots can spot enemies outside of the sub. +- Improved bots' ability to return back to the submarine from caves. +- Fixed oxygen shards from old saves still being used as oxygen sources by bots. +- Fixed bots getting stuck in certain spots with ladders (e.g. Berilia's reactor room). +- Fixed contextual "clean up" order not being visible for weapons. +- Defined preferred containers for some items and added "locker" as the secondary preferred container for most items. Helps bots clean up things even when they can't find the primary container for the items. +- Fixed bots sometimes halting briefly next to a door when they shouldn't. +- "Fight intruders" order doesn't make the bots enter abandoned outposts to fight the enemies there. +- Disable aggressive behavior towards the player for the friendly crew members in single player (= accidental friendly fire never turns the security hostile in single player). +- Fixed bots saying they can't find items to load when someone takes an item they were targeting. +- Fixed bots saying they can't reach a leak when someone else fixes it before them. +- Fixed bots sometimes trying to adjust auto-controlled pumps when doing the autonomous Pump Water objective. +- Arrested pirate captains don't try to give orders to their crew. +- Change how captains (and theoretically other bots with an autonomous fight intruders order) behave: instead of idling around, they'll flee to the safety. And if there's no security officers around, they should fight the enemy aggressively. +- Fixed bots filling target containers with items that can't be refilled/recharged when given the Load Items order (e.g. putting welding fuel tanks in oxygen tank shelves). +- Made bots prefer the same fuel rods or ammo as already loaded when they're operating a reactor or a turret and need to find new ones. +- Bots that follow a character who's going inside/outside stick closer to the character they're following. Helps the bots to get back inside with you. +- Fixed pets becoming hostile towards the crew and other pets if a human attacks the character they're protecting. +- Fixed pirates not operating turrets when they have no power. +- Fixed bots not unequipping diving suits when they have an order but not actively following it (i.e. they are on idle). +- Fixed Operate orders not being dismissed automatically when another character is ordered to operate the same device. +- Fixed the dialogue reserved for rearranging character orders not being used in multiplayer. +- Fixed bots sometimes getting stuck on ladders while swimming. +- Fixed bots returning to the sub even when they have an active wait order. Happened when the order was given inside and then when e.g. the character is controlled by the player, and then when the player changes the character, the bot falls to the "find safety objective", because it's not allowed to stay outside. +- Bots can find buttons connected to a door using links made in the sub editor. Allows working around complex circuits that prevent the bots from figuring out which button controls a door. +- Fixed bots taking battery cells from portable pumps without considering their condition when acting on the Recharge Battery Cells order. +- Fixed bots not always reacting to monsters when they should be able to see them, while swimming outside. +- Fixed bots accidentally damaging friendly characters while trying to hit Swarmfeeders latched on to them. +- Fixed bots not using melee weapons when there's Swarmfeeders latched on to them. +- Fixed bots being able to shoot without any delay if they already have a weapon equipped. +- Fixed bots dropping the syringe inside PUCS when replacing the oxygen tank. +- Fixed bots sometimes failing to find a path to a docked shuttle or drone. +- Fixed NPCs reacting to combat between other characters when they shouldn't (e.g. when they don't witness it). +- Fixed bots not re-equipping body armor/ballistic helmet/something else when they drop the diving gear. +- Fixed bots having issues with some stairs. Note: these changes might require alterations on stair waypoints. Currently the generator doesn't do perfect job there. Look for the examples on how to fix them manually in the vanilla subs. +- Fixed bots sometimes trying to put items they're cleaning up into containers inside fabricators/deconstructors (e.g. an oxygen tank into a diving suit in a fabricator). +- Fixed bots equipping gene splicers when cleaning them up, causing the genetic material to get destroyed when the bot puts the splicer in a container. + +Talents: +- Removed the special stat boosts from "Olympian" (now it only increases the skill cap to 200). +- Halved the amount of damage "Still Kicking" heals (100 -> 50) +- Reduced gunshot wounds inflicted by handcannon. +- "True Potential" only has a chance of instakilling things smaller than a moloch. +- Halved damage buff from "Quickdraw" (80% -> 40%). +- Reduced skill gain from "Field Medic" (7 -> 3). +- Nerfed "Warlord" (20% chance of doubling the damage -> 5% chance). +- Reduced damage buff from "Expert Commando" (40% -> 20%). +- Health scanner doesn't show buffs from talents. +- Fixed "unused talent points" indicator staying visible after all talents have been unlocked if the character's gained extra talent points from other talents. +- Fixed incorrect talents sometimes unlocking server-side when unlocking "All-Seeing Eye". Happened because the server checked how many talents the client can unlock before applying All-Seeing Eye, which meant that the 3 extra talents would not be available, and the server would leave the last 3 talents unlocked. +- Fixed "Inspired to Act" talent only giving a skill bonus of 9.98 instead of 10. +- Fixed "Atmos Machine" talent not spawning psychosis artifacts or alien pistols. +- Fixed "Hazardous Materials" considering any reactor outside the main sub (e.g. beacon station) a wreck reactor. +- Fixed ranged weapons (including turrets) triggering "Electrochemist" talent's stun. + +Bugfixes: +- Fixes character resetting in MP campaign if you join mid-round and don't get to spawn in before the next round in a campaign you've previously played in. +- Fixed equipping two of the same genetic material and then unequipping one of them removing all the genetic effects. +- Fixed "novice seafarer", "experienced seafarer" and "naval architect" achievements being possible to unlock even if cheats are enabled. +- Fixed kills in multiplayer sessions not progressing the "xenocide" and "genocide" achievements, and kills being reported to Steam unreliably. +- Fixed clients not spawning the respawn shuttle if they join after the server had disabled the shuttle mid-round, leading to an "entity not found" kick. +- Fixed medical effects being different when the medical item is fired with a syringe gun. +- Fixed wall healths being half of what they should be on vanilla subs, increased structure damages to compensate. +- Fixed fabricator sometimes desyncing in MP when some of the ingredients are in the user's inventory. +- Fixed clients trying to reconnect to SteamP2P indefinitely if establishing the initial connection fails, eventually leading to a crash. +- Fixed "allow linking wifi to chat" server setting causing syncing problems with headsets. The setting wasn't synced with clients who don't have settings management permissions, which would cause them to get some of the wifi components' properties mixed up and sometimes prevent them from communicating using the headsets. +- Fixed motion detector requiring the target's velocity to be higher than the specified minimum velocity, instead of higher than or equal. As a result, a minimum velocity of 0 would not sometimes detect targets in range. +- Fixed an exploit that allowed combining genetic materials in deconstructors. +- Fixed "fixitems" command setting genetic materials' condition to 100. +- Fixed "reset to prefab" not resetting wall healths. +- Fixed certain logic components not passing forwards the character who sent the signal, preventing e.g. the character who undocked a drone from being logged or the character who killed something from being determined if the signal activates a weapon. +- Orca 2: Fixed missing power wires to a couple small pumps, neutral ballast level, gunnery marked as wet room. Added a duct block between upper and lower deck. Some minor visual fixes. +- Fixed voronoi sites sometimes getting placed outside the level's bounds, leading to messed up level geometry. +- Fixed turrets always starting at rotation 0 at the beginning of the round (instead of halfway between the min/max angles like in the editor). +- Fixed changing a delay component's delay using the editing hud not having an effect in-game when the component is receiving a continuous signal. +- Take structures/items with a collider into account when calculating a sub's dimensions (as opposed to just hulls). Fixes dimensions being incorrect in the submarine's info if the sub includes structures that extend far outside the hulls. +- Fixed crashing when swimming up from hull to another in a specific kind of hull configuration (two hulls side-by-side, with a gap leading up to another one). +- Fixed oxygen shards from old saves still being used as oxygen sources by bots. +- Fixed changes not being applied to all selected items when multi-editing a string field in the sub editor and deselecting the items without applying the changes by pressing enter. +- The game doesn't try to save a campaign if an exception occurs at any point during the saving process (should fix rare occurrences of campaign saves getting corrupted). +- Don't allow signals to deactivate ItemContainers. Fixes portable pumps' "toggle" input not working. +- Fixed removed items staying visible on the status monitor's electrical tab. +- Fixed plants still using the old values in old saves (i.e. dying too fast when not watered). +- Outposts can't request the "psychosisartifact_event" item (an event-specific special artifact that looks identical to the normal ones). +- Fixed size of a door's gap relative to the door changing when rescaling the door in the sub editor. +- Fixed fabricator consuming all the suitable ingredients when the ingredient is configured using a tag instead of an identifier (e.g. fabricating a stun gun dart would consume all the wires in the input slots). +- Fixed motion detector's detect offset getting mirrored when copying a mirrored detector. +- Fixed status monitor's submarine blueprint refreshing when initiating docking with a shuttle, instead of when the docking ports lock (sometimes causing the shuttle to appear slightly off from the docking port on the monito). +- Fixed fabricator failing to stack oxygenite tanks. +- Fixed items in the player's inventory not getting highlighted as valid ingredients when using a fabricator. +- Attempt to fix a rare crash caused by ScalableFont.DrawStringWithColors. +- Fixed freezing when trying to enable GameAnalytics from the settings menu on Mac. +- Fixed locked connection panel and non-interactable lights in R-29. +- Fixed Delay Component failing to parse set_delay inputs on systems that use comma as the decimal separator. +- Fixed charge rate not being displayed correctly on batteries in Chinese. +- Fixed junction box load not being displayed on status monitors in Russian. +- Fixed oxygen tanks being misaligned in oxygen generators. +- Fixed motion sensor not being able to detect subs in the sub editor test mode. +- Fixed recycled volatile fulgurium rods incorrectly using mechanical instead of electrical skill. +- Consider the character who severed a limb as the character who inflicted the afflictions caused by severing the limb. + Consider the character who caused bleeding as the character who caused the resulting bloodloss. Fixes achievements not unlocking and talents not triggering if you kill a target by cutting its limbs off or by making it bleed to death. +- Fixed characters sometimes becoming momentarily unresponsive when swimming out from a hull. +- Fixed speed penalty caused by the vegetation in caves sometimes not disappearing after passing through the vegetation. +- Fixed links from a docking port to a linked sub not being considered valid in the sub editor (only a link from linked sub to a docking port). Now the order of the link doesn't matter. +- Fixed repair window showing up if you use a periscope wired to a broken device. +- Fixed sonar getting misaligned when switching to the docking mode (the amount of misalignment being relative to the distance of the docking port from the sub's center). +- Fixed light textures not rotating with the lamps in the sub editor. +- Fixed elements in CustomInterface getting misaligned if the signal_out connections aren't used in sequential order (e.g. if you only connect a wire to outputs 2 and 3). +- Fixed server including lines multiple times in the saved server logs (e.g. the 2nd saved log file would include some lines that were already saved to the 1st log file). +- Fixed initial husk infection message being displayed immediately after getting infected, not after the infection advances. +- Fixed equip slots being misplaced if you open the health interface when the equip slots have been hidden. +- Fixed wrecks sometimes not spawning in levels despite a wreck mission being selected. +- Fixed characters moving slowly downwards when aiming underwater. +- Fixed moloch shell shields not protecting the user from non-hitscan weapons or melee weapons. +- Fixed messed up mining crane sprite. +- Fixed crashing when pirates try to operate the sub using a nav terminal that doesn't control any sub (doesn't affect vanilla subs because they don't contain that kind of nav terminals). +- Fixed fabricator showing the info of the selected item wrong when selecting the fabricator with another character (e.g. fabrication time still calculated based on the previous user's skills). +- Fixed characters reading skillbooks upside-down. +- Fixed personality traits changing after every round in mp campaign. +- Fixed monsters always eating the character they're grabbing, even when the monster is configured as not being able to eat (in practice only happened when a player controlled something like a fractal guardian and grabbed another character). +- Fixed characters sometimes using the "priorities have changed" dialogue when giving a new order. +- Fixed pumps' auto-controlled status not being updated correctly. +- Added some extra logging to diagnose the "did not receive STARTGAMEFINALIZE message from the server" errors. +- Misc localization fixes and improvements. +- Fixed tab menu's character tab not refreshing when switching to another character. +- Fixed ballast flora still being present when you replace an infested lost shuttle in an outpost. +- Fixed occasional crashes and entity ID errors when entering a new level with a ballast flora infested sub. +- Fixed inability to swap SMG magazines (or other items that go inside the held item) by double-clicking. +- Fixed "failed to parse the string to Vector2" when loading bot orders that have been saved on a system that uses comma as a decimal separator. +- Fixed rounding error in RespawnManager that caused it to require 1 extra dead player to trigger a respawn (e.g. 9 players and a minimum of 30% players to respawn required 3 players, but the client-side texts showed 2). +- Fixed "stairs left" appearing mirrored in the status monitor's sub blueprint and in the sub editor's entity selection menu. +- Fixed Wifi Component's "set_channel" input not working when sending signals to it via chat in multiplayer. +- Fixed autoshotgun not taking stacks into account in the ammo indicator below the inventory slot (= displaying it as being full when there's one shell in each slot, even though more could be stacked on the slots). +- Fixed hitscan turrets sometimes hitting targets inside your own sub when there's linked subs present. +- Fixed faraday and nasonov artifacts' periodic explosions stopping if the round is ended during their 0.5s "reset" period. +- Fixed oxygenite shards not exploding in depth charge shells. +- Fixed killer sometimes being determined incorrectly when a character gets killed by something else than another character: e.g. if a character got crushed by pressure, the character who last did damage to them was considered to be the killer, which could for example lead to achievements being unlocked in inappropriate situations. +- Fixed pumps not taking the volumes/shapes of the linked hulls into account when using "set_targetlevel", causing the neural level to be off in irregularly shaped multi-hull ballasts. +- Fixed artifacts sometimes spawning outside the level when there's no artifact holder to place them in (e.g. when having 2 artifact missions active at the same time). +- The electrical grid in beacon stations is turned indestructible after activating it. Should fix beacon missions sometimes failing for no apparent reason (if something happened to damage the beacon's walls during the round and flood it). +- Attempt to fix a null reference exception in Map.RemoveFogOfWar (suspecting it was caused by a mod that didn't configure the campaign map's sprite for some biome). +- Fixed nav terminal's docking button staying visible if the terminal is disconnected from the docking port by deactivating a relay between them. +- Fixed cursor position jittering when the sub is moving fast. +- Fixed discharge coils in Berilia and Orca 2 being connected to junction boxes instead of supercapacitors. +- Fixed wall colliders generating twice on abyss islands without caves, and the 1st generated wall not getting mirrored along with the level, leading to "invisible walls" in some areas of the abyss in mirrored levels. +- Pirates that are outside or unconscious count as being dead in the pirate missions. Fixes pirate missions failing if e.g. one of the pirates gets stranded outside their sub. +- Fixed some turrets being possible to power with batteries, even though the maximum power output of the batteries shouldn't be high enough. +- Fixed the drug dealer in the "heart of gold" event fleeing from the other bandits. +- Fixed decapitating not working as it should. +- Fixed being able to grab hostile NPCs. +- Fixed sound effects not playing when a monster hits the sub's inner wall. +- Fixed correct sprite not being used in the great sea on the campaign map. +- Fixed "settings" text overlapping in the settings menu when using a very large text size. +- EventManager doesn't consider monsters in a docked non-player sub (e.g. abandoned outpost) to be "inside the sub". Fixes intensity always being at 100% in monster-infested outposts. +- Fixed outpost cabinet's sprite having empty space above it. +- Fixed inability to put syringe guns, toy hammers, welding tools, plasma cutters and sprayers in weapon holders. +- Fixed escort missions giving huge rewards in higher difficulty levels. +- Fixed a nullref exception in CharacterHUD.Draw when an icon can't be found for a campaign interaction. + +Modding: +- Added "HealCostMultiplier" attribute to AfflictionPrefabs that adjusts the heal cost in medical clinic. +- EntitySpawnerComponent treats positive y offset as up to make it more consistent with other components. +- Added an option to define a hard limit for how many entities EntitySpawnerComponent can spawn. +- Fixed "targetself" attack conditionals checking both the attacker and the target. +- Added "delaybetweenspawns" property to MonsterEvents (determines the delay between spawning the individual monsters of a given monster event). +- Don't allow setting an item's or limb's density to 0 (leads to "attempted to apply invalid force/torque" errors). +- Fixed shields blocking projectiles from the user's weapon. Didn't affect any vanilla items, because all the shields are 2-hand items that prevent using a weapon at the same time. +- Fixed ButtonTerminals without an ItemContainer component causing crashes. +- If a mod makes a vanilla item movable/detachable and sets it as being attached by default, attach it to a wall when loading a sub that already had those items placed. I.e. making static devices movable doesn't cause them to deattach in existing subs. +- Fixed monster AI's targeting priorities doing nothing if the threshold is 0 and the target hasn't done any damage. +- Fixed custom ID card tags not working in wrecks. +- Fixed Rope component not attaching to the limb it's fired from in multiplayer (doesn't affect any vanilla content). +- Fixed crashing in multiplayer when there are spectators in the server and someone reaches the final stage of a modded husk affliction that allows remaining in control of the final form. +- Fixed wearables that are equipped into multiple slots (e.g. InnerClothes+OuterClothes) not being visible when worn. + --------------------------------------------------------------------------------------------------------- v0.15.23.0 --------------------------------------------------------------------------------------------------------- diff --git a/README.md b/README.md index 2d02615e2..7c2cd0ccb 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This is a Barotrauma modification that adds Lua modding support. # Barotrauma -Copyright © FakeFish Ltd 2017-2021 +Copyright © FakeFish Ltd 2017-2022 Before downloading the source code, please read the [EULA](EULA.txt).