diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs index c43d3f26b..56cb2ac83 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs @@ -47,8 +47,8 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, wallTargetPos - new Vector2(10.0f, 10.0f), new Vector2(20.0f, 20.0f), Color.Orange, false); GUI.DrawLine(spriteBatch, pos, wallTargetPos, Color.Orange * 0.5f, 0, 5); } - GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 60.0f, $"{SelectedAiTarget.Entity} ({GetTargetMemory(SelectedAiTarget, false)?.Priority.FormatZeroDecimal()})", GUIStyle.Red, Color.Black); - GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 40.0f, $"({targetValue.FormatZeroDecimal()})", GUIStyle.Red, Color.Black); + GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 60.0f, $"{SelectedAiTarget.Entity}", GUIStyle.Red, Color.Black); + GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 40.0f, $"{targetValue.FormatZeroDecimal()} (M: {SelectedTargetMemory?.Priority.FormatZeroDecimal()}, P: {SelectedTargetingParams?.Priority.FormatZeroDecimal()})", GUIStyle.Red, Color.Black); } /*GUIStyle.Font.DrawString(spriteBatch, targetValue.ToString(), pos - Vector2.UnitY * 80.0f, GUIStyle.Red); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 99cd9bd2d..7cbb4e91d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -981,7 +981,24 @@ namespace Barotrauma } if (IsDead) { return; } - + + var healthBarMode = GameMain.NetworkMember?.ServerSettings.ShowEnemyHealthBars ?? GameSettings.CurrentConfig.ShowEnemyHealthBars; + if (healthBarMode != EnemyHealthBarMode.ShowAll) + { + if (Controlled == null) + { + if (!IsOnPlayerTeam) { return; } + } + else + { + if (!HumanAIController.IsFriendly(Controlled, this) || + (AIController is HumanAIController humanAi && humanAi.ObjectiveManager.CurrentObjective is AIObjectiveCombat combatObjective && HumanAIController.IsFriendly(Controlled, combatObjective.Enemy))) + { + return; + } + } + } + if (CharacterHealth.DisplayedVitality < MaxVitality * 0.98f && hudInfoVisible) { hudInfoAlpha = Math.Max(hudInfoAlpha, Math.Min(CharacterHealth.DamageOverlayTimer, 1.0f)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index df6381f18..44c9ab4ac 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -1,6 +1,5 @@ using Barotrauma.Extensions; using Barotrauma.Items.Components; -using Barotrauma.Tutorials; using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -114,7 +113,7 @@ namespace Barotrauma return character?.Inventory != null && !character.Removed && !character.IsKnockedDown && - (controller?.User != character || !controller.HideHUD) && + (controller?.User != character || !controller.HideHUD || Screen.Selected.IsEditor) && !IsCampaignInterfaceOpen && !ConversationAction.FadeScreenToBlack; } @@ -548,7 +547,7 @@ namespace Barotrauma if (CharacterHealth.OpenHealthWindow == character.SelectedCharacter.CharacterHealth) { character.SelectedCharacter.CharacterHealth.Alignment = Alignment.Left; - character.SelectedCharacter.CharacterHealth.DrawStatusHUD(spriteBatch); + //character.SelectedCharacter.CharacterHealth.DrawStatusHUD(spriteBatch); } } else if (character.Inventory != null) @@ -644,6 +643,12 @@ namespace Barotrauma { if (character == null || character.IsDead || character.Removed) { return; } + var healthBarMode = GameMain.NetworkMember?.ServerSettings.ShowEnemyHealthBars ?? GameSettings.CurrentConfig.ShowEnemyHealthBars; + if (healthBarMode == EnemyHealthBarMode.HideAll) + { + return; + } + var existingBar = bossHealthBars.Find(b => b.Character == character); if (existingBar != null) { @@ -669,6 +674,8 @@ namespace Barotrauma public static void UpdateBossHealthBars(float deltaTime) { + var healthBarMode = GameMain.NetworkMember?.ServerSettings.ShowEnemyHealthBars ?? GameSettings.CurrentConfig.ShowEnemyHealthBars; + for (int i = 0; i < bossHealthBars.Count; i++) { var bossHealthBar = bossHealthBars[i]; @@ -710,7 +717,7 @@ namespace Barotrauma for (int i = bossHealthBars.Count - 1; i >= 0 ; i--) { var bossHealthBar = bossHealthBars[i]; - if (bossHealthBar.FadeTimer <= 0) + if (bossHealthBar.FadeTimer <= 0 || healthBarMode == EnemyHealthBarMode.HideAll) { bossHealthBar.SideContainer.Parent?.RemoveChild(bossHealthBar.SideContainer); bossHealthBar.TopContainer.Parent?.RemoveChild(bossHealthBar.TopContainer); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index ab2942b42..b472749ff 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -78,7 +78,7 @@ namespace Barotrauma Color? nameColor = null; if (Job != null) { nameColor = Job.Prefab.UIColor; } - GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), ToolBox.LimitString(Name, GUIStyle.Font, headerTextArea.Rect.Width), textColor: nameColor, font: GUIStyle.Font) + GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.25f), headerTextArea.RectTransform), ToolBox.LimitString(Name, GUIStyle.Font, headerTextArea.Rect.Width), textColor: nameColor, font: GUIStyle.Font) { ForceUpperCase = ForceUpperCase.Yes, Padding = Vector4.Zero @@ -92,8 +92,8 @@ namespace Barotrauma } if (Job != null) - { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), Job.Name, textColor: Job.Prefab.UIColor, font: font) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.25f), headerTextArea.RectTransform), Job.Name, textColor: Job.Prefab.UIColor, font: font) { Padding = Vector4.Zero }; @@ -101,7 +101,7 @@ namespace Barotrauma if (PersonalityTrait != null) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.25f), headerTextArea.RectTransform), TextManager.AddPunctuation(':', TextManager.Get("PersonalityTrait"), PersonalityTrait.DisplayName), font: font) { @@ -109,7 +109,23 @@ namespace Barotrauma }; } - if (Job != null && (Character == null || !Character.IsDead)) + GUIButton manageTalentButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.25f), headerTextArea.RectTransform), + text: TextManager.Get("ClientPermission.ManageBotTalents"), style: "GUIButtonSmall") + { + Enabled = false, + UserData = TalentMenu.ManageBotTalentsButtonUserData, + TextBlock = + { + AutoScaleHorizontal = true + } + }; + + if (TalentMenu.CanManageTalents(this)) + { + manageTalentButton.Enabled = true; + } + + if (Job != null && Character is not { IsDead: true }) { var skillsArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.63f), paddedFrame.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter)) { @@ -120,7 +136,7 @@ namespace Barotrauma skills.Sort((s1, s2) => -s1.Level.CompareTo(s2.Level)); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillsArea.RectTransform), TextManager.AddPunctuation(':', TextManager.Get("skills"), string.Empty), font: font) { Padding = Vector4.Zero }; - + foreach (Skill skill in skills) { Color textColor = Color.White * (0.5f + skill.Level / 200.0f); @@ -144,7 +160,7 @@ namespace Barotrauma } } } - else if (Character != null && Character.IsDead) + else if (Character is { IsDead: true }) { var deadArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.63f), paddedFrame.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 983906c72..ac3cbf968 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -89,11 +89,23 @@ namespace Barotrauma private int selectedLimbIndex = -1; private LimbHealth currentDisplayedLimb; + /// + /// Container for the icons above the health bar + /// + private GUIComponent afflictionIconContainer; + + private GUIButton showHiddenAfflictionsButton; + + /// + /// Container for passive afflictions that have been hidden from afflictionIconContainer + /// + private GUIComponent hiddenAfflictionIconContainer; + private GUIProgressBar healthWindowHealthBar; private GUIProgressBar healthWindowHealthBarShadow; private GUITextBlock characterName; - private GUIListBox afflictionIconContainer; + private GUIListBox afflictionIconList; private GUILayoutGroup treatmentLayout; private GUIListBox recommendedTreatmentContainer; @@ -331,7 +343,7 @@ namespace Barotrauma deadIndicator.AutoScaleHorizontal = true; } - afflictionIconContainer = new GUIListBox(new RectTransform(new Vector2(0.25f, 1.0f), characterIndicatorArea.RectTransform), style: null); + afflictionIconList = new GUIListBox(new RectTransform(new Vector2(0.25f, 1.0f), characterIndicatorArea.RectTransform), style: null); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), healthWindowVerticalLayout.RectTransform), TextManager.Get("SuitableTreatments"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomCenter); @@ -379,6 +391,25 @@ namespace Barotrauma Enabled = true }; + afflictionIconContainer = new GUILayoutGroup( + HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.HealthBarAfflictionArea, GUI.Canvas), + isHorizontal: true, childAnchor: Anchor.CenterRight) + { + AbsoluteSpacing = GUI.IntScale(5) + }; + + showHiddenAfflictionsButton = new GUIButton(new RectTransform(new Point(afflictionIconContainer.Rect.Height), afflictionIconContainer.RectTransform), style: "GUIButtonCircular") + { + CanBeFocused = false + }; + + hiddenAfflictionIconContainer = new GUILayoutGroup( + HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.HealthBarAfflictionArea, GUI.Canvas), + isHorizontal: true, childAnchor: Anchor.CenterRight) + { + AbsoluteSpacing = GUI.IntScale(5) + }; + UpdateAlignment(); SuicideButton = new GUIButton(new RectTransform(new Vector2(0.1f, 0.02f), GUI.Canvas, Anchor.TopCenter) @@ -596,7 +627,7 @@ namespace Barotrauma public void UpdateHUD(float deltaTime) { - if (GUI.DisableHUD) return; + if (GUI.DisableHUD) { return; } if (openHealthWindow != null) { if (openHealthWindow != Character.Controlled?.CharacterHealth && openHealthWindow != Character.Controlled?.SelectedCharacter?.CharacterHealth) @@ -700,6 +731,8 @@ namespace Barotrauma distortTimer = 0.0f; } + UpdateStatusHUD(deltaTime); + if (PlayerInput.KeyHit(InputType.Health) && GUI.KeyboardDispatcher.Subscriber == null && Character.Controlled.AllowInput && !toggledThisFrame) { @@ -726,9 +759,9 @@ namespace Barotrauma OpenHealthWindow = null; } - foreach (GUIComponent afflictionIcon in afflictionIconContainer.Content.Children) + foreach (GUIComponent afflictionIcon in afflictionIconList.Content.Children) { - if (!(afflictionIcon.UserData is Affliction affliction)) { continue; } + if (afflictionIcon.UserData is not Affliction affliction) { continue; } if (affliction.AppliedAsFailedTreatmentTime > Timing.TotalTime - 1.0 && afflictionIcon.FlashTimer <= 0.0f) { afflictionIcon.Flash(GUIStyle.Red); @@ -900,7 +933,7 @@ namespace Barotrauma healthBarHolder.CanBeFocused = healthBar.CanBeFocused = healthBarShadow.CanBeFocused = !Character.ShouldLockHud(); if (Character.AllowInput && UseHealthWindow && !Character.DisableHealthWindow && healthBar.Enabled && healthBar.CanBeFocused && - (GUI.IsMouseOn(healthBar) || highlightedAfflictionIcon != null) && Inventory.SelectedSlot == null) + (GUI.IsMouseOn(healthBar) || GUI.MouseOn?.UserData is AfflictionPrefab) && Inventory.SelectedSlot == null) { healthBar.State = GUIComponent.ComponentState.Hover; if (PlayerInput.PrimaryMouseButtonClicked()) @@ -960,7 +993,12 @@ namespace Barotrauma } else if (Character.Controlled == Character && !CharacterHUD.IsCampaignInterfaceOpen) { - healthBarHolder.AddToGUIUpdateList(); + healthBarHolder.AddToGUIUpdateList(); + afflictionIconContainer.AddToGUIUpdateList(); + if (hiddenAfflictionIconContainer.Visible) + { + hiddenAfflictionIconContainer.AddToGUIUpdateList(); + } } if (SuicideButton.Visible && Character == Character.Controlled) { @@ -989,7 +1027,7 @@ namespace Barotrauma if (affliction.Prefab.AfflictionOverlay != null) { Sprite ScreenAfflictionOverlay = affliction.Prefab.AfflictionOverlay; - ScreenAfflictionOverlay?.Draw(spriteBatch, Vector2.Zero, Color.White * (affliction.GetAfflictionOverlayMultiplier()), Vector2.Zero, 0.0f, + ScreenAfflictionOverlay?.Draw(spriteBatch, Vector2.Zero, Color.White * affliction.GetAfflictionOverlayMultiplier(), Vector2.Zero, 0.0f, new Vector2(GameMain.GraphicsWidth / DamageOverlay.size.X, GameMain.GraphicsHeight / DamageOverlay.size.Y)); } } @@ -1021,94 +1059,133 @@ namespace Barotrauma // 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, LocalizedString NameToolTip)? highlightedAfflictionIcon = null; - public void DrawStatusHUD(SpriteBatch spriteBatch) + //private (Affliction Affliction, LocalizedString NameToolTip)? highlightedAfflictionIcon = null; + + private readonly List statusIcons = new List(); + private readonly Dictionary statusIconVisibleTime = new Dictionary(); + private float hideStatusIconDelay = 5.0f; + + public void UpdateStatusHUD(float deltaTime) { - highlightedAfflictionIcon = null; - //Rectangle interactArea = healthBar.Rect; if (Character.Controlled?.SelectedCharacter == null && openHealthWindow == null) { - var statusIcons = new List<(Affliction Affliction, LocalizedString Warning)>(); + statusIcons.Clear(); if (Character.InPressure) { - statusIcons.Add((pressureAffliction, TextManager.Get("PressureHUDWarning"))); + statusIcons.Add(pressureAffliction); } if (Character.CurrentHull != null && Character.OxygenAvailable < LowOxygenThreshold && oxygenLowAffliction.Strength < oxygenLowAffliction.Prefab.ShowIconThreshold) { - statusIcons.Add((oxygenLowAffliction, TextManager.Get("OxygenHUDWarning"))); + statusIcons.Add(oxygenLowAffliction); } foreach (Affliction affliction in currentDisplayedAfflictions) { - statusIcons.Add((affliction, affliction.Prefab.Name)); + statusIcons.Add(affliction); } - Vector2 highlightedIconPos = Vector2.Zero; - Rectangle afflictionArea = HUDLayoutSettings.AfflictionAreaLeft; - - // Push the icons down since the portrait doesn't get rendered + int spacing = GUI.IntScale(10); if (Character.ShouldLockHud()) { - afflictionArea.Y += HUDLayoutSettings.PortraitArea.Height; + // Push the icons down since the portrait doesn't get rendered + afflictionIconContainer.RectTransform.ScreenSpaceOffset = new Point(0, HUDLayoutSettings.PortraitArea.Height); + hiddenAfflictionIconContainer.RectTransform.ScreenSpaceOffset = new Point(0, -hiddenAfflictionIconContainer.Rect.Height - spacing + HUDLayoutSettings.PortraitArea.Height); } + else + { + afflictionIconContainer.RectTransform.ScreenSpaceOffset = new Point(0, 0); + hiddenAfflictionIconContainer.RectTransform.ScreenSpaceOffset = new Point(0, -hiddenAfflictionIconContainer.Rect.Height - spacing); + } + //remove affliction icons for afflictions that no longer exist - bool horizontal = afflictionArea.Width > afflictionArea.Height; - int iconSize = horizontal ? afflictionArea.Height : afflictionArea.Width; - - Point pos = new Point(afflictionArea.Right - iconSize, afflictionArea.Top); + RemoveNonExistentIcons(afflictionIconContainer); + RemoveNonExistentIcons(hiddenAfflictionIconContainer); + void RemoveNonExistentIcons(GUIComponent container) + { + for (int i = container.CountChildren - 1; i >= 0; i--) + { + var child = container.GetChild(i); + if (child.UserData is not AfflictionPrefab afflictionPrefab) { continue; } + if (!statusIcons.Any(s => s.Prefab == afflictionPrefab)) + { + container.RemoveChild(child); + statusIconVisibleTime.Remove(afflictionPrefab); + } + } + } foreach (var statusIcon in statusIcons) { - Affliction affliction = statusIcon.Affliction; + Affliction affliction = statusIcon; AfflictionPrefab afflictionPrefab = affliction.Prefab; - Rectangle afflictionIconRect = new Rectangle(pos, new Point(iconSize)); - if (afflictionIconRect.Contains(PlayerInput.MousePosition) && !Character.ShouldLockHud() && GUI.MouseOn == null) + if (!statusIconVisibleTime.ContainsKey(afflictionPrefab)) { statusIconVisibleTime.Add(afflictionPrefab, 0.0f); } + statusIconVisibleTime[afflictionPrefab] += deltaTime; + + var matchingIcon = + afflictionIconContainer.GetChildByUserData(afflictionPrefab) ?? + hiddenAfflictionIconContainer.GetChildByUserData(afflictionPrefab); + if (matchingIcon == null) { - highlightedAfflictionIcon = statusIcon; - highlightedIconPos = afflictionIconRect.Location.ToVector2(); + matchingIcon = new GUIButton(new RectTransform(new Point(afflictionIconContainer.Rect.Height), afflictionIconContainer.RectTransform), style: null) + { + UserData = afflictionPrefab, + ToolTip = affliction.Prefab.Name, + CanBeSelected = false + }; + if (affliction == pressureAffliction) + { + matchingIcon.ToolTip = TextManager.Get("PressureHUDWarning"); + } + else if (affliction == pressureAffliction) + { + matchingIcon.ToolTip = TextManager.Get("OxygenHUDWarning"); + } + new GUIImage(new RectTransform(Vector2.One, matchingIcon.RectTransform, Anchor.BottomCenter), afflictionPrefab.Icon, scaleToFit: true) + { + CanBeFocused = false + }; } - - if (affliction.DamagePerSecond > 1.0f) + if (afflictionPrefab.HideIconAfterDelay && statusIconVisibleTime[afflictionPrefab] > hideStatusIconDelay) { - Rectangle glowRect = afflictionIconRect; - glowRect.Inflate((int)(20 * GUI.Scale), (int)(20 * GUI.Scale)); - var glow = GUIStyle.GetComponentStyle("OuterGlowCircular"); - glow.Sprites[GUIComponent.ComponentState.None][0].Draw( - spriteBatch, glowRect, - GUIStyle.Red * (float)((Math.Sin(affliction.DamagePerSecondTimer * MathHelper.TwoPi - MathHelper.PiOver2) + 1.0f) * 0.5f)); + matchingIcon.RectTransform.Parent = hiddenAfflictionIconContainer.RectTransform; } + var image = matchingIcon.GetChild(); + image.Color = GetAfflictionIconColor(afflictionPrefab, affliction); + image.HoverColor = Color.Lerp(image.Color, Color.White, 0.5f); - float alphaMultiplier = highlightedAfflictionIcon == statusIcon ? 1f : 0.8f; - - afflictionPrefab.Icon?.Draw(spriteBatch, - pos.ToVector2(), - /*highlightedIcon == statusIcon ? statusIcon.First.Prefab.IconColor : statusIcon.First.Prefab.IconColor * 0.8f,*/ // OLD IMPLEMENTATION - GetAfflictionIconColor(afflictionPrefab, affliction) * alphaMultiplier, - rotate: 0, - scale: iconSize / afflictionPrefab.Icon.size.X); - - if (horizontal) - pos.X -= iconSize + (int)(5 * GUI.Scale); - else - pos.Y += iconSize + (int)(5 * GUI.Scale); + if (affliction.DamagePerSecond > 1.0f && matchingIcon.FlashTimer <= 0.0f) + { + matchingIcon.Flash(useCircularFlash: true, flashDuration: 1.5f, flashRectInflate: Vector2.One * 15.0f * GUI.Scale); + image.Pulsate(Vector2.One, Vector2.One * 1.2f, 1.0f); + } } - if (highlightedAfflictionIcon != null) + afflictionIconContainer.RectTransform.SortChildren((r1, r2) => { - LocalizedString nameTooltip = highlightedAfflictionIcon.Value.NameToolTip; - Vector2 offset = GUIStyle.Font.MeasureString(nameTooltip); + if (r1.GUIComponent.UserData is not AfflictionPrefab prefab1) { return -1; } + if (r2.GUIComponent.UserData is not AfflictionPrefab prefab2) { return 1; } + var index1 = statusIcons.IndexOf(s => s.Prefab == prefab1); + var index2 = statusIcons.IndexOf(s => s.Prefab == prefab2); + return index1.CompareTo(index2); + }); + (afflictionIconContainer as GUILayoutGroup).NeedsToRecalculate = true; - GUI.DrawString(spriteBatch, - alignment == Alignment.Left ? highlightedIconPos + offset : highlightedIconPos - offset, - nameTooltip, - Color.White * 0.8f, Color.Black * 0.5f); + Rectangle hiddenAfflictionHoverArea = showHiddenAfflictionsButton.Rect; + foreach (GUIComponent child in hiddenAfflictionIconContainer.Children) + { + hiddenAfflictionHoverArea = Rectangle.Union(hiddenAfflictionHoverArea, child.Rect); } + hiddenAfflictionIconContainer.Visible = + showHiddenAfflictionsButton.Rect.Contains(PlayerInput.MousePosition) || + (hiddenAfflictionIconContainer.Visible && hiddenAfflictionHoverArea.Contains(PlayerInput.MousePosition)); + showHiddenAfflictionsButton.Visible = hiddenAfflictionIconContainer.CountChildren > 0; + showHiddenAfflictionsButton.IgnoreLayoutGroups = !showHiddenAfflictionsButton.Visible; + showHiddenAfflictionsButton.Text = $"+{hiddenAfflictionIconContainer.CountChildren}"; + if (Vitality > 0.0f) { float currHealth = healthBar.BarSize; @@ -1150,18 +1227,20 @@ namespace Barotrauma public static Color GetAfflictionIconColor(AfflictionPrefab prefab, float afflictionStrength) { + //use sqrt to make the color change rapidly when strength is low + //(low strength is where seeing the severity of the affliction makes more difference - at high strengths the character is already unconscious or dead) + float colorT = MathF.Sqrt(afflictionStrength / prefab.MaxStrength); // No specific colors, use generic if (prefab.IconColors == null) { if (prefab.IsBuff) { - return ToolBox.GradientLerp(afflictionStrength / prefab.MaxStrength, GUIStyle.BuffColorLow, GUIStyle.BuffColorMedium, GUIStyle.BuffColorHigh); + return ToolBox.GradientLerp(colorT, GUIStyle.BuffColorLow, GUIStyle.BuffColorMedium, GUIStyle.BuffColorHigh); } - return ToolBox.GradientLerp(afflictionStrength / prefab.MaxStrength, GUIStyle.DebuffColorLow, GUIStyle.DebuffColorMedium, GUIStyle.DebuffColorHigh); + return ToolBox.GradientLerp(colorT, GUIStyle.DebuffColorLow, GUIStyle.DebuffColorMedium, GUIStyle.DebuffColorHigh); } - - return ToolBox.GradientLerp(afflictionStrength / prefab.MaxStrength, prefab.IconColors); + return ToolBox.GradientLerp(colorT, prefab.IconColors); } public static Color GetAfflictionIconColor(Affliction affliction) => GetAfflictionIconColor(affliction.Prefab, affliction); @@ -1172,7 +1251,7 @@ namespace Barotrauma { if (selectedLimb == null) { - afflictionIconContainer.Content.ClearChildren(); + afflictionIconList.Content.ClearChildren(); return; } @@ -1207,7 +1286,7 @@ namespace Barotrauma private void CreateAfflictionInfos(IEnumerable afflictions) { - afflictionIconContainer.ClearChildren(); + afflictionIconList.ClearChildren(); displayedAfflictions.Clear(); Affliction mostSevereAffliction = SortAfflictionsBySeverity(afflictions, excludeBuffs: false).FirstOrDefault(); @@ -1217,7 +1296,7 @@ namespace Barotrauma { displayedAfflictions.Add((affliction, affliction.Strength)); - var frame = new GUIButton(new RectTransform(new Vector2(1.0f, 0.25f), afflictionIconContainer.Content.RectTransform), style: "ListBoxElement") + var frame = new GUIButton(new RectTransform(new Vector2(1.0f, 0.25f), afflictionIconList.Content.RectTransform), style: "ListBoxElement") { UserData = affliction, OnClicked = SelectAffliction @@ -1275,7 +1354,7 @@ namespace Barotrauma } buttonToSelect?.OnClicked(buttonToSelect, buttonToSelect.UserData); - afflictionIconContainer.RecalculateChildren(); + afflictionIconList.RecalculateChildren(); } private void CreateRecommendedTreatments() @@ -1386,7 +1465,7 @@ namespace Barotrauma recommendedTreatmentContainer.RecalculateChildren(); - afflictionIconContainer.Content.RectTransform.SortChildren((r1, r2) => + afflictionIconList.Content.RectTransform.SortChildren((r1, r2) => { var first = r1.GUIComponent.UserData as Affliction; var second = r2.GUIComponent.UserData as Affliction; @@ -1437,7 +1516,10 @@ namespace Barotrauma }; var description = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), parent.RectTransform), - affliction.Prefab.Description, textAlignment: Alignment.TopLeft, wrap: true) + affliction.Prefab.GetDescription( + affliction.Strength, + Character == Character.Controlled ? AfflictionPrefab.Description.TargetType.Self : AfflictionPrefab.Description.TargetType.OtherCharacter), + textAlignment: Alignment.TopLeft, wrap: true) { CanBeFocused = false }; @@ -1482,10 +1564,9 @@ namespace Barotrauma private bool SelectAffliction(GUIButton button, object userData) { bool selected = button.Selected; - foreach (var child in afflictionIconContainer.Content.Children) + foreach (var child in afflictionIconList.Content.Children) { - GUIButton btn = child.GetChild(); - if (btn != null) + if (child is GUIButton btn) { btn.Selected = btn == button && !selected; } @@ -1516,7 +1597,7 @@ namespace Barotrauma afflictionEffectColor = GUIStyle.Green; } - var child = afflictionIconContainer.Content.FindChild(affliction); + var child = afflictionIconList.Content.FindChild(affliction); var afflictionStrengthPredictionBar = child.GetChild().GetChildByUserData("afflictionstrengthprediction") as GUIProgressBar; afflictionStrengthPredictionBar.BarSize = 0.0f; @@ -1836,14 +1917,14 @@ namespace Barotrauma i++; } - if (selectedLimbIndex > -1 && afflictionIconContainer.Content.CountChildren > 0) + if (selectedLimbIndex > -1 && afflictionIconList.Content.CountChildren > 0) { LimbHealth limbHealth = limbHealths[selectedLimbIndex]; if (limbHealth?.IndicatorSprite != null) { Rectangle selectedLimbArea = GetLimbHighlightArea(limbHealth, drawArea); GUI.DrawLine(spriteBatch, - new Vector2(afflictionIconContainer.Rect.X, afflictionIconContainer.Rect.Y), + new Vector2(afflictionIconList.Rect.X, afflictionIconList.Rect.Y), selectedLimbArea.Center.ToVector2(), Color.LightGray * 0.5f, width: 4); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 9d6be3cbd..f3350b2ed 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -1823,7 +1823,18 @@ namespace Barotrauma Identifier afflictionId = affliction.TranslationIdentifier; addIfMissing($"afflictionname.{afflictionId}".ToIdentifier(), language); - addIfMissing($"afflictiondescription.{afflictionId}".ToIdentifier(), language); + + if (affliction.Descriptions.Any()) + { + foreach (var description in affliction.Descriptions) + { + addIfMissing(description.TextTag, language); + } + } + else + { + addIfMissing($"afflictiondescription.{afflictionId}".ToIdentifier(), language); + } } foreach (var talentTree in TalentTree.JobTalentTrees) diff --git a/Barotrauma/BarotraumaClient/ClientSource/EventInput/EventInput.cs b/Barotrauma/BarotraumaClient/ClientSource/EventInput/EventInput.cs index 474f8c570..fdf54af3f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/EventInput/EventInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/EventInput/EventInput.cs @@ -70,6 +70,7 @@ namespace EventInput public delegate void CharEnteredHandler(object sender, CharacterEventArgs e); public delegate void KeyEventHandler(object sender, KeyEventArgs e); + public delegate void EditingTextHandler(object sender, TextEditingEventArgs e); public static class EventInput { @@ -88,6 +89,15 @@ namespace EventInput /// public static event KeyEventHandler KeyUp; + +#if !WINDOWS + /// + /// Raised when the user is editing text and IME is in progress. + /// Windows build uses ImeSharp instead because SDL2's IME implementation is broken on Windows (https://github.com/libsdl-org/SDL/issues/2243) + /// + public static event EditingTextHandler EditingText; +#endif + static bool initialized; /// @@ -102,6 +112,9 @@ namespace EventInput } window.TextInput += ReceiveInput; +#if !WINDOWS + window.TextEditing += ReceiveTextEditing; +#endif initialized = true; } @@ -112,6 +125,13 @@ namespace EventInput KeyDown?.Invoke(sender, new KeyEventArgs(e.Key)); } +#if !WINDOWS + private static void ReceiveTextEditing(object sender, TextEditingEventArgs e) + { + EditingText?.Invoke(sender, e); + } +#endif + public static void OnCharEntered(char character) { CharEntered?.Invoke(null, new CharacterEventArgs(character, 0)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/EventInput/KeyboardDispatcher.cs b/Barotrauma/BarotraumaClient/ClientSource/EventInput/KeyboardDispatcher.cs index b52e392a1..f18dbcd73 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/EventInput/KeyboardDispatcher.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/EventInput/KeyboardDispatcher.cs @@ -1,8 +1,4 @@ -using System; -using System.Threading; -#if WINDOWS -using System.Windows; -#endif +using Barotrauma; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; @@ -15,6 +11,11 @@ namespace EventInput void ReceiveCommandInput(char command); void ReceiveSpecialInput(Keys key); +#if !WINDOWS + /// Windows build uses ImeSharp instead because SDL2's IME implementation is broken on Windows (https://github.com/libsdl-org/SDL/issues/2243) + void ReceiveEditingInput(string text, int start); +#endif + bool Selected { get; set; } //or Focused } @@ -25,14 +26,26 @@ namespace EventInput EventInput.Initialize(window); EventInput.CharEntered += EventInput_CharEntered; EventInput.KeyDown += EventInput_KeyDown; - } +#if !WINDOWS + EventInput.EditingText += EventInput_TextEditing; +#endif + /* + * SDL by default starts in a state where it accepts IME inputs + * this is bad because this blocks keybinds since the IME thinks + * it's typing in a text box and not forwarding keybinds to the game. + */ + TextInput.StopTextInput(); + } +#if !WINDOWS + public void EventInput_TextEditing(object sender, TextEditingEventArgs e) + { + _subscriber?.ReceiveEditingInput(e.Text, e.Start); + } +#endif public void EventInput_KeyDown(object sender, KeyEventArgs e) { - if (_subscriber == null) - return; - - _subscriber.ReceiveSpecialInput(e.KeyCode); + _subscriber?.ReceiveSpecialInput(e.KeyCode); } void EventInput_CharEntered(object sender, CharacterEventArgs e) @@ -74,12 +87,25 @@ namespace EventInput get { return _subscriber; } set { - if (_subscriber == value) return; - if (_subscriber != null) + if (_subscriber == value) { return; } + + if (_subscriber is GUITextBox) + { + TextInput.StopTextInput(); _subscriber.Selected = false; + } + + if (value is GUITextBox box) + { + TextInput.SetTextInputRect(box.Rect); + TextInput.StartTextInput(); + } + _subscriber = value; if (value != null) + { value.Selected = true; + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/CheckObjectiveAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/CheckObjectiveAction.cs new file mode 100644 index 000000000..ce251c05a --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/CheckObjectiveAction.cs @@ -0,0 +1,37 @@ +using Barotrauma.Tutorials; +using Segment = Barotrauma.ObjectiveManager.Segment; + +namespace Barotrauma; + +partial class CheckObjectiveAction : BinaryOptionAction +{ + public enum CheckType + { + Added, + Completed + } + + [Serialize(CheckType.Completed, IsPropertySaveable.Yes)] + public CheckType Type { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Identifier { get; set; } + + partial void DetermineSuccessProjSpecific(ref bool success) + { + success = false; + if (Identifier.IsEmpty) + { + success = ObjectiveManager.AllActiveObjectivesCompleted(); + } + else if (ObjectiveManager.GetObjective(Identifier) is Segment segment) + { + success = Type switch + { + CheckType.Added => true, + CheckType.Completed => segment.IsCompleted, + _ => false + }; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs index 865634dd1..18e93bfbd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs @@ -323,7 +323,10 @@ namespace Barotrauma AlwaysOverrideCursor = true }; - LocalizedString translatedText = TextManager.ParseInputTypes(TextManager.Get(text)).Fallback(text); + LocalizedString translatedText = speaker?.DisplayName is not null ? + TextManager.GetWithVariable(text, "[speakername]", speaker?.DisplayName) : + TextManager.Get(text); + translatedText = TextManager.ParseInputTypes(translatedText).Fallback(text); if (speaker?.Info != null && drawChathead) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/MessageBoxAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/MessageBoxAction.cs index b75a1af4a..2cf27ab55 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/MessageBoxAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/MessageBoxAction.cs @@ -11,11 +11,13 @@ partial class MessageBoxAction : EventAction if (Type == ActionType.Create || Type == ActionType.ConnectObjective) { CreateMessageBox(); - if (!ObjectiveTag.IsEmpty && GameMain.GameSession?.GameMode is TutorialMode tutorialMode) + if (!ObjectiveTag.IsEmpty) { Identifier id = Identifier.IfEmpty(Text); - var segment = Tutorial.Segment.CreateMessageBoxSegment(id, ObjectiveTag, CreateMessageBox); - tutorialMode.Tutorial?.TriggerTutorialSegment(segment, connectObjective: Type == ActionType.ConnectObjective); + var segment = ObjectiveManager.Segment.CreateMessageBoxSegment(id, ObjectiveTag, CreateMessageBox); + segment.CanBeCompleted = ObjectiveCanBeCompleted; + segment.ParentId = ParentObjectiveId; + ObjectiveManager.TriggerTutorialSegment(segment, connectObjective: Type == ActionType.ConnectObjective); } } else if (Type == ActionType.Close) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialSegmentAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialSegmentAction.cs index ab9b097d3..6420009e8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialSegmentAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialSegmentAction.cs @@ -1,50 +1,43 @@ -using Barotrauma.Tutorials; - namespace Barotrauma; partial class TutorialSegmentAction : EventAction { - private Tutorial.Segment segment; + private ObjectiveManager.Segment segment; partial void UpdateProjSpecific() { // Only need to create the segment when it's being triggered (otherwise the tutorial already has the segment instance) if (Type == SegmentActionType.Trigger) { - segment = Tutorial.Segment.CreateInfoBoxSegment(Identifier, ObjectiveTag, AutoPlayVideo ? Tutorials.AutoPlayVideo.Yes : Tutorials.AutoPlayVideo.No, - new Tutorial.Segment.Text(TextTag, Width, Height, Anchor.Center), - new Tutorial.Segment.Video(VideoFile, TextTag, Width, Height)); + segment = ObjectiveManager.Segment.CreateInfoBoxSegment(Identifier, ObjectiveTag, AutoPlayVideo ? Tutorials.AutoPlayVideo.Yes : Tutorials.AutoPlayVideo.No, + new ObjectiveManager.Segment.Text(TextTag, Width, Height, Anchor.Center), + new ObjectiveManager.Segment.Video(VideoFile, TextTag, Width, Height)); } else if (Type == SegmentActionType.Add) { - segment = Tutorial.Segment.CreateObjectiveSegment(Identifier, !ObjectiveTag.IsEmpty ? ObjectiveTag : Identifier); + segment = ObjectiveManager.Segment.CreateObjectiveSegment(Identifier, !ObjectiveTag.IsEmpty ? ObjectiveTag : Identifier); } - if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode) + if (segment is not null) { - if (tutorialMode.Tutorial is Tutorial tutorial) - { - switch (Type) - { - case SegmentActionType.Trigger: - case SegmentActionType.Add: - tutorial.TriggerTutorialSegment(segment); - break; - case SegmentActionType.Complete: - tutorial.CompleteTutorialSegment(Identifier); - break; - case SegmentActionType.Remove: - tutorial.RemoveTutorialSegment(Identifier); - break; - case SegmentActionType.CompleteAndRemove: - tutorial.CompleteTutorialSegment(Identifier); - tutorial.RemoveTutorialSegment(Identifier); - break; - } - } + segment.CanBeCompleted = CanBeCompleted; + segment.ParentId = ParentObjectiveId; } - else + switch (Type) { - DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\": attempting to use TutorialSegmentAction during a non-Tutorial game mode!"); + case SegmentActionType.Trigger: + case SegmentActionType.Add: + ObjectiveManager.TriggerTutorialSegment(segment); + break; + case SegmentActionType.Complete: + ObjectiveManager.CompleteTutorialSegment(Identifier); + break; + case SegmentActionType.Remove: + ObjectiveManager.RemoveTutorialSegment(Identifier); + break; + case SegmentActionType.CompleteAndRemove: + ObjectiveManager.CompleteTutorialSegment(Identifier); + ObjectiveManager.RemoveTutorialSegment(Identifier); + break; } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index fb4fd861b..2a78ce8ef 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -255,7 +255,17 @@ namespace Barotrauma ScreenChanged = false; } - updateList.ForEach(c => c.DrawAuto(spriteBatch)); + foreach (GUIComponent c in updateList) + { + c.DrawAuto(spriteBatch); + } + + // always draw IME preview on top of everything else + foreach (GUIComponent c in updateList) + { + if (c is not GUITextBox box) { continue; } + box.DrawIMEPreview(spriteBatch); + } if (ScreenOverlayColor.A > 0.0f) { @@ -1251,6 +1261,10 @@ namespace Barotrauma UpdateMessages(deltaTime); UpdateSavingIndicator(deltaTime); } + +#if WINDOWS + GUITextBox.UpdateIME(); +#endif } public static void UpdateGUIMessageBoxesOnly(float deltaTime) @@ -1357,7 +1371,7 @@ namespace Barotrauma } } - #region Element drawing +#region Element drawing private static readonly List usedIndicatorAngles = new List(); @@ -1843,9 +1857,9 @@ namespace Barotrauma Vector2 pos = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) - new Vector2(HUDLayoutSettings.Padding) - 2 * Scale * sheet.FrameSize.ToVector2(); sheet.Draw(spriteBatch, (int)Math.Floor(savingIndicatorSpriteIndex), pos, savingIndicatorColor, origin: Vector2.Zero, rotate: 0.0f, scale: new Vector2(Scale)); } - #endregion +#endregion - #region Element creation +#region Element creation public static Texture2D CreateCircle(int radius, bool filled = false) { @@ -2217,9 +2231,9 @@ namespace Barotrauma return msgBox; } - #endregion +#endregion - #region Element positioning +#region Element positioning private static List CreateElements(int count, RectTransform parent, Func constructor, Vector2? relativeSize = null, Point? absoluteSize = null, Anchor anchor = Anchor.TopLeft, Pivot? pivot = null, Point? minSize = null, Point? maxSize = null, @@ -2418,9 +2432,9 @@ namespace Barotrauma } } - #endregion +#endregion - #region Misc +#region Misc public static void TogglePauseMenu() { if (Screen.Selected == GameMain.MainMenuScreen) { return; } @@ -2658,6 +2672,6 @@ namespace Barotrauma if (!isSavingIndicatorEnabled) { return; } timeUntilSavingIndicatorDisabled = delay; } - #endregion +#endregion } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index 926e5520b..2d8523b58 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -9,6 +9,7 @@ using Barotrauma.IO; using RestSharp; using System.Net; using System.Collections.Immutable; +using Barotrauma.Tutorials; namespace Barotrauma { @@ -736,7 +737,7 @@ namespace Barotrauma public static void DrawToolTip(SpriteBatch spriteBatch, RichString toolTip, Vector2 pos) { - if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode && tutorialMode.Tutorial.ContentRunning) { return; } + if (ObjectiveManager.ContentRunning) { return; } int width = (int)(400 * GUI.Scale); int height = (int)(18 * GUI.Scale); @@ -759,7 +760,7 @@ namespace Barotrauma public static void DrawToolTip(SpriteBatch spriteBatch, RichString toolTip, Rectangle targetElement) { - if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode && tutorialMode.Tutorial.ContentRunning) { return; } + if (ObjectiveManager.ContentRunning) { return; } int width = (int)(400 * GUI.Scale); int height = (int)(18 * GUI.Scale); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs index a8ca188cc..465e5d771 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs @@ -112,6 +112,10 @@ namespace Barotrauma public void ReceiveTextInput(string text) { } public void ReceiveCommandInput(char command) { } +#if !WINDOWS + public void ReceiveEditingInput(string text, int start) { } +#endif + public void ReceiveSpecialInput(Keys key) { switch (key) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs index 71173d75d..fc0b5c098 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs @@ -1349,6 +1349,10 @@ namespace Barotrauma public void ReceiveTextInput(string text) { } public void ReceiveCommandInput(char command) { } +#if !WINDOWS + public void ReceiveEditingInput(string text, int start) { } +#endif + public void ReceiveSpecialInput(Keys key) { switch (key) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs index 3aa1cff1c..81ad4ac35 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs @@ -24,7 +24,8 @@ namespace Barotrauma InGame, Vote, Hint, - Tutorial + Tutorial, + Warning // Keep this last so that it's always drawn in front } private bool IsAnimated => type == Type.InGame || type == Type.Hint || type == Type.Tutorial; @@ -84,8 +85,8 @@ namespace Barotrauma public static GUIComponent VisibleBox => MessageBoxes.LastOrDefault(); - public GUIMessageBox(LocalizedString headerText, LocalizedString text, Vector2? relativeSize = null, Point? minSize = null) - : this(headerText, text, new LocalizedString[] { "OK" }, relativeSize, minSize) + public GUIMessageBox(LocalizedString headerText, LocalizedString text, Vector2? relativeSize = null, Point? minSize = null, Type type = Type.Default) + : this(headerText, text, new LocalizedString[] { "OK" }, relativeSize, minSize, type: type) { this.Buttons[0].OnClicked = Close; } @@ -147,7 +148,7 @@ namespace Barotrauma Tag = tag.ToIdentifier(); #warning TODO: These should be broken into separate methods at least - if (type == Type.Default || type == Type.Vote) + if (type == Type.Default || type == Type.Vote || type == Type.Warning) { Content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.85f), InnerFrame.RectTransform, Anchor.Center)) { AbsoluteSpacing = 5 }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs index e4a94bc56..168f53a7e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Linq; namespace Barotrauma @@ -285,8 +286,8 @@ namespace Barotrauma /// This is the new constructor. /// If the rectT height is set 0, the height is calculated from the text. /// - public GUITextBlock(RectTransform rectT, RichString text, Color? textColor = null, GUIFont font = null, - Alignment textAlignment = Alignment.Left, bool wrap = false, string style = "", Color? color = null) + public GUITextBlock(RectTransform rectT, RichString text, Color? textColor = null, GUIFont font = null, + Alignment textAlignment = Alignment.Left, bool wrap = false, string style = "", Color? color = null) : base(style, rectT) { if (color.HasValue) @@ -551,6 +552,8 @@ namespace Barotrauma if (TextGetter != null) { Text = TextGetter(); } + string textToShow = Censor ? censoredText : (Wrap ? wrappedText.Value : text.SanitizedValue); + Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle; if (overflowClipActive) { @@ -561,7 +564,7 @@ namespace Barotrauma spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); } - if (!text.IsNullOrEmpty()) + if (!string.IsNullOrEmpty(textToShow)) { Vector2 pos = rect.Location.ToVector2() + textPos + TextOffset; if (RoundToNearestPixel) @@ -570,7 +573,8 @@ namespace Barotrauma pos.Y = (int)pos.Y; } - Color currentTextColor = State == ComponentState.Hover || State == ComponentState.HoverSelected ? HoverTextColor : TextColor; + Color currentTextColor = State is ComponentState.Hover or ComponentState.HoverSelected ? HoverTextColor : TextColor; + if (!enabled) { currentTextColor = disabledTextColor; @@ -582,7 +586,6 @@ namespace Barotrauma if (!HasColorHighlight) { - string textToShow = Censor ? censoredText : (Wrap ? wrappedText.Value : text.SanitizedValue); Color colorToShow = currentTextColor * (currentTextColor.A / 255.0f); if (TextManager.DebugDraw) { @@ -604,10 +607,10 @@ namespace Barotrauma { if (OverrideRichTextDataAlpha) { - RichTextData.Value.ForEach(rt => rt.Alpha = currentTextColor.A / 255.0f); + RichTextData?.ForEach(rt => rt.Alpha = currentTextColor.A / 255.0f); } - Font.DrawStringWithColors(spriteBatch, Censor ? censoredText : (Wrap ? wrappedText : text.SanitizedString).Value, pos, - currentTextColor * (currentTextColor.A / 255.0f), 0.0f, origin, TextScale, SpriteEffects.None, textDepth, RichTextData.Value, alignment: textAlignment, forceUpperCase: ForceUpperCase); + Font.DrawStringWithColors(spriteBatch, textToShow, pos, + currentTextColor * (currentTextColor.A / 255.0f), 0.0f, origin, TextScale, SpriteEffects.None, textDepth, RichTextData, alignment: textAlignment, forceUpperCase: ForceUpperCase); } Strikethrough?.Draw(spriteBatch, (int)Math.Ceiling(TextSize.X / 2f), pos.X, diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index 27a558c5c..dae60140f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -11,7 +11,7 @@ namespace Barotrauma public delegate void TextBoxEvent(GUITextBox sender, Keys key); - public class GUITextBox : GUIComponent, IKeyboardSubscriber + public partial class GUITextBox : GUIComponent, IKeyboardSubscriber { public event TextBoxEvent OnSelected; public event TextBoxEvent OnDeselected; @@ -67,12 +67,12 @@ namespace Barotrauma private int selectionEndIndex; private bool IsLeftToRight => selectionStartIndex <= selectionEndIndex; - private GUICustomComponent caretAndSelectionRenderer; + private readonly GUICustomComponent caretAndSelectionRenderer; private bool mouseHeldInside; private readonly Memento memento = new Memento(); - + // Skip one update cycle, fixes Enter key instantly deselecting the chatbox private bool skipUpdate; @@ -189,6 +189,7 @@ namespace Barotrauma base.Font = value; if (textBlock == null) { return; } textBlock.Font = value; + imePreviewTextHandler.Font = Font; } } @@ -253,6 +254,8 @@ namespace Barotrauma public override bool PlaySoundOnSelect { get; set; } = true; + private readonly IMEPreviewTextHandler imePreviewTextHandler; + public GUITextBox(RectTransform rectT, string text = "", Color? textColor = null, GUIFont font = null, Alignment textAlignment = Alignment.Left, bool wrap = false, string style = "", Color? color = null, bool createClearButton = false, bool createPenIcon = true) : base(style, rectT) @@ -264,6 +267,7 @@ namespace Barotrauma frame = new GUIFrame(new RectTransform(Vector2.One, rectT, Anchor.Center), style, color); GUIStyle.Apply(frame, style == "" ? "GUITextBox" : style); textBlock = new GUITextBlock(new RectTransform(Vector2.One, frame.RectTransform, Anchor.CenterLeft), text ?? "", textColor, font, textAlignment, wrap); + imePreviewTextHandler = new IMEPreviewTextHandler(textBlock.Font); GUIStyle.Apply(textBlock, "", this); if (font != null) { textBlock.Font = font; } CaretEnabled = true; @@ -295,18 +299,17 @@ namespace Barotrauma textBlock.RectTransform.MaxSize = new Point(frame.Rect.Width - icon.Rect.Height - clearButtonWidth - icon.RectTransform.AbsoluteOffset.X * 2, int.MaxValue); } Font = textBlock.Font; - Enabled = true; - rectT.SizeChanged += () => + rectT.SizeChanged += () => { if (icon != null) { textBlock.RectTransform.MaxSize = new Point(frame.Rect.Width - icon.Rect.Height - icon.RectTransform.AbsoluteOffset.X * 2, int.MaxValue); } - caretPosDirty = true; + caretPosDirty = true; }; rectT.ScaleChanged += () => { if (icon != null) { textBlock.RectTransform.MaxSize = new Point(frame.Rect.Width - icon.Rect.Height - icon.RectTransform.AbsoluteOffset.X * 2, int.MaxValue); } - caretPosDirty = true; + caretPosDirty = true; }; } @@ -381,14 +384,16 @@ namespace Barotrauma { GUI.KeyboardDispatcher.Subscriber = null; } + OnDeselected?.Invoke(this, Keys.None); + imePreviewTextHandler.Reset(); } public override void Flash(Color? color = null, float flashDuration = 1.5f, bool useRectangleFlash = false, bool useCircularFlash = false, Vector2? flashRectOffset = null) { frame.Flash(color, flashDuration, useRectangleFlash, useCircularFlash, flashRectOffset); } - + protected override void Update(float deltaTime) { if (!Visible) return; @@ -673,6 +678,18 @@ namespace Barotrauma break; } } +#if !WINDOWS + public void ReceiveEditingInput(string text, int start) + { + if (string.IsNullOrEmpty(text)) + { + if (start is 0) { imePreviewTextHandler.Reset(); } + return; + } + + imePreviewTextHandler.UpdateText(text, start); + } +#endif public void ReceiveSpecialInput(Keys key) { @@ -864,6 +881,24 @@ namespace Barotrauma } } + public void DrawIMEPreview(SpriteBatch spriteBatch) + { + if (!imePreviewTextHandler.HasText) { return; } + + Vector2 imePosition = CaretScreenPos; + int inflate = GUI.IntScale(3); + + RectangleF rect = new RectangleF(imePosition, imePreviewTextHandler.TextSize); + rect.Inflate(inflate, inflate); + + RectangleF borderRect = rect; + borderRect.Inflate(1, 1); + + GUI.DrawFilledRectangle(spriteBatch, borderRect, Color.White, depth: 0.02f); + GUI.DrawFilledRectangle(spriteBatch, rect, Color.Black, depth: 0.01f); + Font.DrawString(spriteBatch, imePreviewTextHandler.PreviewText, imePosition, GUIStyle.Orange, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, 0, alignment: textBlock.TextAlignment, forceUpperCase: textBlock.ForceUpperCase); + } + private void CalculateSelection() { string textDrawn = Censor ? textBlock.CensoredText : WrappedText; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBoxIME.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBoxIME.cs new file mode 100644 index 000000000..ca20e30cf --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBoxIME.cs @@ -0,0 +1,86 @@ +using ImeSharp; +using Microsoft.Xna.Framework; +using System; + +namespace Barotrauma; + +/// +/// A class for handling Input Method Editor (used for inputting e.g. Chinese and Japanese text) +/// +public partial class GUITextBox : GUIComponent +{ + private static bool initialized; + + public static GUIFrame IMEWindow { get; set; } + public static GUITextBlock IMETextBlock { get; set; } + + public static void UpdateIME() + { + if (!initialized) { InitializeIME(); } + if (GUI.KeyboardDispatcher.Subscriber is GUITextBox { Selected: true }) + { + IMEWindow?.AddToGUIUpdateList(order: 10); + } + } + + private static void InitializeIME() + { + InputMethod.Initialize(GameMain.Instance.Window.Hwnd, false); + InputMethod.TextCompositionCallback = OnTextComposition; + InputMethod.CommitTextCompositionCallback = OnCommitTextComposition; + InputMethod.Enabled = true; + IMEWindow = new GUIFrame(new RectTransform(new Point(GUI.IntScale(300), GUI.IntScale(300)), GUI.Canvas), "InnerFrame") { CanBeFocused = false, Visible = false }; + IMETextBlock = new GUITextBlock(new RectTransform(Vector2.One, IMEWindow.RectTransform), "") { CanBeFocused = false }; + + initialized = true; + } + + private static void OnTextComposition(IMEString compositionText, int cursorPosition, IMEString[] candidateList, int candidatePageStart, int candidatePageSize, int candidateSelection) + { + if (GUI.KeyboardDispatcher.Subscriber is not GUITextBox { Selected: true } textBox) { return; } + IMEWindow.Visible = true; + string text = compositionText.ToString().Insert(cursorPosition, "|"); + if (candidateList != null) + { + text += "\n"; + for (int i = 0; i < candidatePageSize; i++) + { + string candidateStr = $"\t{candidatePageStart + i + 1} {candidateList[i]}"; + if (candidateSelection == i) + { + candidateStr = $" ‖color:{XMLExtensions.ToStringHex(Color.White)}‖{candidateStr}‖end‖"; + } + candidateStr += "\n"; + text += candidateStr; + } + } + IMETextBlock.Text = RichString.Rich(text); + + IMEWindow.RectTransform.NonScaledSize = new Point( + Math.Max(IMEWindow.Rect.Width, (int)IMETextBlock.TextSize.X + GUI.IntScale(32)), + (int)IMETextBlock.TextSize.Y); + + Point windowPos = new Point(textBox.Rect.X, textBox.Rect.Bottom); + if (windowPos.Y + IMEWindow.Rect.Height > GameMain.GraphicsHeight) + { + windowPos.Y = textBox.Rect.Y - IMEWindow.Rect.Height; + } + IMEWindow.RectTransform.ScreenSpaceOffset = windowPos; + } + + private static void OnCommitTextComposition(string text) + { + if (IMEWindow.Visible) + { + foreach (char c in text) + { + if (!char.IsControl(c)) + { + GUI.KeyboardDispatcher.Subscriber?.ReceiveTextInput(c); + } + } + } + IMEWindow.Visible = false; + } +} + diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs index ded2b870e..dec3485bb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs @@ -65,7 +65,7 @@ namespace Barotrauma get; private set; } - public static Rectangle AfflictionAreaLeft + public static Rectangle HealthBarAfflictionArea { get; private set; } @@ -143,7 +143,7 @@ namespace Barotrauma } int healthBarHeight = (int)(50f * GUI.Scale); HealthBarArea = new Rectangle(BottomRightInfoArea.Right - healthBarWidth + (int)Math.Floor(1 / GUI.Scale), BottomRightInfoArea.Y - healthBarHeight + GUI.IntScale(10), healthBarWidth, healthBarHeight); - AfflictionAreaLeft = new Rectangle(HealthBarArea.X, HealthBarArea.Y - Padding - afflictionAreaHeight, HealthBarArea.Width, afflictionAreaHeight); + HealthBarAfflictionArea = new Rectangle(HealthBarArea.X, HealthBarArea.Y - Padding - afflictionAreaHeight, HealthBarArea.Width, afflictionAreaHeight); int messageAreaWidth = GameMain.GraphicsWidth / 3; @@ -173,7 +173,7 @@ namespace Barotrauma int objectiveListAreaX = HealthWindowAreaLeft.Right + Padding; int objectiveListAreaY = ButtonAreaTop.Bottom + Padding; - TutorialObjectiveListArea = new Rectangle(objectiveListAreaX, objectiveListAreaY, (GameMain.GraphicsWidth - Padding) - objectiveListAreaX, (AfflictionAreaLeft.Top - Padding) - objectiveListAreaY); + TutorialObjectiveListArea = new Rectangle(objectiveListAreaX, objectiveListAreaY, (GameMain.GraphicsWidth - Padding) - objectiveListAreaX, (HealthBarAfflictionArea.Top - Padding) - objectiveListAreaY); int votingAreaWidth = (int)(400 * GUI.Scale); int votingAreaX = GameMain.GraphicsWidth - Padding - votingAreaWidth; @@ -193,7 +193,7 @@ namespace Barotrauma DrawRectangle(CrewArea, Color.Blue * 0.5f); DrawRectangle(ChatBoxArea, Color.Cyan * 0.5f); DrawRectangle(HealthBarArea, Color.Red * 0.5f); - DrawRectangle(AfflictionAreaLeft, Color.Red * 0.5f); + DrawRectangle(HealthBarAfflictionArea, Color.Red * 0.5f); DrawRectangle(InventoryAreaLower, Color.Yellow * 0.5f); DrawRectangle(HealthWindowAreaLeft, Color.Red * 0.5f); DrawRectangle(BottomRightInfoArea, Color.Green * 0.5f); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/IMEPreviewText.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/IMEPreviewText.cs new file mode 100644 index 000000000..96f137a08 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/IMEPreviewText.cs @@ -0,0 +1,54 @@ +#nullable enable + +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + public sealed class IMEPreviewTextHandler + { + public string PreviewText { get; private set; } = string.Empty; + public Vector2 TextSize { get; private set; } + public bool HasText => !string.IsNullOrEmpty(PreviewText); + + // This has to be settable because for some reason we update the font of GUITextBox in some places + public GUIFont Font { get; set; } + + public IMEPreviewTextHandler(GUIFont font) + { + Font = font; + } + + public void Reset() + { + TextSize = Vector2.Zero; + PreviewText = string.Empty; + } + + public void UpdateText(string text, int start) + { + if (string.IsNullOrEmpty(text) && start is 0) + { + Reset(); + return; + } + + int totalLength = start + text.Length; + string newText = PreviewText; + if (newText.Length > totalLength) + { + newText = newText[..totalLength]; + } + + if (totalLength > newText.Length) + { + // this is required for some reason on Windows + // my guess is that the order which TextEditing events come thru is not guaranteed + newText = newText.PadRight(totalLength); + } + + newText = newText.Remove(start, text.Length).Insert(start, text); + PreviewText = newText; + TextSize = Font.MeasureString(PreviewText); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs index 54373bcff..6e7e2e637 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs @@ -12,7 +12,7 @@ using PlayerBalanceElement = Barotrauma.CampaignUI.PlayerBalanceElement; namespace Barotrauma { [SuppressMessage("ReSharper", "UnusedVariable")] - internal class MedicalClinicUI + internal sealed class MedicalClinicUI { private enum ElementState { @@ -127,12 +127,14 @@ namespace Barotrauma { public readonly GUIComponent Panel; public readonly GUIListBox HealList; + public readonly GUIComponent TreatAllButton; public readonly List HealElements; - public CrewHealList(GUIListBox healList, GUIComponent panel) + public CrewHealList(GUIListBox healList, GUIComponent panel, GUIComponent treatAllButton) { Panel = panel; HealList = healList; + TreatAllButton = treatAllButton; HealElements = new List(); } } @@ -179,7 +181,7 @@ namespace Barotrauma private PopupAfflictionList? selectedCrewAfflictionList; private bool isWaitingForServer; private const float refreshTimerMax = 3f; - private float refreshTimer = 0; + private float refreshTimer; private PlayerBalanceElement? playerBalanceElement; @@ -196,7 +198,7 @@ namespace Barotrauma { new GUIButton(new RectTransform(new Vector2(0.2f, 0.1f), parent.RectTransform, Anchor.TopCenter), "Recreate UI - NOT PRESENT IN RELEASE!") { - OnClicked = (_, __) => + OnClicked = (_, _) => { parent.ClearChildren(); CreateUI(); @@ -254,7 +256,7 @@ namespace Barotrauma continue; } - CreatePendingHealElement(healList.HealList.Content, crewMember, healList, Array.Empty()); + CreatePendingHealElement(healList.HealList.Content, crewMember, healList, ImmutableArray.Empty); } // check if there are elements that the crew doesn't have @@ -309,7 +311,7 @@ namespace Barotrauma private void UpdateCrewPanel() { - if (!(crewHealList is { } healList)) { return; } + if (crewHealList is not { } healList) { return; } ImmutableArray crew = MedicalClinic.GetCrewCharacters(); @@ -334,12 +336,21 @@ namespace Barotrauma healList.HealList.Content.RemoveChild(element.UIElement); } - IEnumerable orderedList = healList.HealElements.OrderBy(element => element.Target.Character?.HealthPercentage ?? 100); + IEnumerable orderedList = healList.HealElements.OrderBy(static element => element.Target.Character?.HealthPercentage ?? 100); foreach (CrewElement element in orderedList) { element.UIElement.SetAsLastChild(); } + + healList.TreatAllButton.Enabled = false; + foreach (CrewElement element in healList.HealElements) + { + if (element.Afflictions.Count is 0) { continue; } + + healList.TreatAllButton.Enabled = true; + break; + } } private static void UpdateAfflictionList(CrewElement healElement) @@ -350,7 +361,7 @@ namespace Barotrauma // sum up all the afflictions and their strengths Dictionary afflictionAndStrength = new Dictionary(); - foreach (Affliction affliction in health.GetAllAfflictions().Where(a => MedicalClinic.IsHealable(a))) + foreach (Affliction affliction in health.GetAllAfflictions().Where(MedicalClinic.IsHealable)) { if (afflictionAndStrength.TryGetValue(affliction.Prefab, out float strength)) { @@ -446,8 +457,8 @@ namespace Barotrauma }; GUILayoutGroup clinicLabelLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), clinicContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); - GUIImage clinicIcon = new GUIImage(new RectTransform(Vector2.One, clinicLabelLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "CrewManagementHeaderIcon", scaleToFit: true); - GUITextBlock clinicLabel = new GUITextBlock(new RectTransform(Vector2.One, clinicLabelLayout.RectTransform), TextManager.Get("medicalclinic.medicalclinic"), font: GUIStyle.LargeFont); + new GUIImage(new RectTransform(Vector2.One, clinicLabelLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "CrewManagementHeaderIcon", scaleToFit: true); + new GUITextBlock(new RectTransform(Vector2.One, clinicLabelLayout.RectTransform), TextManager.Get("medicalclinic.medicalclinic"), font: GUIStyle.LargeFont); GUIFrame clinicBackground = new GUIFrame(new RectTransform(Vector2.One, clinicContent.RectTransform)); @@ -480,22 +491,24 @@ namespace Barotrauma 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); + GUIButton treatAllButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), clinicContainer.RectTransform), TextManager.Get("medicalclinic.treateveryone")) + { + OnClicked = (_, _) => + { + isWaitingForServer = true; + medicalClinic.TreatAllButtonAction(OnReceived); + return true; + } + }; + + crewHealList = new CrewHealList(crewList, parent, treatAllButton); + + void OnReceived(MedicalClinic.CallbackOnlyRequest obj) + { + isWaitingForServer = false; + } } private void CreateCrewEntry(GUIComponent parent, CrewHealList healList, CharacterInfo info, GUIComponent panel) @@ -525,9 +538,9 @@ namespace Barotrauma TextColor = GUIStyle.Red }; - MedicalClinic.NetCrewMember member = new MedicalClinic.NetCrewMember { CharacterInfo = info, Afflictions = Array.Empty() }; + MedicalClinic.NetCrewMember member = new MedicalClinic.NetCrewMember(info); - crewBackground.OnClicked = (_, __) => + crewBackground.OnClicked = (_, _) => { SelectCharacter(member, new Vector2(panel.Rect.Right, crewBackground.Rect.Top)); return true; @@ -618,7 +631,7 @@ namespace Barotrauma pendingHealList = list; } - private void CreatePendingHealElement(GUIComponent parent, MedicalClinic.NetCrewMember crewMember, PendingHealList healList, MedicalClinic.NetAffliction[] afflictions) + private void CreatePendingHealElement(GUIComponent parent, MedicalClinic.NetCrewMember crewMember, PendingHealList healList, ImmutableArray afflictions) { CharacterInfo? healInfo = crewMember.FindCharacterInfo(MedicalClinic.GetCrewCharacters()); if (healInfo is null) { return; } @@ -803,7 +816,7 @@ namespace Barotrauma } allComponents.Add(treatAllButton); - treatAllButton.OnClicked = (_, __) => + treatAllButton.OnClicked = (_, _) => { ImmutableArray afflictions = request.Afflictions.Where(a => !medicalClinic.IsAfflictionPending(crewMember, a)).ToImmutableArray(); if (!afflictions.Any()) { return true; } @@ -873,9 +886,13 @@ namespace Barotrauma { RelativeSpacing = 0.05f }; - GUITextBlock descriptionBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.6f), bottomTextLayout.RectTransform), prefab.Description, font: GUIStyle.SmallFont, wrap: true) + LocalizedString description = affliction.Prefab.GetDescription(affliction.Strength, AfflictionPrefab.Description.TargetType.OtherCharacter); + GUITextBlock descriptionBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.6f), bottomTextLayout.RectTransform), + description, + font: GUIStyle.SmallFont, + wrap: true) { - ToolTip = prefab.Description + ToolTip = description }; bool truncated = false; while (descriptionBlock.TextSize.Y > descriptionBlock.Rect.Height && descriptionBlock.WrappedText.Contains('\n')) @@ -919,10 +936,9 @@ namespace Barotrauma } else { - MedicalClinic.NetCrewMember newMember = new MedicalClinic.NetCrewMember + MedicalClinic.NetCrewMember newMember = crewMember with { - CharacterInfoID = crewMember.CharacterInfoID, - Afflictions = Array.Empty() + Afflictions = ImmutableArray.Empty }; existingMember = newMember; @@ -936,7 +952,7 @@ namespace Barotrauma } } - existingMember.Afflictions = existingMember.Afflictions.Concat(afflictions).ToArray(); + existingMember.Afflictions = existingMember.Afflictions.Concat(afflictions).ToImmutableArray(); ToggleElements(ElementState.Disabled, elementsToDisable); medicalClinic.AddPendingButtonAction(existingMember, request => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index b48a3867f..a9a532278 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -745,7 +745,7 @@ namespace Barotrauma } ?? Enumerable.Empty(); foreach (var button in itemCategoryButtons) { - if (!(button.UserData is MapEntityCategory category)) + if (button.UserData is not MapEntityCategory category) { continue; } @@ -1110,7 +1110,7 @@ namespace Barotrauma private void SetPriceGetters(GUIComponent itemFrame, bool buying) { - if (itemFrame == null || !(itemFrame.UserData is PurchasedItem pi)) { return; } + if (itemFrame == null || itemFrame.UserData is not PurchasedItem pi) { return; } if (itemFrame.FindChild("undiscountedprice", recursive: true) is GUITextBlock undiscountedPriceBlock) { @@ -1667,8 +1667,8 @@ namespace Barotrauma { 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 (!subItem.Components.All(c => c is not Holdable h || !h.Attachable || !h.Attached)) { continue; } + if (!subItem.Components.All(c => c is not Wire w || w.Connections.All(c => c == null))) { continue; } if (!ItemAndAllContainersInteractable(subItem)) { continue; } AddOwnedItem(subItem); } @@ -1701,7 +1701,7 @@ namespace Barotrauma void AddOwnedItem(Item item) { - if (!(item?.Prefab.GetPriceInfo(ActiveStore) is PriceInfo priceInfo)) { return; } + if (item?.Prefab.GetPriceInfo(ActiveStore) is not PriceInfo priceInfo) { return; } bool isNonEmpty = !priceInfo.DisplayNonEmpty || item.ConditionPercentage > 5.0f; if (OwnedItems.TryGetValue(item.Prefab, out ItemQuantity itemQuantity)) { @@ -1729,7 +1729,7 @@ namespace Barotrauma private void SetItemFrameStatus(GUIComponent itemFrame, bool enabled) { - if (!(itemFrame?.UserData is PurchasedItem pi)) { return; } + if (itemFrame?.UserData is not PurchasedItem pi) { return; } bool refreshFrameStatus = !pi.IsStoreComponentEnabled.HasValue || pi.IsStoreComponentEnabled.Value != enabled; if (!refreshFrameStatus) { return; } if (itemFrame.FindChild("icon", recursive: true) is GUIImage icon) @@ -1841,11 +1841,7 @@ namespace Barotrauma LocalizedString toolTip = string.Empty; if (purchasedItem.ItemPrefab != null) { - toolTip = purchasedItem.ItemPrefab.Name; - if (!purchasedItem.ItemPrefab.Description.IsNullOrEmpty()) - { - toolTip += $"\n{purchasedItem.ItemPrefab.Description}"; - } + toolTip = purchasedItem.ItemPrefab.GetTooltip(); if (itemQuantity != null) { if (itemQuantity.AllNonEmpty) @@ -1859,7 +1855,7 @@ namespace Barotrauma } } } - itemComponent.ToolTip = toolTip; + itemComponent.ToolTip = RichString.Rich(toolTip); } if (ownedLabel != null) { @@ -2181,7 +2177,7 @@ namespace Barotrauma { needsRefresh = itemsToSellFromSub.Count != prevSubItems.Count || itemsToSellFromSub.Sum(i => i.Quantity) != prevSubItems.Sum(i => i.Quantity) || - itemsToSellFromSub.Any(i => !(prevSubItems.FirstOrDefault(prev => prev.ItemPrefab == i.ItemPrefab) is PurchasedItem prev) || i.Quantity != prev.Quantity) || + itemsToSellFromSub.Any(i => prevSubItems.FirstOrDefault(prev => prev.ItemPrefab == i.ItemPrefab) is not PurchasedItem prev || i.Quantity != prev.Quantity) || prevSubItems.Any(prev => itemsToSellFromSub.None(i => i.ItemPrefab == prev.ItemPrefab)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index 0bdb26de7..21481ef4e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -472,7 +472,7 @@ namespace Barotrauma if (transferService) { subsToShow.AddRange(GameMain.GameSession.OwnedSubmarines); - subsToShow.Sort((x, y) => x.SubmarineClass.CompareTo(y.SubmarineClass)); + subsToShow.Sort(ComparePrice); string currentSubName = CurrentOrPendingSubmarine().Name; int currentIndex = subsToShow.FindIndex(s => s.Name == currentSubName); if (currentIndex != -1) @@ -484,7 +484,11 @@ namespace Barotrauma { 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)); + if (GameMain.GameSession.Campaign?.Map?.CurrentLocation is Location currentLocation) + { + subsToShow.RemoveAll(sub => !currentLocation.IsSubmarineAvailable(sub)); + } + subsToShow.Sort(ComparePrice); } if (transferService) @@ -492,10 +496,14 @@ namespace Barotrauma SetConfirmButtonState(selectedSubmarine != null && selectedSubmarine.Name != CurrentOrPendingSubmarine().Name); } - subsToShow.Sort((x, y) => x.SubmarineClass.CompareTo(y.SubmarineClass)); pageCount = Math.Max(1, (int)Math.Ceiling(subsToShow.Count / (float)submarinesPerPage)); UpdatePaging(); ContentRefreshRequired = false; + + static int ComparePrice(SubmarineInfo x, SubmarineInfo y) + { + return x.Price.CompareTo(y.Price) * 100 + x.Name.CompareTo(y.Name); + } } private SubmarineInfo GetSubToDisplay(int index) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 7518da497..d001fa648 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -208,10 +208,7 @@ namespace Barotrauma } GameSession.UpdateTalentNotificationIndicator(talentPointNotification); - if (SelectedTab is InfoFrameTab.Talents) - { - talentMenu?.Update(); - } + talentMenu?.Update(); if (SelectedTab != InfoFrameTab.Crew) { return; } if (linkedGUIList == null) { return; } @@ -248,10 +245,6 @@ namespace Barotrauma { infoFrame?.AddToGUIUpdateList(); NetLobbyScreen.JobInfoFrame?.AddToGUIUpdateList(); - if (SelectedTab is InfoFrameTab.Talents) - { - talentMenu?.AddToGUIUpdateList(); - } } public static void OnRoundEnded() @@ -404,7 +397,7 @@ namespace Barotrauma CreateSubmarineInfo(infoFrameHolder, Submarine.MainSub); break; case InfoFrameTab.Talents: - talentMenu.CreateGUI(infoFrameHolder); + talentMenu.CreateGUI(infoFrameHolder, Character.Controlled ?? GameMain.Client?.Character); break; } } @@ -958,16 +951,26 @@ namespace Barotrauma if (character != null) { - if (GameMain.Client == null) + if (GameMain.Client is null) { GUIComponent preview = character.Info.CreateInfoFrame(background, false, null); } else { GUIComponent preview = character.Info.CreateInfoFrame(background, false, GetPermissionIcon(GameMain.Client.ConnectedClients.Find(c => c.Character == character))); + GameMain.Client.SelectCrewCharacter(character, preview); if (!character.IsBot && GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign) { CreateWalletFrame(background, character, mpCampaign); } } + + if (background.FindChild(TalentMenu.ManageBotTalentsButtonUserData, recursive: true) is GUIButton { Enabled: true } talentButton) + { + talentButton.OnClicked = (button, o) => + { + talentMenu.CreateGUI(infoFrameHolder, character); + return true; + }; + } } else if (client != null) { @@ -1792,10 +1795,10 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), skillContainer.RectTransform), Math.Floor(skill.Level).ToString("F0"), textAlignment: Alignment.TopRight); - float modifiedSkillLevel = character?.GetSkillLevel(skill.Identifier) ?? skill.Level; + float modifiedSkillLevel = MathF.Floor(character?.GetSkillLevel(skill.Identifier) ?? skill.Level); if (!MathUtils.NearlyEqual(MathF.Floor(modifiedSkillLevel), MathF.Floor(skill.Level))) { - int skillChange = (int)MathF.Floor(modifiedSkillLevel - skill.Level); + int skillChange = (int)MathF.Floor(modifiedSkillLevel - MathF.Floor(skill.Level)); string stringColor = skillChange switch { > 0 => XMLExtensions.ToStringHex(GUIStyle.Green), @@ -1806,7 +1809,7 @@ namespace Barotrauma RichString changeText = RichString.Rich($"(‖color:{stringColor}‖{(skillChange > 0 ? "+" : string.Empty) + skillChange}‖color:end‖)"); new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), skillContainer.RectTransform), changeText) { Padding = Vector4.Zero }; } - //skillContainer.Recalculate(); + skillContainer.Recalculate(); } parent.RecalculateChildren(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs index a3151bf46..cdb5059d9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs @@ -5,15 +5,18 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Barotrauma.Extensions; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; using static Barotrauma.TalentTree; -using static Barotrauma.TalentTree.TalentTreeStageState; +using static Barotrauma.TalentTree.TalentStages; namespace Barotrauma { - internal readonly record struct TalentButton(GUIButton Button, - GUIComponent IconComponent, + internal readonly record struct TalentShowCaseButton(ImmutableHashSet Buttons, + GUIComponent IconComponent); + + internal readonly record struct TalentButton(GUIComponent IconComponent, TalentPrefab Prefab) { public Identifier Identifier => Prefab.Identifier; @@ -39,6 +42,11 @@ namespace Barotrauma internal sealed class TalentMenu { + public const string ManageBotTalentsButtonUserData = "managebottalentsbutton"; + + private Character? character; + private CharacterInfo? characterInfo; + private static readonly Color unselectedColor = new Color(240, 255, 255, 225), unselectableColor = new Color(100, 100, 100, 225), pressedColor = new Color(60, 60, 60, 225), @@ -46,8 +54,8 @@ namespace Barotrauma unlockedColor = new Color(24, 37, 31, 255), availableColor = new Color(50, 47, 33, 255); - private static readonly ImmutableDictionary talentStageStyles = - new Dictionary + private static readonly ImmutableDictionary talentStageStyles = + new Dictionary { [Invalid] = new TalentTreeStyle("TalentTreeLocked", lockedColor), [Locked] = new TalentTreeStyle("TalentTreeLocked", lockedColor), @@ -57,10 +65,13 @@ namespace Barotrauma }.ToImmutableDictionary(); private readonly HashSet talentButtons = new HashSet(); + private readonly HashSet talentShowCaseButtons = new HashSet(); private readonly HashSet showCaseTalentFrames = new HashSet(); private readonly HashSet talentCornerIcons = new HashSet(); private HashSet selectedTalents = new HashSet(); + private readonly Queue showCaseClosureQueue = new(); + private GUIListBox? skillListBox; private GUITextBlock? talentPointText; private GUIProgressBar? experienceBar; @@ -70,13 +81,17 @@ namespace Barotrauma private GUIButton? talentApplyButton, talentResetButton; - public void CreateGUI(GUIFrame parent) + public void CreateGUI(GUIFrame parent, Character? targetCharacter) { parent.ClearChildren(); talentButtons.Clear(); + talentShowCaseButtons.Clear(); talentCornerIcons.Clear(); showCaseTalentFrames.Clear(); + character = targetCharacter; + characterInfo = targetCharacter?.Info; + GUIFrame background = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox"); int padding = GUI.IntScale(15); GUIFrame frame = new GUIFrame(new RectTransform(new Point(background.Rect.Width - padding, background.Rect.Height - padding), parent.RectTransform, Anchor.Center), style: null); @@ -89,23 +104,21 @@ namespace Barotrauma Stretch = true }; - Character? controlledCharacter = Character.Controlled; - CharacterInfo? info = controlledCharacter?.Info ?? GameMain.Client?.CharacterInfo; - if (info is null) { return; } + if (characterInfo is null) { return; } - CreateStatPanel(contentLayout, info); + CreateStatPanel(contentLayout, characterInfo); new GUIFrame(new RectTransform(new Vector2(1f, 1f), contentLayout.RectTransform), style: "HorizontalLine"); - if (JobTalentTrees.TryGet(info.Job.Prefab.Identifier, out TalentTree? talentTree)) + if (JobTalentTrees.TryGet(characterInfo.Job.Prefab.Identifier, out TalentTree? talentTree)) { - CreateTalentMenu(contentLayout, info, talentTree!); + CreateTalentMenu(contentLayout, characterInfo, talentTree!); } - CreateFooter(contentLayout, info); + CreateFooter(contentLayout, characterInfo); UpdateTalentInfo(); - if (GameMain.NetworkMember != null) + if (GameMain.NetworkMember != null && IsOwnCharacter(characterInfo)) { CreateMultiplayerCharacterSettings(frame, content); } @@ -164,7 +177,7 @@ namespace Barotrauma OnClicked = (button, o) => { GameMain.Client?.SendCharacterInfo(GameMain.Client.PendingName); - characterSettingsFrame!.Visible = false; + characterSettingsFrame.Visible = false; content.Visible = true; return true; } @@ -179,8 +192,7 @@ namespace Barotrauma new GUICustomComponent(new RectTransform(new Vector2(0.25f, 1f), topLayout.RectTransform), onDraw: (batch, component) => { - float posY = component.Rect.Center.Y - component.Rect.Width / 2; - info.DrawPortrait(batch, new Vector2(component.Rect.X, posY), Vector2.Zero, component.Rect.Width, false, false); + info.DrawPortrait(batch, component.Rect.Location.ToVector2(), Vector2.Zero, component.Rect.Width, false, false); }); GUILayoutGroup nameLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1f), topLayout.RectTransform)) @@ -209,20 +221,22 @@ namespace Barotrauma if (talentsOutsideTree.Any()) { //spacing - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), nameLayout.RectTransform), style: null); + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), nameLayout.RectTransform), style: null); - GUILayoutGroup extraTalentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), nameLayout.RectTransform), childAnchor: Anchor.TopCenter); + GUILayoutGroup extraTalentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.55f), nameLayout.RectTransform), childAnchor: Anchor.TopCenter); - talentPointText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), extraTalentLayout.RectTransform, anchor: Anchor.Center), TextManager.Get("talentmenu.extratalents"), font: GUIStyle.SubHeadingFont); + talentPointText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), extraTalentLayout.RectTransform, anchor: Anchor.Center), TextManager.Get("talentmenu.extratalents"), font: GUIStyle.SubHeadingFont) + { + AutoScaleVertical = true + }; talentPointText.RectTransform.MaxSize = new Point(int.MaxValue, (int)talentPointText.TextSize.Y); - var extraTalentList = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.8f), extraTalentLayout.RectTransform, anchor: Anchor.Center), isHorizontal: true) + var extraTalentList = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.7f), extraTalentLayout.RectTransform, anchor: Anchor.Center), isHorizontal: true) { AutoHideScrollBar = false, ResizeContentToMakeSpaceForScrollBar = false }; extraTalentList.ScrollBar.RectTransform.SetPosition(Anchor.BottomCenter, Pivot.TopCenter); - extraTalentList.RectTransform.MinSize = new Point(0, GUI.IntScale(65)); extraTalentLayout.Recalculate(); extraTalentList.ForceLayoutRecalculation(); @@ -231,7 +245,7 @@ namespace Barotrauma if (extraTalent is null) { continue; } GUIImage talentImg = new GUIImage(new RectTransform(Vector2.One, extraTalentList.Content.RectTransform, scaleBasis: ScaleBasis.BothHeight), sprite: extraTalent.Icon, scaleToFit: true) { - ToolTip = RichString.Rich($"‖color:{Color.White.ToStringHex()}‖{extraTalent.DisplayName}‖color:end‖" + "\n\n" + extraTalent.Description), + ToolTip = RichString.Rich($"‖color:{Color.White.ToStringHex()}‖{extraTalent.DisplayName}‖color:end‖" + "\n\n" + ToolBox.ExtendColorToPercentageSigns(extraTalent.Description.Value)), Color = GUIStyle.Green }; } @@ -298,10 +312,11 @@ namespace Barotrauma TalentOption option = subTree.TalentOptionStages[i]; CreateTalentOption(subTreeLayoutGroup, subTree, i, option, info); } - subTreeLayoutGroup.RectTransform.Resize(new Point(subTreeLayoutGroup.Rect.Width, + subTreeLayoutGroup.RectTransform.Resize(new Point(subTreeLayoutGroup.Rect.Width, subTreeLayoutGroup.Children.Sum(c => c.Rect.Height + subTreeLayoutGroup.AbsoluteSpacing))); subTreeLayoutGroup.RectTransform.MinSize = new Point(subTreeLayoutGroup.Rect.Width, subTreeLayoutGroup.Rect.Height); subTreeLayoutGroup.Recalculate(); + if (subTree.Type == TalentTreeType.Specialization) { talentList.RectTransform.Resize(new Point(talentList.Rect.Width, Math.Max(subTreeLayoutGroup.Rect.Height, talentList.Rect.Height))); @@ -310,15 +325,15 @@ namespace Barotrauma } var specializationList = GetSpecializationList(); - GetSpecializationList().RectTransform.Resize(new Point(specializationList.Content.Children.Sum(c => c.Rect.Width), specializationList.Rect.Height)); + GetSpecializationList().RectTransform.Resize(new Point(specializationList.Content.Children.Sum(static c => c.Rect.Width), specializationList.Rect.Height)); GUITextBlock.AutoScaleAndNormalize(subTreeNames); GUIListBox GetSpecializationList() { - if (mainList.Content.Children.LastOrDefault() is GUIListBox specializationList) + if (mainList.Content.Children.LastOrDefault() is GUIListBox specList) { - return specializationList; + return specList; } GUIListBox newSpecializationList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.5f), mainList.Content.RectTransform, Anchor.TopCenter), isHorizontal: true, style: null); @@ -354,21 +369,24 @@ namespace Barotrauma GUILayoutGroup talentOptionLayoutGroup = new GUILayoutGroup(new RectTransform(Vector2.One, talentOptionCenterGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; HashSet talentOptionIdentifiers = talentOption.TalentIdentifiers.OrderBy(static t => t).ToHashSet(); - bool hasShowcase = talentOption.ShowcaseTalent.TryUnwrap(out Identifier showcaseTalentIdentifier); - GUILayoutGroup showcaseLayout = talentOptionLayoutGroup; + HashSet buttonsToAdd = new(); - if (hasShowcase) + Dictionary> showCaseTalentParents = new(); + Dictionary showCaseTalentButtonsToAdd = new(); + + foreach (var (showCaseTalentIdentifier, talents) in talentOption.ShowCaseTalents) { - talentOptionIdentifiers.Add(showcaseTalentIdentifier); + talentOptionIdentifiers.Add(showCaseTalentIdentifier); Point parentSize = talentBackground.RectTransform.NonScaledSize; - GUIFrame showCaseFrame = new GUIFrame(new RectTransform(new Point((int)(parentSize.X / 3f * (talentOptionIdentifiers.Count - 1)), parentSize.Y)), style: "GUITooltip") + GUIFrame showCaseFrame = new GUIFrame(new RectTransform(new Point((int)(parentSize.X / 3f * (talents.Count - 1)), parentSize.Y)), style: "GUITooltip") { - UserData = showcaseTalentIdentifier, + UserData = showCaseTalentIdentifier, IgnoreLayoutGroups = true, Visible = false }; GUILayoutGroup showcaseCenterGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.7f), showCaseFrame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterLeft); - showcaseLayout = new GUILayoutGroup(new RectTransform(Vector2.One, showcaseCenterGroup.RectTransform), isHorizontal: true) { Stretch = true }; + GUILayoutGroup showcaseLayout = new GUILayoutGroup(new RectTransform(Vector2.One, showcaseCenterGroup.RectTransform), isHorizontal: true) { Stretch = true }; + showCaseTalentParents.Add(showcaseLayout, talents); showCaseTalentFrames.Add(showCaseFrame); } @@ -376,16 +394,16 @@ namespace Barotrauma { if (!TalentPrefab.TalentPrefabs.TryGet(talentId, out TalentPrefab? talent)) { continue; } - bool isShowCaseTalent = hasShowcase && talentId == showcaseTalentIdentifier; - GUIComponent talentParent; + bool isShowCaseTalent = talentOption.ShowCaseTalents.ContainsKey(talentId); + GUIComponent talentParent = talentOptionLayoutGroup; - if (hasShowcase && talentId != showcaseTalentIdentifier) + foreach (var (key, value) in showCaseTalentParents) { - talentParent = showcaseLayout; - } - else - { - talentParent = talentOptionLayoutGroup; + if (value.Contains(talentId)) + { + talentParent = key; + break; + } } GUIFrame talentFrame = new GUIFrame(new RectTransform(Vector2.One, talentParent.RectTransform), style: null) @@ -396,7 +414,7 @@ namespace Barotrauma GUIFrame croppedTalentFrame = new GUIFrame(new RectTransform(Vector2.One, talentFrame.RectTransform, anchor: Anchor.Center, scaleBasis: ScaleBasis.BothHeight), style: null); GUIButton talentButton = new GUIButton(new RectTransform(Vector2.One, croppedTalentFrame.RectTransform, anchor: Anchor.Center), style: null) { - ToolTip = RichString.Rich($"‖color:{Color.White.ToStringHex()}‖{talent.DisplayName}‖color:end‖" + "\n\n" + talent.Description), + ToolTip = RichString.Rich($"‖color:{Color.White.ToStringHex()}‖{talent.DisplayName}‖color:end‖" + "\n\n" + ToolBox.ExtendColorToPercentageSigns(talent.Description.Value)), UserData = talent.Identifier, PressedColor = pressedColor, Enabled = info.Character != null, @@ -420,20 +438,18 @@ namespace Barotrauma return true; } - Character? controlledCharacter = info.Character; - if (controlledCharacter is null) { return false; } + if (character is null) { return false; } if (talentOption.MaxChosenTalents is 1) { // deselect other buttons in tier by removing their selected talents from pool - foreach (GUIButton guiButton in talentOptionLayoutGroup.GetAllChildren()) + foreach (Identifier identifier in selectedTalents) { - if (guiButton.UserData is Identifier otherTalentIdentifier && guiButton != button) + if (character.HasTalent(identifier) || identifier == talentId) { continue; } + + if (talentOptionIdentifiers.Contains(identifier)) { - if (!controlledCharacter.HasTalent(otherTalentIdentifier)) - { - selectedTalents.Remove(otherTalentIdentifier); - } + selectedTalents.Remove(identifier); } } } @@ -451,7 +467,7 @@ namespace Barotrauma selectedTalents.Remove(talentIdentifier); } } - else if (!controlledCharacter.HasTalent(talentIdentifier)) + else if (!character.HasTalent(talentIdentifier)) { selectedTalents.Remove(talentIdentifier); } @@ -487,9 +503,31 @@ namespace Barotrauma } iconImage.Enabled = talentButton.Enabled; - if (isShowCaseTalent) { continue; } + if (isShowCaseTalent) + { + showCaseTalentButtonsToAdd.Add(talentId, iconImage); + continue; + } - talentButtons.Add(new TalentButton(talentButton, iconImage, talent)); + buttonsToAdd.Add(new TalentButton(iconImage, talent)); + } + + foreach (TalentButton button in buttonsToAdd) + { + talentButtons.Add(button); + } + + foreach (var (key, value) in showCaseTalentButtonsToAdd) + { + HashSet buttons = new(); + foreach (Identifier identifier in talentOption.ShowCaseTalents[key]) + { + if (talentButtons.FirstOrNull(talentButton => talentButton.Identifier == identifier) is not { } button) { continue; } + + buttons.Add(button); + } + + talentShowCaseButtons.Add(new TalentShowCaseButton(buttons.ToImmutableHashSet(), value)); } talentCornerIcons.Add(new TalentCornerIcon(subTree.Identifier, index, cornerIcon, talentBackground, talentBackgroundHighlight)); @@ -534,6 +572,8 @@ namespace Barotrauma private bool ResetTalentSelection(GUIButton guiButton, object userData) { + if (characterInfo is null) { return false; } + selectedTalents = characterInfo.GetUnlockedTalentsInTree().ToHashSet(); UpdateTalentInfo(); return true; } @@ -554,16 +594,15 @@ namespace Barotrauma private bool ApplyTalentSelection(GUIButton guiButton, object userData) { - Character controlledCharacter = Character.Controlled; - if (controlledCharacter is null) { return false; } + if (character is null) { return false; } - ApplyTalents(controlledCharacter); + ApplyTalents(character); return true; } public void UpdateTalentInfo() { - if (!(Character.Controlled is { Info: var info } character)) { return; } + if (character is null || characterInfo is null) { return; } bool unlockedAllTalents = character.HasUnlockedAllTalents(); @@ -576,15 +615,15 @@ namespace Barotrauma } else { - experienceText.Text = $"{info.ExperiencePoints - info.GetExperienceRequiredForCurrentLevel()} / {info.GetExperienceRequiredToLevelUp() - info.GetExperienceRequiredForCurrentLevel()}"; - experienceBar.BarSize = info.GetProgressTowardsNextLevel(); + experienceText.Text = $"{characterInfo.ExperiencePoints - characterInfo.GetExperienceRequiredForCurrentLevel()} / {characterInfo.GetExperienceRequiredToLevelUp() - characterInfo.GetExperienceRequiredForCurrentLevel()}"; + experienceBar.BarSize = characterInfo.GetProgressTowardsNextLevel(); } selectedTalents = CheckTalentSelection(character, selectedTalents).ToHashSet(); - string pointsLeft = info.GetAvailableTalentPoints().ToString(); + string pointsLeft = characterInfo.GetAvailableTalentPoints().ToString(); - int talentCount = selectedTalents.Count - info.GetUnlockedTalentsInTree().Count(); + int talentCount = selectedTalents.Count - characterInfo.GetUnlockedTalentsInTree().Count(); if (unlockedAllTalents) { @@ -603,7 +642,7 @@ namespace Barotrauma foreach (TalentCornerIcon cornerIcon in talentCornerIcons) { - TalentTreeStageState state = GetTalentOptionStageState(character, cornerIcon.TalentTree, cornerIcon.Index, selectedTalents); + TalentStages state = GetTalentOptionStageState(character, cornerIcon.TalentTree, cornerIcon.Index, selectedTalents); TalentTreeStyle style = talentStageStyles[state]; GUIComponentStyle newStyle = style.ComponentStyle; cornerIcon.IconComponent.ApplyStyle(newStyle); @@ -614,60 +653,94 @@ namespace Barotrauma foreach (TalentButton talentButton in talentButtons) { - Identifier talentIdentifier = talentButton.Identifier; - bool unselectable = !IsViableTalentForCharacter(character, talentIdentifier, selectedTalents) || character.HasTalent(talentIdentifier); - Color newTalentColor = unselectable ? unselectableColor : unselectedColor; - Color hoverColor = Color.White; - bool selected = false; + TalentStages stage = GetTalentState(character, talentButton.Identifier, selectedTalents); + ApplyTalentIconColor(stage, talentButton.IconComponent, talentButton.Prefab.ColorOverride); + } - if (character.HasTalent(talentIdentifier)) - { - selected = true; - newTalentColor = GUIStyle.Green; - } - else if (selectedTalents.Contains(talentIdentifier)) - { - selected = true; - newTalentColor = GUIStyle.Orange; - hoverColor = Color.Lerp(GUIStyle.Orange, Color.White, 0.7f); - } - - bool shouldOverride = !unselectable || selected; - - if (shouldOverride && talentButton.Prefab.ColorOverride.TryUnwrap(out Color overrideColor)) - { - newTalentColor = overrideColor; - } - - talentButton.IconComponent.Color = newTalentColor; - talentButton.IconComponent.HoverColor = hoverColor; + foreach (TalentShowCaseButton showCaseTalentButton in talentShowCaseButtons) + { + TalentStages collectiveTalentStage = GetCollectiveTalentState(character, showCaseTalentButton.Buttons, selectedTalents); + ApplyTalentIconColor(collectiveTalentStage, showCaseTalentButton.IconComponent, Option.None()); } if (skillListBox is null) { return; } - TabMenu.CreateSkillList(character, info, skillListBox); - } + TabMenu.CreateSkillList(character, characterInfo, skillListBox); - public void AddToGUIUpdateList() - { - bool mouseInteracted = PlayerInput.PrimaryMouseButtonClicked() || PlayerInput.SecondaryMouseButtonClicked() || PlayerInput.ScrollWheelSpeed != 0; - bool keyboardInteracted = PlayerInput.KeyHit(Keys.Escape) || GameSettings.CurrentConfig.KeyMap.Bindings[InputType.InfoTab].IsHit(); - - foreach (GUIComponent component in showCaseTalentFrames) + static TalentStages GetTalentState(Character character, Identifier talentIdentifier, IReadOnlyCollection selectedTalents) { - component.AddToGUIUpdateList(order: 1); - if (!component.Visible) { continue; } - - if (keyboardInteracted || (mouseInteracted && !component.Rect.Contains(PlayerInput.MousePosition))) + bool unselectable = !IsViableTalentForCharacter(character, talentIdentifier, selectedTalents) || character.HasTalent(talentIdentifier); + TalentStages stage = unselectable ? Locked : Available; + if (unselectable) { - component.Visible = false; + stage = Locked; } + + if (character.HasTalent(talentIdentifier)) + { + stage = Unlocked; + } + else if (selectedTalents.Contains(talentIdentifier)) + { + stage = Highlighted; + } + + return stage; + } + + static void ApplyTalentIconColor(TalentStages stage, GUIComponent component, Option colorOverride) + { + Color color = stage switch + { + Invalid => unselectableColor, + Locked => unselectableColor, + Unlocked => GetColorOrOverride(GUIStyle.Green, colorOverride), + Highlighted => GetColorOrOverride(GUIStyle.Orange, colorOverride), + Available => GetColorOrOverride(unselectedColor, colorOverride), + _ => throw new ArgumentOutOfRangeException(nameof(stage), stage, null) + }; + + component.Color = color; + component.HoverColor = Color.Lerp(color, Color.White, 0.7f); + + static Color GetColorOrOverride(Color color, Option colorOverride) => colorOverride.TryUnwrap(out Color overrideColor) ? overrideColor : color; + } + + // this could also be reused for setting colors for talentCornerIcons but that's for another time + static TalentStages GetCollectiveTalentState(Character character, IReadOnlyCollection buttons, IReadOnlyCollection selectedTalents) + { + HashSet talentStages = new HashSet(); + foreach (TalentButton button in buttons) + { + talentStages.Add(GetTalentState(character, button.Identifier, selectedTalents)); + } + + TalentStages collectiveStage = talentStages.Any(static stage => stage is Locked) + ? Locked + : Available; + + foreach (TalentStages stage in talentStages) + { + if (stage is Highlighted) + { + collectiveStage = Highlighted; + break; + } + + if (stage is Unlocked) + { + collectiveStage = Unlocked; + break; + } + } + + return collectiveStage; } } public void Update() { - if (Character.Controlled?.Info is not { } characterInfo || talentResetButton is null || talentApplyButton is null) { return; } + if (characterInfo is null || talentResetButton is null || talentApplyButton is null) { return; } int talentCount = selectedTalents.Count - characterInfo.GetUnlockedTalentsInTree().Count(); talentResetButton.Enabled = talentApplyButton.Enabled = talentCount > 0; @@ -675,6 +748,58 @@ namespace Barotrauma { talentApplyButton.Flash(GUIStyle.Orange); } + + while (showCaseClosureQueue.TryDequeue(out Identifier identifier)) + { + foreach (GUIComponent component in showCaseTalentFrames) + { + if (component.UserData is Identifier showcaseIdentifier && showcaseIdentifier == identifier) + { + component.Visible = false; + } + } + } + + bool mouseInteracted = PlayerInput.PrimaryMouseButtonClicked() || PlayerInput.SecondaryMouseButtonClicked() || PlayerInput.ScrollWheelSpeed != 0; + bool keyboardInteracted = PlayerInput.KeyHit(Keys.Escape) || GameSettings.CurrentConfig.KeyMap.Bindings[InputType.InfoTab].IsHit(); + + foreach (GUIComponent component in showCaseTalentFrames) + { + if (component.UserData is not Identifier identifier) { continue; } + + component.AddToGUIUpdateList(order: 1); + if (!component.Visible) { continue; } + + if (keyboardInteracted || (mouseInteracted && !component.Rect.Contains(PlayerInput.MousePosition))) + { + showCaseClosureQueue.Enqueue(identifier); + } + } + } + + private static bool IsOwnCharacter(CharacterInfo? info) + { + if (info is null) { return false; } + + CharacterInfo? ownCharacterInfo = Character.Controlled?.Info ?? GameMain.Client?.CharacterInfo; + if (ownCharacterInfo is null) { return false; } + + return info == ownCharacterInfo; + } + + public static bool CanManageTalents(CharacterInfo targetInfo) + { + // in singleplayer we can do whatever we want + if (GameMain.IsSingleplayer) { return true; } + + // always allow managing talents for own character + if (IsOwnCharacter(targetInfo)) { return true; } + + // don't allow controlling non-bot characters + if (targetInfo.Character is not { IsBot: true }) { return false; } + + // lastly check if we have the permission to do this + return GameMain.Client is { } client && client.HasPermission(ClientPermissions.ManageBotTalents); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index b58bcc3fc..445a77787 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -278,10 +278,13 @@ namespace Barotrauma * | upgrades | maintenance | <- 1/3rd empty space | * |---------------------------------------------------------------------------------------------------| */ - GUILayoutGroup leftLayout = new GUILayoutGroup(rectT(0.5f, 1, topHeaderLayout)) { RelativeSpacing = 0.05f }; + GUILayoutGroup leftLayout = new GUILayoutGroup(rectT(0.4f, 1, topHeaderLayout)) { RelativeSpacing = 0.05f }; GUILayoutGroup locationLayout = new GUILayoutGroup(rectT(1, 0.5f, leftLayout), isHorizontal: true); GUIImage submarineIcon = new GUIImage(rectT(new Point(locationLayout.Rect.Height, locationLayout.Rect.Height), locationLayout), style: "SubmarineIcon", scaleToFit: true); - new GUITextBlock(rectT(1.0f - submarineIcon.RectTransform.RelativeSize.X, 1, locationLayout), TextManager.Get("UpgradeUI.Title"), font: GUIStyle.LargeFont); + var header = new GUITextBlock(rectT(1.0f - submarineIcon.RectTransform.RelativeSize.X, 1, locationLayout), TextManager.Get("UpgradeUI.Title"), font: GUIStyle.LargeFont); + header.RectTransform.MaxSize = new Point((int)(header.TextSize.X + header.Padding.X + header.Padding.Z), int.MaxValue); + new GUITextBlock(rectT(1.0f, 1, locationLayout), TextManager.Get("UpgradeUI.AllSubmarinesInfo"), font: GUIStyle.SmallFont, wrap: true); + categoryButtonLayout = new GUILayoutGroup(rectT(0.4f, 0.3f, leftLayout), isHorizontal: true) { Stretch = true }; GUIButton upgradeButton = new GUIButton(rectT(1, 1f, categoryButtonLayout), TextManager.Get("UICategory.Upgrades"), style: "GUITabButton") { UserData = UpgradeTab.Upgrade, Selected = selectedUpgradeTab == UpgradeTab.Upgrade }; GUIButton repairButton = new GUIButton(rectT(1, 1f, categoryButtonLayout), TextManager.Get("UICategory.Maintenance"), style: "GUITabButton") { UserData = UpgradeTab.Repairs, Selected = selectedUpgradeTab == UpgradeTab.Repairs }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 5965d350e..21dc20279 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -4,6 +4,7 @@ using Barotrauma.Networking; using Barotrauma.Particles; using Barotrauma.Steam; using Barotrauma.Transition; +using Barotrauma.Tutorials; using FarseerPhysics; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; @@ -774,9 +775,9 @@ namespace Barotrauma { GUIMessageBox.MessageBoxes.Remove(GUIMessageBox.VisibleBox); } - else if (GameSession?.GameMode is TutorialMode tutorialMode && tutorialMode.Tutorial.ContentRunning) + else if (ObjectiveManager.ContentRunning) { - tutorialMode.Tutorial.CloseActiveContentGUI(); + ObjectiveManager.CloseActiveContentGUI(); } else if (GameSession.IsTabMenuOpen) { @@ -828,7 +829,7 @@ namespace Barotrauma Paused = (DebugConsole.IsOpen || DebugConsole.Paused || GUI.PauseMenuOpen || GUI.SettingsMenuOpen || - (GameSession?.GameMode is TutorialMode tutoMode && tutoMode.Tutorial.ContentRunning)) && + (GameSession?.GameMode is TutorialMode && ObjectiveManager.ContentRunning)) && (NetworkMember == null || !NetworkMember.GameStarted); if (GameSession?.GameMode != null && GameSession.GameMode.Paused) { @@ -862,8 +863,9 @@ namespace Barotrauma { Screen.Selected.Update(Timing.Step); } - else if (GameSession?.GameMode is TutorialMode tutorialMode && tutorialMode.Tutorial.ContentRunning) + else if (ObjectiveManager.ContentRunning && GameSession?.GameMode is TutorialMode tutorialMode) { + ObjectiveManager.VideoPlayer.Update(); tutorialMode.Update((float)Timing.Step); } else diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index 3311d1650..856417e04 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -4,6 +4,7 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -203,6 +204,10 @@ namespace Barotrauma } break; } + if (Level.IsLoadedOutpost && !ObjectiveManager.AllActiveObjectivesCompleted()) + { + endRoundButton.Visible = false; + } if (ReadyCheckButton != null) { ReadyCheckButton.Visible = endRoundButton.Visible; } @@ -266,7 +271,7 @@ namespace Barotrauma Rand.ThreadId = Thread.CurrentThread.ManagedThreadId; try { - GameMain.GameSession.StartRound(newLevel, mirrorLevel: mirror); + GameMain.GameSession.StartRound(newLevel, mirrorLevel: mirror, startOutpost: GetPredefinedStartOutpost()); } catch (Exception e) { @@ -283,6 +288,18 @@ namespace Barotrauma return loadTask; } + protected SubmarineInfo GetPredefinedStartOutpost() + { + if (Map?.CurrentLocation?.Type?.GetForcedOutpostGenerationParams() is OutpostGenerationParams parameters && !parameters.OutpostFilePath.IsNullOrEmpty()) + { + return new SubmarineInfo(parameters.OutpostFilePath.Value) + { + OutpostGenerationParams = parameters + }; + } + return null; + } + partial void NPCInteractProjSpecific(Character npc, Character interactor) { if (npc == null || interactor == null) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index 965b3a274..74b5d0b9d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -265,8 +265,8 @@ namespace Barotrauma private IEnumerable DoLoadInitialLevel(LevelData level, bool mirror) { - GameMain.GameSession.StartRound(level, - mirrorLevel: mirror); + + GameMain.GameSession.StartRound(level, mirrorLevel: mirror, startOutpost: GetPredefinedStartOutpost()); GameMain.GameScreen.Select(); CoroutineManager.StartCoroutine(DoInitialCameraTransition(), "SinglePlayerCampaign.DoInitialCameraTransition"); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs index a9c7f14e8..d6c8f225d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs @@ -1,7 +1,5 @@ using Barotrauma.Extensions; -using Barotrauma.IO; using Barotrauma.Items.Components; -using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; @@ -25,107 +23,9 @@ namespace Barotrauma.Tutorials #region Tutorial variables public readonly Identifier Identifier; - public LocalizedString DisplayName { get; } - - public bool ContentRunning { get; private set; } - - private GUIComponent infoBox; - private Action infoBoxClosedCallback; - - private VideoPlayer videoPlayer; - private Point screenResolution; - private WindowMode windowMode; - private float prevUIScale; - - private GUILayoutGroup objectiveGroup; - private readonly LocalizedString objectiveTextTranslated; - - private readonly List ActiveObjectives = new List(); - private const float ObjectiveComponentAnimationTime = 1.5f; - private Segment ActiveContentSegment { get; set; } - - public class Segment - { - public readonly record struct Text( - Identifier Tag, - int Width = DefaultWidth, - int Height = DefaultHeight, - Anchor Anchor = Anchor.Center); - - public readonly record struct Video( - string FullPath, - Identifier TextTag, - int Width = DefaultWidth, - int Height = DefaultHeight) - { - public string FileName => Path.GetFileName(FullPath.CleanUpPath()); - public string ContentPath => Path.GetDirectoryName(FullPath.CleanUpPath()); - } - - private const int DefaultWidth = 450; - private const int DefaultHeight = 80; - - public GUIImage ObjectiveStateIndicator; - public GUIButton ObjectiveButton; - public GUITextBlock LinkedTextBlock; - public LocalizedString ObjectiveText; - - public readonly Identifier Id; - public readonly Text TextContent; - public readonly Video VideoContent; - public readonly AutoPlayVideo AutoPlayVideo; - - public Action OnClickObjective; - - public TutorialSegmentType SegmentType { get; private set; } - - public static Segment CreateInfoBoxSegment(Identifier id, Identifier objectiveTextTag, AutoPlayVideo autoPlayVideo, Text textContent = default, Video videoContent = default) - { - return new Segment(id, objectiveTextTag, autoPlayVideo, textContent, videoContent); - } - - public static Segment CreateMessageBoxSegment(Identifier id, Identifier objectiveTextTag, Action onClickObjective) - { - return new Segment(id, objectiveTextTag, onClickObjective); - } - - public static Segment CreateObjectiveSegment(Identifier id, Identifier objectiveTextTag) - { - return new Segment(id, objectiveTextTag); - } - - private Segment(Identifier id, Identifier objectiveTextTag, AutoPlayVideo autoPlayVideo, Text textContent = default, Video videoContent = default) - { - Id = id; - ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag)); - AutoPlayVideo = autoPlayVideo; - TextContent = textContent; - VideoContent = videoContent; - SegmentType = TutorialSegmentType.InfoBox; - } - - private Segment(Identifier id, Identifier objectiveTextTag, Action onClickObjective) - { - Id = id; - ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag)); - OnClickObjective = onClickObjective; - SegmentType = TutorialSegmentType.MessageBox; - } - - private Segment(Identifier id, Identifier objectiveTextTag) - { - Id = id; - ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag)); - SegmentType = TutorialSegmentType.Objective; - } - - public void ConnectMessageBox(Segment messageBoxSegment) - { - SegmentType = TutorialSegmentType.MessageBox; - OnClickObjective = messageBoxSegment.OnClickObjective; - } - } + public LocalizedString Description { get; } + private bool completed; public bool Completed @@ -163,6 +63,8 @@ namespace Barotrauma.Tutorials public readonly List<(Entity entity, Identifier iconStyle)> Icons = new List<(Entity entity, Identifier iconStyle)>(); + public bool Paused { get; private set; } + #endregion #region Tutorial Controls @@ -171,8 +73,7 @@ namespace Barotrauma.Tutorials { Identifier = $"tutorial.{prefab.Identifier}".ToIdentifier(); DisplayName = TextManager.Get(Identifier); - objectiveTextTranslated = TextManager.Get("Tutorial.Objective"); - + Description = TextManager.Get($"tutorial.{prefab.Identifier}.description"); TutorialPrefab = prefab; eventPrefab = EventSet.GetEventPrefab(prefab.EventIdentifier); } @@ -260,35 +161,26 @@ namespace Barotrauma.Tutorials tutorialCoroutine = CoroutineManager.StartCoroutine(UpdateState()); - Initialize(); + GameMain.GameSession.CrewManager.AllowCharacterSwitch = TutorialPrefab.AllowCharacterSwitch; + GameMain.GameSession.CrewManager.AutoHideCrewList(); + + if (Character.Controlled?.Inventory is CharacterInventory inventory) + { + foreach (Item item in inventory.AllItemsMod) + { + if (item.HasTag(TutorialPrefab.StartingItemTags)) { continue; } + item.Unequip(Character.Controlled); + Character.Controlled.Inventory.RemoveItem(item); + } + } yield return CoroutineStatus.Success; } - private void Initialize() - { - GameMain.GameSession.CrewManager.AllowCharacterSwitch = TutorialPrefab.AllowCharacterSwitch; - GameMain.GameSession.CrewManager.AutoHideCrewList(); - - if (Character.Controlled is Character character) - { - foreach (Item item in character.Inventory.AllItemsMod) - { - if (item.HasTag(TutorialPrefab.StartingItemTags)) { continue; } - item.Unequip(character); - character.Inventory.RemoveItem(item); - } - } - } - public void Start() { - videoPlayer = new VideoPlayer(); GameMain.Instance.ShowLoading(Loading()); - ActiveObjectives.Clear(); - ActiveContentSegment = null; - - CreateObjectiveFrame(); + ObjectiveManager.ResetObjectives(); // Setup doors: Clear all requirements, unless the door is setup as locked. foreach (var item in Item.ItemList) @@ -304,24 +196,8 @@ namespace Barotrauma.Tutorials } } - public void AddToGUIUpdateList() - { - if (GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y || prevUIScale != GUI.Scale || GameSettings.CurrentConfig.Graphics.DisplayMode != windowMode) - { - CreateObjectiveFrame(); - } - if (ActiveObjectives.Count > 0) - { - objectiveGroup?.AddToGUIUpdateList(order: -1); - } - infoBox?.AddToGUIUpdateList(order: 100); - videoPlayer?.AddToGUIUpdateList(order: 100); - } - public void Update() { - videoPlayer?.Update(); - if (character != null) { if (character.Oxygen < 1) @@ -342,8 +218,7 @@ namespace Barotrauma.Tutorials { GUI.PreventPauseMenuToggle = false; } - ContentRunning = false; - infoBox = null; + ObjectiveManager.ClearContent(); } else { @@ -374,18 +249,6 @@ namespace Barotrauma.Tutorials yield return CoroutineStatus.Success; } - public void CloseActiveContentGUI() - { - if (videoPlayer.IsPlaying) - { - videoPlayer.Stop(); - } - else if (infoBox != null) - { - CloseInfoFrame(); - } - } - public IEnumerable UpdateState() { while (GameMain.Instance.LoadingScreenOpen || Level.Loaded == null || Level.Loaded.Generating) @@ -432,13 +295,56 @@ namespace Barotrauma.Tutorials yield return new WaitForSeconds(WaitBeforeFade); + Action onEnd = () => GameMain.MainMenuScreen.ReturnToMainMenu(null, null); + + TutorialPrefab nextTutorialPrefab = null; + bool displayEndMessage = + TutorialPrefab.EndMessage.EndType == TutorialPrefab.EndType.Restart || + (TutorialPrefab.EndMessage.EndType == TutorialPrefab.EndType.Continue && TutorialPrefab.Prefabs.TryGet(TutorialPrefab.EndMessage.NextTutorialIdentifier, out nextTutorialPrefab)); + + if (displayEndMessage) + { + Paused = true; + var endingMessageBox = new GUIMessageBox( + headerText: "", + text: TextManager.Get($"{Identifier}.completed"), + buttons: new LocalizedString[] + { + TextManager.Get(nextTutorialPrefab is null ? "restart" : "campaigncontinue"), + TextManager.Get("pausemenuquit") + }); + + endingMessageBox.Buttons[0].OnClicked += (_, _) => + { + if (nextTutorialPrefab is null) + { + onEnd = () => Restart(null, null); + } + else + { + onEnd = () => + { + GameMain.MainMenuScreen.ReturnToMainMenu(null, null); + new Tutorial(nextTutorialPrefab).Start(); + }; + } + return true; + }; + endingMessageBox.Buttons[0].OnClicked += endingMessageBox.Close; + endingMessageBox.Buttons[0].OnClicked += (_, _) => Paused = false; + endingMessageBox.Buttons[1].OnClicked += endingMessageBox.Close; + endingMessageBox.Buttons[1].OnClicked += (_, _) => Paused = false; + } + + while (Paused) { yield return CoroutineStatus.Running; } + var endCinematic = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, null, Alignment.Center, panDuration: FadeOutTime); Completed = true; while (endCinematic.Running) { yield return CoroutineStatus.Running; } Stop(); - GameMain.MainMenuScreen.ReturnToMainMenu(null, null); + onEnd(); } } @@ -450,379 +356,15 @@ namespace Barotrauma.Tutorials return true; } - public void TriggerTutorialSegment(Segment segment, bool connectObjective = false) - { - if (segment.SegmentType != TutorialSegmentType.InfoBox) - { - ActiveObjectives.Add(segment); - AddToObjectiveList(segment, connectObjective); - return; - } - - Inventory.DraggingItems.Clear(); - ContentRunning = true; - ActiveContentSegment = segment; - - var title = TextManager.Get(segment.Id); - LocalizedString tutorialText = TextManager.GetFormatted(segment.TextContent.Tag); - tutorialText = TextManager.ParseInputTypes(tutorialText); - - switch (segment.AutoPlayVideo) - { - case AutoPlayVideo.Yes: - infoBox = CreateInfoFrame( - title, - tutorialText, - segment.TextContent.Width, - segment.TextContent.Height, - segment.TextContent.Anchor, - hasButton: true, - onInfoBoxClosed: LoadActiveContentVideo); - break; - case AutoPlayVideo.No: - infoBox = CreateInfoFrame( - title, - tutorialText, - segment.TextContent.Width, - segment.TextContent.Height, - segment.TextContent.Anchor, - hasButton: true, - onInfoBoxClosed: StopCurrentContentSegment, - onVideoButtonClicked: LoadActiveContentVideo); - break; - } - } - - public void CompleteTutorialSegment(Identifier segmentId) - { - if (GetActiveObjective(segmentId) is not Segment segment) - { - DebugConsole.AddWarning($"Warning: tried to complete the tutorial segment \"{segmentId}\" in tutorial \"{Identifier}\" but it isn't active!"); - return; - } - if (GUIStyle.GetComponentStyle("ObjectiveIndicatorCompleted") is GUIComponentStyle style) - { - //return if already completed - if (segment.ObjectiveStateIndicator.Style == style) { return; } - segment.ObjectiveStateIndicator.ApplyStyle(style); - } - segment.ObjectiveStateIndicator.Parent.Flash(color: GUIStyle.Green, flashDuration: 0.35f, useRectangleFlash: true); - segment.ObjectiveButton.OnClicked = null; - segment.ObjectiveButton.CanBeFocused = false; - GameAnalyticsManager.AddDesignEvent($"Tutorial:{Identifier}:{segmentId}:Completed"); - } - - public void RemoveTutorialSegment(Identifier segmentId) - { - if (GetActiveObjective(segmentId) is not Segment segment) - { - DebugConsole.AddWarning($"Warning: tried to remove the tutorial segment \"{segmentId}\" in tutorial \"{Identifier}\" but it isn't active!"); - return; - } - segment.ObjectiveStateIndicator.FadeOut(ObjectiveComponentAnimationTime, false); - segment.LinkedTextBlock.FadeOut(ObjectiveComponentAnimationTime, false); - var parent = segment.LinkedTextBlock.Parent; - parent.FadeOut(ObjectiveComponentAnimationTime, true, onRemove: () => - { - ActiveObjectives.Remove(segment); - objectiveGroup?.Recalculate(); - }); - parent.RectTransform.MoveOverTime(GetObjectiveHiddenPosition(parent.RectTransform), ObjectiveComponentAnimationTime); - segment.ObjectiveButton.OnClicked = null; - segment.ObjectiveButton.CanBeFocused = false; - } - - private Segment GetActiveObjective(Identifier id) => ActiveObjectives.FirstOrDefault(s => s.Id == id); - public void Stop() { if (tutorialCoroutine != null) { CoroutineManager.StopCoroutines(tutorialCoroutine); } - ContentRunning = false; - infoBox = null; - videoPlayer?.Remove(); + ObjectiveManager.ResetUI(); } #endregion - - #region Objectives - - /// - /// Create the objective list that holds the objectives (called on start and on resolution change) - /// - private void CreateObjectiveFrame() - { - var objectiveListFrame = new GUIFrame(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.TutorialObjectiveListArea, GUI.Canvas), style: null); - objectiveGroup = new GUILayoutGroup(new RectTransform(Vector2.One, objectiveListFrame.RectTransform)) - { - AbsoluteSpacing = (int)GUIStyle.Font.LineHeight - }; - for (int i = 0; i < ActiveObjectives.Count; i++) - { - AddToObjectiveList(ActiveObjectives[i]); - } - screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); - windowMode = GameSettings.CurrentConfig.Graphics.DisplayMode; - prevUIScale = GUI.Scale; - } - - /// - /// Stops content running and adds the active segment to the objective list - /// - private void StopCurrentContentSegment() - { - if (!ActiveContentSegment.ObjectiveText.IsNullOrEmpty()) - { - ActiveObjectives.Add(ActiveContentSegment); - AddToObjectiveList(ActiveContentSegment); - } - ContentRunning = false; - ActiveContentSegment = null; - } - - /// - /// Adds the segment to the objective list - /// - private void AddToObjectiveList(Segment segment, bool connectExisting = false) - { - if (connectExisting) - { - if (ActiveObjectives.Find(o => o.Id == segment.Id) is { } existingSegment) - { - existingSegment.ConnectMessageBox(segment); - SetButtonBehavior(existingSegment); - } - return; - } - - var frameRt = new RectTransform(new Vector2(1.0f, 0.1f), objectiveGroup.RectTransform) - { - AbsoluteOffset = GetObjectiveHiddenPosition(), - MinSize = new Point(0, objectiveGroup.AbsoluteSpacing) - }; - var frame = new GUIFrame(frameRt, style: null) - { - CanBeFocused = true - }; - objectiveGroup.Recalculate(); - - segment.LinkedTextBlock = new GUITextBlock( - new RectTransform(new Point(frameRt.Rect.Width - objectiveGroup.AbsoluteSpacing, 0), frame.RectTransform, anchor: Anchor.TopRight), - TextManager.ParseInputTypes(segment.ObjectiveText), - wrap: true); - - var size = new Point(segment.LinkedTextBlock.Rect.Width, segment.LinkedTextBlock.Rect.Height); - segment.LinkedTextBlock.RectTransform.NonScaledSize = size; - segment.LinkedTextBlock.RectTransform.MinSize = size; - segment.LinkedTextBlock.RectTransform.MaxSize = size; - segment.LinkedTextBlock.RectTransform.IsFixedSize = true; - frame.RectTransform.Resize(new Point(frame.Rect.Width, segment.LinkedTextBlock.RectTransform.Rect.Height), resizeChildren: false); - frame.RectTransform.IsFixedSize = true; - - var indicatorRt = new RectTransform(new Point(objectiveGroup.AbsoluteSpacing), frame.RectTransform, isFixedSize: true); - segment.ObjectiveStateIndicator = new GUIImage(indicatorRt, "ObjectiveIndicatorIncomplete"); - - SetTransparent(segment.LinkedTextBlock); - - segment.ObjectiveButton = new GUIButton(new RectTransform(Vector2.One, segment.LinkedTextBlock.RectTransform, Anchor.TopLeft, Pivot.TopLeft), style: null) - { - ToolTip = objectiveTextTranslated - }; - SetButtonBehavior(segment); - SetTransparent(segment.ObjectiveButton); - - frameRt.MoveOverTime(new Point(0, frameRt.AbsoluteOffset.Y), ObjectiveComponentAnimationTime, onDoneMoving: () => objectiveGroup?.Recalculate()); - - static void SetTransparent(GUIComponent component) => component.Color = component.HoverColor = component.PressedColor = component.SelectedColor = Color.Transparent; - - void SetButtonBehavior(Segment segment) - { - segment.ObjectiveButton.CanBeFocused = segment.SegmentType != TutorialSegmentType.Objective; - segment.ObjectiveButton.OnClicked = (GUIButton btn, object userdata) => - { - if (segment.SegmentType == TutorialSegmentType.InfoBox) - { - if (segment.AutoPlayVideo == AutoPlayVideo.Yes) - { - ReplaySegmentVideo(segment); - } - else - { - ShowSegmentText(segment); - } - } - else if (segment.SegmentType == TutorialSegmentType.MessageBox) - { - segment.OnClickObjective?.Invoke(); - } - return true; - }; - } - } - - private void ReplaySegmentVideo(Segment segment) - { - if (ContentRunning) { return; } - Inventory.DraggingItems.Clear(); - ContentRunning = true; - LoadVideo(segment); - } - - private void ShowSegmentText(Segment segment) - { - if (ContentRunning) { return; } - Inventory.DraggingItems.Clear(); - ContentRunning = true; - ActiveContentSegment = segment; - infoBox = CreateInfoFrame( - TextManager.Get(segment.Id), - TextManager.Get(segment.TextContent.Tag), - segment.TextContent.Width, - segment.TextContent.Height, - segment.TextContent.Anchor, - hasButton: true, - onInfoBoxClosed: () => ContentRunning = false, - onVideoButtonClicked: () => LoadVideo(segment)); - } - - private Point GetObjectiveHiddenPosition(RectTransform rt = null) - { - return new Point(GameMain.GraphicsWidth - objectiveGroup.Rect.X, rt?.AbsoluteOffset.Y ?? 0); - } - - #endregion - - #region InfoFrame - - private void CloseInfoFrame() => CloseInfoFrame(null, null); - - private bool CloseInfoFrame(GUIButton button, object userData) - { - infoBox = null; - infoBoxClosedCallback?.Invoke(); - return true; - } - - /// - // Creates and displays a tutorial info box - /// - private GUIComponent CreateInfoFrame(LocalizedString title, LocalizedString text, int width = 300, int height = 80, Anchor anchor = Anchor.TopRight, bool hasButton = false, Action onInfoBoxClosed = null, Action onVideoButtonClicked = null) - { - if (hasButton) - { - height += 60; - } - - width = (int)(width * GUI.Scale); - height = (int)(height * GUI.Scale); - - LocalizedString wrappedText = ToolBox.WrapText(text, width, GUIStyle.Font); - height += (int)GUIStyle.Font.MeasureString(wrappedText).Y; - - if (title.Length > 0) - { - height += (int)GUIStyle.Font.MeasureString(title).Y + (int)(150 * GUI.Scale); - } - - var background = new GUIFrame(new RectTransform(new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight), GUI.Canvas, Anchor.Center), style: "GUIBackgroundBlocker"); - - var infoBlock = new GUIFrame(new RectTransform(new Point(width, height), background.RectTransform, anchor)); - infoBlock.Flash(GUIStyle.Green); - - var infoContent = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), infoBlock.RectTransform, Anchor.Center)) - { - Stretch = true, - AbsoluteSpacing = 5 - }; - - if (title.Length > 0) - { - var titleBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), - title, font: GUIStyle.LargeFont, textAlignment: Alignment.Center, textColor: new Color(253, 174, 0)); - titleBlock.RectTransform.IsFixedSize = true; - } - - text = RichString.Rich(text); - GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), text, wrap: true); - - textBlock.RectTransform.IsFixedSize = true; - infoBoxClosedCallback = onInfoBoxClosed; - - if (hasButton) - { - var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), infoContent.RectTransform), isHorizontal: true) - { - RelativeSpacing = 0.1f - }; - buttonContainer.RectTransform.IsFixedSize = true; - - if (onVideoButtonClicked != null) - { - buttonContainer.Stretch = true; - var videoButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), buttonContainer.RectTransform), - TextManager.Get("Video"), style: "GUIButtonLarge") - { - OnClicked = (GUIButton button, object obj) => - { - onVideoButtonClicked(); - return true; - } - }; - } - else - { - buttonContainer.Stretch = false; - buttonContainer.ChildAnchor = Anchor.Center; - } - - var okButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), buttonContainer.RectTransform), - TextManager.Get("OK"), style: "GUIButtonLarge") - { - OnClicked = CloseInfoFrame - }; - } - - infoBlock.RectTransform.NonScaledSize = new Point(infoBlock.Rect.Width, (int)(infoContent.Children.Sum(c => c.Rect.Height + infoContent.AbsoluteSpacing) / infoContent.RectTransform.RelativeSize.Y)); - - SoundPlayer.PlayUISound(GUISoundType.UIMessage); - - return background; - } - - #endregion - - #region Video - - private void LoadVideo(Segment segment) - { - videoPlayer ??= new VideoPlayer(); - if (segment.AutoPlayVideo == AutoPlayVideo.Yes) - { - videoPlayer.LoadContent( - contentPath: segment.VideoContent.ContentPath, - videoSettings: new VideoPlayer.VideoSettings(segment.VideoContent.FileName), - textSettings: new VideoPlayer.TextSettings(segment.VideoContent.TextTag, segment.VideoContent.Width), - contentId: segment.Id, - startPlayback: true, - objective: segment.ObjectiveText, - onStop: StopCurrentContentSegment); - } - else - { - videoPlayer.LoadContent( - contentPath: segment.VideoContent.ContentPath, - videoSettings: new VideoPlayer.VideoSettings(segment.VideoContent.FileName), - textSettings: null, - contentId: segment.Id, - startPlayback: true, - objective: string.Empty); - } - } - - private void LoadActiveContentVideo() => LoadVideo(ActiveContentSegment); - - #endregion } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/TutorialMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/TutorialMode.cs index d155a1c23..3a8b4a7f5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/TutorialMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/TutorialMode.cs @@ -6,6 +6,8 @@ namespace Barotrauma { public Tutorial Tutorial; + public override bool Paused => Tutorial.Paused; + public TutorialMode(GameModePreset preset) : base(preset) { } public override void Start() @@ -19,12 +21,6 @@ namespace Barotrauma } } - public override void AddToGUIUpdateList() - { - base.AddToGUIUpdateList(); - Tutorial.AddToGUIUpdateList(); - } - public override void Update(float deltaTime) { base.Update(deltaTime); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs index dd566ee34..90226c5e0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Tutorials; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace Barotrauma @@ -128,8 +129,9 @@ namespace Barotrauma if (GUI.DisableHUD) { return; } GameMode?.AddToGUIUpdateList(); tabMenu?.AddToGUIUpdateList(); + ObjectiveManager.AddToGUIUpdateList(); - if ((!(GameMode is CampaignMode campaign) || (!campaign.ForceMapUI && !campaign.ShowCampaignUI)) && + if ((GameMode is not CampaignMode campaign || (!campaign.ForceMapUI && !campaign.ShowCampaignUI)) && !CoroutineManager.IsCoroutineRunning("LevelTransition") && !CoroutineManager.IsCoroutineRunning("SubmarineTransition")) { if (topLeftButtonGroup == null) @@ -223,6 +225,7 @@ namespace Barotrauma } HintManager.Update(); + ObjectiveManager.VideoPlayer.Update(); } public void SetRespawnInfo(bool visible, string text, Color textColor, bool buttonsVisible, bool waitForNextRoundRespawn) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs index 0dfc29ed7..fcb5438dc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs @@ -1,6 +1,7 @@ using Barotrauma.Extensions; using Barotrauma.IO; using Barotrauma.Items.Components; +using Barotrauma.Tutorials; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -209,7 +210,7 @@ namespace Barotrauma { if (item.CurrentHull == null) { continue; } if (item.GetComponent() == null) { continue; } - if (!item.HasTag("ballast")) { continue; } + if (!item.HasTag("ballast") && !item.CurrentHull.RoomName.Contains("ballast", StringComparison.OrdinalIgnoreCase)) { continue; } BallastHulls.Add(item.CurrentHull); } } @@ -383,6 +384,34 @@ namespace Barotrauma IgnoreReminder("tabmenu"); } + public static void OnObtainedItem(Character character, Item item) + { + if (!CanDisplayHints()) { return; } + if (character != Character.Controlled || item == null) { return; } + + if (DisplayHint($"onobtaineditem.{item.Prefab.Identifier}".ToIdentifier())) { return; } + foreach (Identifier tag in item.GetTags()) + { + if (DisplayHint($"onobtaineditem.{tag}".ToIdentifier())) { return; } + } + + if ((item.HasTag("geneticmaterial") && character.Inventory.FindItemByTag("geneticdevice".ToIdentifier(), recursive: true) != null) || + (item.HasTag("geneticdevice") && character.Inventory.FindItemByTag("geneticmaterial".ToIdentifier(), recursive: true) != null)) + { + if (DisplayHint($"geneticmaterial.useinstructions".ToIdentifier())) { return; } + } + } + + public static void OnStartDeconstructing(Character character, Deconstructor deconstructor) + { + if (!CanDisplayHints()) { return; } + if (character != Character.Controlled || deconstructor == null) { return; } + if (deconstructor.InputContainer.Inventory.AllItems.All(it => it.GetComponent() is not null)) + { + DisplayHint($"geneticmaterial.onrefiningorcombining".ToIdentifier()); + } + } + public static void OnStoleItem(Character character, Item item) { if (!CanDisplayHints()) { return; } @@ -507,7 +536,7 @@ namespace Barotrauma if (!CanDisplayHints()) { return; } if (character != Character.Controlled) { return; } // Could make this more generic if there will ever be any other status effect related hints - if (!(component is Repairable) || actionType != ActionType.OnFailure) { return; } + if (component is not Repairable || actionType != ActionType.OnFailure) { return; } DisplayHint("onrepairfailed".ToIdentifier()); } @@ -563,7 +592,7 @@ namespace Barotrauma foreach (var me in gap.linkedTo) { if (me == Character.Controlled.CurrentHull) { continue; } - if (!(me is Hull adjacentHull)) { continue; } + if (me is not Hull adjacentHull) { continue; } if (!IsOnFriendlySub()) { continue; } if (IsWearingDivingSuit()) { continue; } if (adjacentHull.LethalPressure > 5.0f && DisplayHint("onadjacenthull.highpressure".ToIdentifier())) { return; } @@ -720,6 +749,7 @@ namespace Barotrauma if (requireControllingCharacter && Character.Controlled == null) { return false; } var gameMode = GameMain.GameSession?.GameMode; if (!(gameMode is CampaignMode || gameMode is MissionMode)) { return false; } + if (ObjectiveManager.AnyObjectives) { return false; } if (requireGameScreen && Screen.Selected != GameMain.GameScreen) { return false; } return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs index c06efe8d5..232e84838 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs @@ -9,7 +9,7 @@ using Barotrauma.Networking; namespace Barotrauma { - internal partial class MedicalClinic + internal sealed partial class MedicalClinic { public enum RequestResult { @@ -19,63 +19,11 @@ namespace Barotrauma 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; - } - } + public readonly record struct RequestAction(Action Callback, DateTimeOffset Timeout); + public readonly record struct AfflictionRequest(RequestResult Result, ImmutableArray Afflictions); + public readonly record struct PendingRequest(RequestResult Result, NetCollection CrewMembers); + public readonly record struct CallbackOnlyRequest(RequestResult Result); + public readonly record struct HealRequest(RequestResult Result, HealRequestResult HealResult); private readonly List> afflictionRequests = new List>(); private readonly List> pendingHealRequests = new List>(); @@ -96,7 +44,7 @@ namespace Barotrauma } #endif - if (!(info is { Character: { CharacterHealth: { } health } })) + if (info is not { Character.CharacterHealth: { } health }) { onReceived.Invoke(new AfflictionRequest(RequestResult.Error, ImmutableArray.Empty)); return; @@ -123,14 +71,14 @@ namespace Barotrauma 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(afflictionRequests, now, onTimeout: static callback => { callback(new AfflictionRequest(RequestResult.Timeout, ImmutableArray.Empty)); }); + UpdateQueue(pendingHealRequests, now, onTimeout: static callback => { callback(new PendingRequest(RequestResult.Timeout, NetCollection.Empty)); }); + UpdateQueue(healAllRequests, now, onTimeout: static 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)); } + static void CallbackOnlyTimeout(Action callback) { callback(new CallbackOnlyRequest(RequestResult.Timeout)); } } public bool IsAfflictionPending(NetCrewMember character, NetAffliction affliction) @@ -148,9 +96,9 @@ namespace Barotrauma private static bool TryDequeue(List> requestQueue, out Action result) { RequestAction? first = requestQueue.FirstOrNull(); - if (!(first is { } action)) + if (first is not { } action) { - result = _ => { }; + result = static _ => { }; return false; } @@ -191,11 +139,25 @@ namespace Barotrauma private static int GetPing() { - if (GameMain.IsSingleplayer || !(GameMain.Client?.Name is { } ownName) || !(GameMain.NetworkMember?.ConnectedClients is { } clients)) { return 0; } + if (GameMain.IsSingleplayer || GameMain.Client?.Name is not { } ownName || GameMain.NetworkMember?.ConnectedClients is not { } clients) { return 0; } return (from client in clients where client.Name == ownName select client.Ping).FirstOrDefault(); } + public void TreatAllButtonAction(Action onReceived) + { + if (GameMain.IsSingleplayer) + { + AddEverythingToPending(); + onReceived(new CallbackOnlyRequest(RequestResult.Success)); + OnUpdate?.Invoke(); + return; + } + + addRequests.Add(new RequestAction(onReceived, GetTimeout())); + ClientSend(null, NetworkHeader.ADD_EVERYTHING_TO_PENDING, DeliveryMethod.Reliable); + } + public void HealAllButtonAction(Action onReceived) { if (GameMain.IsSingleplayer) @@ -296,8 +258,11 @@ namespace Barotrauma private void NewAdditonReceived(IReadMessage inc, MessageFlag flag) { - NetCrewMember crewMember = INetSerializableStruct.Read(inc); - InsertPendingCrewMember(crewMember); + var crewMembers = INetSerializableStruct.Read>(inc); + foreach (var crewMember in crewMembers) + { + InsertPendingCrewMember(crewMember); + } if (flag == MessageFlag.Response && TryDequeue(addRequests, out var callback)) { callback(new CallbackOnlyRequest(RequestResult.Success)); @@ -318,11 +283,7 @@ namespace Barotrauma private static void SendAfflictionRequest(CharacterInfo info) { - INetSerializableStruct crewMember = new NetCrewMember - { - CharacterInfo = info, - Afflictions = Array.Empty() - }; + INetSerializableStruct crewMember = new NetCrewMember(info); ClientSend(crewMember, NetworkHeader.REQUEST_AFFLICTIONS, DeliveryMethod.Unreliable); } @@ -337,17 +298,17 @@ namespace Barotrauma NetCrewMember crewMember = INetSerializableStruct.Read(inc); if (TryDequeue(afflictionRequests, out var callback)) { - RequestResult result = crewMember.CharacterInfoID == 0 ? RequestResult.Error : RequestResult.Success; + RequestResult result = crewMember.CharacterInfoID is 0 ? RequestResult.Error : RequestResult.Success; callback(new AfflictionRequest(result, crewMember.Afflictions.ToImmutableArray())); } } private void PendingRequestReceived(IReadMessage inc) { - NetPendingCrew pendingCrew = INetSerializableStruct.Read(inc); + var pendingCrew = INetSerializableStruct.Read>(inc); if (TryDequeue(pendingHealRequests, out var callback)) { - callback(new PendingRequest(RequestResult.Success, pendingCrew.CrewMembers.ToImmutableArray())); + callback(new PendingRequest(RequestResult.Success, pendingCrew)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ObjectiveManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ObjectiveManager.cs new file mode 100644 index 000000000..7e70ed7f5 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ObjectiveManager.cs @@ -0,0 +1,599 @@ +using Barotrauma.Extensions; +using Barotrauma.IO; +using Barotrauma.Tutorials; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma; + +static class ObjectiveManager +{ + public class Segment + { + public readonly record struct Text( + Identifier Tag, + int Width = DefaultWidth, + int Height = DefaultHeight, + Anchor Anchor = Anchor.Center); + + public readonly record struct Video( + string FullPath, + Identifier TextTag, + int Width = DefaultWidth, + int Height = DefaultHeight) + { + public string FileName => Path.GetFileName(FullPath.CleanUpPath()); + public string ContentPath => Path.GetDirectoryName(FullPath.CleanUpPath()); + } + + private const int DefaultWidth = 450; + private const int DefaultHeight = 80; + + public GUIImage ObjectiveStateIndicator; + public GUIButton ObjectiveButton; + public GUITextBlock LinkedTextBlock; + public LocalizedString ObjectiveText; + + public readonly Identifier Id; + public readonly Text TextContent; + public readonly Video VideoContent; + public readonly AutoPlayVideo AutoPlayVideo; + + public Action OnClickObjective; + + public bool IsCompleted { get; set; } + + public bool CanBeCompleted { get; set; } + + public Identifier ParentId { get; set; } + + public TutorialSegmentType SegmentType { get; private set; } + + public static Segment CreateInfoBoxSegment(Identifier id, Identifier objectiveTextTag, AutoPlayVideo autoPlayVideo, Text textContent = default, Video videoContent = default) + { + return new Segment(id, objectiveTextTag, autoPlayVideo, textContent, videoContent); + } + + public static Segment CreateMessageBoxSegment(Identifier id, Identifier objectiveTextTag, Action onClickObjective) + { + return new Segment(id, objectiveTextTag, onClickObjective); + } + + public static Segment CreateObjectiveSegment(Identifier id, Identifier objectiveTextTag) + { + return new Segment(id, objectiveTextTag); + } + + private Segment(Identifier id, Identifier objectiveTextTag, AutoPlayVideo autoPlayVideo, Text textContent = default, Video videoContent = default) + { + Id = id; + ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag)); + AutoPlayVideo = autoPlayVideo; + TextContent = textContent; + VideoContent = videoContent; + SegmentType = TutorialSegmentType.InfoBox; + } + + private Segment(Identifier id, Identifier objectiveTextTag, Action onClickObjective) + { + Id = id; + ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag)); + OnClickObjective = onClickObjective; + SegmentType = TutorialSegmentType.MessageBox; + } + + private Segment(Identifier id, Identifier objectiveTextTag) + { + Id = id; + ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag)); + SegmentType = TutorialSegmentType.Objective; + } + + public void ConnectMessageBox(Segment messageBoxSegment) + { + SegmentType = TutorialSegmentType.MessageBox; + OnClickObjective = messageBoxSegment.OnClickObjective; + } + } + + private readonly record struct ScreenSettings( + Point ScreenResolution = default, + float UiScale = default, + WindowMode WindowMode = default) + { + public bool HaveChanged() => + GameMain.GraphicsWidth != ScreenResolution.X || + GameMain.GraphicsHeight != ScreenResolution.Y || + GUI.Scale != UiScale || + GameSettings.CurrentConfig.Graphics.DisplayMode != WindowMode; + }; + + private const float ObjectiveComponentAnimationTime = 1.5f; + + public static bool ContentRunning { get; private set; } + + public static VideoPlayer VideoPlayer { get; } = new VideoPlayer(); + + private static Segment ActiveContentSegment { get; set; } + + private readonly static List activeObjectives = new List(); + private static GUIComponent infoBox; + private static Action infoBoxClosedCallback; + private static ScreenSettings screenSettings; + private static GUILayoutGroup objectiveGroup; + private static LocalizedString objectiveTextTranslated; + + public static void AddToGUIUpdateList() + { + if (screenSettings.HaveChanged()) + { + CreateObjectiveFrame(); + } + if (activeObjectives.Count > 0 && GameMain.GameSession?.Campaign is not { ShowCampaignUI: true }) + { + objectiveGroup?.AddToGUIUpdateList(order: -1); + } + infoBox?.AddToGUIUpdateList(order: 100); + VideoPlayer.AddToGUIUpdateList(order: 100); + } + + public static void TriggerTutorialSegment(Segment segment, bool connectObjective = false) + { + if (segment.SegmentType != TutorialSegmentType.InfoBox) + { + activeObjectives.Add(segment); + AddToObjectiveList(segment, connectObjective); + return; + } + + Inventory.DraggingItems.Clear(); + ContentRunning = true; + ActiveContentSegment = segment; + + var title = TextManager.Get(segment.Id); + LocalizedString tutorialText = TextManager.GetFormatted(segment.TextContent.Tag); + tutorialText = TextManager.ParseInputTypes(tutorialText); + + switch (segment.AutoPlayVideo) + { + case AutoPlayVideo.Yes: + infoBox = CreateInfoFrame( + title, + tutorialText, + segment.TextContent.Width, + segment.TextContent.Height, + segment.TextContent.Anchor, + hasButton: true, + onInfoBoxClosed: LoadActiveContentVideo); + break; + case AutoPlayVideo.No: + infoBox = CreateInfoFrame( + title, + tutorialText, + segment.TextContent.Width, + segment.TextContent.Height, + segment.TextContent.Anchor, + hasButton: true, + onInfoBoxClosed: StopCurrentContentSegment, + onVideoButtonClicked: LoadActiveContentVideo); + break; + } + } + + public static void CompleteTutorialSegment(Identifier segmentId) + { + if (GetActiveObjective(segmentId) is not Segment segment || !segment.CanBeCompleted || segment.IsCompleted) + { + return; + } + if (!MarkSegmentCompleted(segment)) + { + return; + } + if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode) + { + GameAnalyticsManager.AddDesignEvent($"Tutorial:{tutorialMode.Tutorial?.Identifier}:{segmentId}:Completed"); + } + else if (GameMain.GameSession?.GameMode is CampaignMode campaign) + { + GameAnalyticsManager.AddDesignEvent($"Tutorial:CampaignMode:{segmentId}:Completed"); + campaign?.CampaignMetadata?.SetValue(segmentId, true); + } + } + + public static bool MarkSegmentCompleted(Segment segment, bool flash = true) + { + segment.IsCompleted = true; + if (GUIStyle.GetComponentStyle("ObjectiveIndicatorCompleted") is GUIComponentStyle style) + { + if (segment.ObjectiveStateIndicator.Style == style) + { + return false; + } + segment.ObjectiveStateIndicator.ApplyStyle(style); + } + if (flash) + { + segment.ObjectiveStateIndicator.Parent.Flash(color: GUIStyle.Green, flashDuration: 0.35f, useRectangleFlash: true); + } + segment.ObjectiveButton.OnClicked = null; + segment.ObjectiveButton.CanBeFocused = false; + return true; + } + + public static void RemoveTutorialSegment(Identifier segmentId) + { + if (GetActiveObjective(segmentId) is not Segment segment) + { + if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode) + { + DebugConsole.AddWarning($"Warning: tried to remove the tutorial segment \"{segmentId}\" in tutorial \"{tutorialMode.Tutorial?.Identifier}\" but it isn't active!"); + } + return; + } + segment.ObjectiveStateIndicator.FadeOut(ObjectiveComponentAnimationTime, false); + segment.LinkedTextBlock.FadeOut(ObjectiveComponentAnimationTime, false); + var parent = segment.LinkedTextBlock.Parent; + parent.FadeOut(ObjectiveComponentAnimationTime, true, onRemove: () => + { + activeObjectives.Remove(segment); + objectiveGroup?.Recalculate(); + }); + parent.RectTransform.MoveOverTime(GetObjectiveHiddenPosition(parent.RectTransform), ObjectiveComponentAnimationTime); + segment.ObjectiveButton.OnClicked = null; + segment.ObjectiveButton.CanBeFocused = false; + } + + public static void CloseActiveContentGUI() + { + if (VideoPlayer.IsPlaying) + { + VideoPlayer.Stop(); + } + else if (infoBox != null) + { + CloseInfoFrame(); + } + } + + public static void ClearContent() + { + ContentRunning = false; + infoBox = null; + } + + public static void ResetUI() + { + ContentRunning = false; + infoBox = null; + VideoPlayer.Remove(); + } + + #region Objectives + private static Segment GetActiveObjective(Identifier id) => activeObjectives.FirstOrDefault(s => s.Id == id); + + public static void ResetObjectives() + { + activeObjectives.Clear(); + ActiveContentSegment = null; + CreateObjectiveFrame(); + } + + /// + /// Create the objective list that holds the objectives (called on start and on resolution change) + /// + private static void CreateObjectiveFrame() + { + var objectiveListFrame = new GUIFrame(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.TutorialObjectiveListArea, GUI.Canvas), style: null) + { + CanBeFocused = false + }; + objectiveGroup = new GUILayoutGroup(new RectTransform(Vector2.One, objectiveListFrame.RectTransform)) + { + AbsoluteSpacing = (int)GUIStyle.Font.LineHeight + }; + for (int i = 0; i < activeObjectives.Count; i++) + { + AddToObjectiveList(activeObjectives[i]); + } + screenSettings = new ScreenSettings(new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight), GUI.Scale, GameSettings.CurrentConfig.Graphics.DisplayMode); + } + + /// + /// Stops content running and adds the active segment to the objective list + /// + private static void StopCurrentContentSegment() + { + if (!ActiveContentSegment.ObjectiveText.IsNullOrEmpty()) + { + activeObjectives.Add(ActiveContentSegment); + AddToObjectiveList(ActiveContentSegment); + } + ContentRunning = false; + ActiveContentSegment = null; + } + + /// + /// Adds the segment to the objective list + /// + private static void AddToObjectiveList(Segment segment, bool connectExisting = false) + { + if (connectExisting) + { + if (activeObjectives.Find(o => o.Id == segment.Id) is { } existingSegment) + { + existingSegment.ConnectMessageBox(segment); + SetButtonBehavior(existingSegment); + } + return; + } + + var frameRt = new RectTransform(new Vector2(1.0f, 0.1f), objectiveGroup.RectTransform) + { + MinSize = new Point(0, objectiveGroup.AbsoluteSpacing) + }; + Segment parentSegment = activeObjectives.FirstOrDefault(s => s.Id == segment.ParentId); + if (parentSegment is not null) + { + // Add this child as the last child in case there are other existing children already + int totalChildren = activeObjectives.Count(s => s.ParentId == segment.ParentId); + int childIndex = activeObjectives.IndexOf(parentSegment) + totalChildren; + if (objectiveGroup.RectTransform.GetChildIndex(frameRt) != childIndex) + { + frameRt.RepositionChildInHierarchy(childIndex); + activeObjectives.Remove(segment); + activeObjectives.Insert(childIndex, segment); + } + } + frameRt.AbsoluteOffset = GetObjectiveHiddenPosition(); + + var frame = new GUIFrame(frameRt, style: null) + { + CanBeFocused = true + }; + + objectiveGroup.Recalculate(); + + int textWidth = parentSegment is null ? frameRt.Rect.Width - objectiveGroup.AbsoluteSpacing + : frameRt.Rect.Width - 2 * objectiveGroup.AbsoluteSpacing; + segment.LinkedTextBlock = new GUITextBlock( + new RectTransform(new Point(textWidth, 0), frame.RectTransform, anchor: Anchor.TopRight), + TextManager.ParseInputTypes(segment.ObjectiveText), + wrap: true); + + var size = new Point(segment.LinkedTextBlock.Rect.Width, segment.LinkedTextBlock.Rect.Height); + segment.LinkedTextBlock.RectTransform.NonScaledSize = size; + segment.LinkedTextBlock.RectTransform.MinSize = size; + segment.LinkedTextBlock.RectTransform.MaxSize = size; + segment.LinkedTextBlock.RectTransform.IsFixedSize = true; + frame.RectTransform.Resize(new Point(frame.Rect.Width, segment.LinkedTextBlock.RectTransform.Rect.Height), resizeChildren: false); + frame.RectTransform.IsFixedSize = true; + + var indicatorRt = new RectTransform(new Point(objectiveGroup.AbsoluteSpacing), frame.RectTransform, isFixedSize: true); + if (parentSegment is not null) + { + indicatorRt.AbsoluteOffset = new Point(objectiveGroup.AbsoluteSpacing, 0); + } + segment.ObjectiveStateIndicator = new GUIImage(indicatorRt, "ObjectiveIndicatorIncomplete"); + + SetTransparent(segment.LinkedTextBlock); + + objectiveTextTranslated ??= TextManager.Get("Tutorial.Objective"); + segment.ObjectiveButton = new GUIButton(new RectTransform(Vector2.One, segment.LinkedTextBlock.RectTransform, Anchor.TopLeft, Pivot.TopLeft), style: null) + { + ToolTip = objectiveTextTranslated + }; + SetButtonBehavior(segment); + SetTransparent(segment.ObjectiveButton); + + frameRt.MoveOverTime(new Point(0, frameRt.AbsoluteOffset.Y), ObjectiveComponentAnimationTime, onDoneMoving: () => objectiveGroup?.Recalculate()); + + // Check if the objective has already been completed in the campaign + if (!segment.IsCompleted && GameMain.GameSession?.Campaign?.CampaignMetadata is CampaignMetadata data && data.GetBoolean(segment.Id)) + { + MarkSegmentCompleted(segment, flash: false); + } + + static void SetTransparent(GUIComponent component) => component.Color = component.HoverColor = component.PressedColor = component.SelectedColor = Color.Transparent; + + void SetButtonBehavior(Segment segment) + { + segment.ObjectiveButton.CanBeFocused = segment.SegmentType != TutorialSegmentType.Objective; + segment.ObjectiveButton.OnClicked = (GUIButton btn, object userdata) => + { + if (segment.SegmentType == TutorialSegmentType.InfoBox) + { + if (segment.AutoPlayVideo == AutoPlayVideo.Yes) + { + ReplaySegmentVideo(segment); + } + else + { + ShowSegmentText(segment); + } + } + else if (segment.SegmentType == TutorialSegmentType.MessageBox) + { + segment.OnClickObjective?.Invoke(); + } + return true; + }; + } + } + + private static void ReplaySegmentVideo(Segment segment) + { + if (ContentRunning) { return; } + Inventory.DraggingItems.Clear(); + ContentRunning = true; + LoadVideo(segment); + } + + private static void ShowSegmentText(Segment segment) + { + if (ContentRunning) { return; } + Inventory.DraggingItems.Clear(); + ContentRunning = true; + ActiveContentSegment = segment; + infoBox = CreateInfoFrame( + TextManager.Get(segment.Id), + TextManager.Get(segment.TextContent.Tag), + segment.TextContent.Width, + segment.TextContent.Height, + segment.TextContent.Anchor, + hasButton: true, + onInfoBoxClosed: () => ContentRunning = false, + onVideoButtonClicked: () => LoadVideo(segment)); + } + + private static Point GetObjectiveHiddenPosition(RectTransform rt = null) + { + return new Point(GameMain.GraphicsWidth - objectiveGroup.Rect.X, rt?.AbsoluteOffset.Y ?? 0); + } + + public static Segment GetObjective(Identifier identifier) + { + return activeObjectives.FirstOrDefault(o => o.Id == identifier); + } + + public static bool AllActiveObjectivesCompleted() + { + return activeObjectives.None() || activeObjectives.All(o => !o.CanBeCompleted || o.IsCompleted); + } + + public static bool AnyObjectives => activeObjectives.Any(); + + #endregion + + #region InfoFrame + + private static void CloseInfoFrame() => CloseInfoFrame(null, null); + + private static bool CloseInfoFrame(GUIButton button, object userData) + { + infoBox = null; + infoBoxClosedCallback?.Invoke(); + return true; + } + + /// + // Creates and displays a tutorial info box + /// + private static GUIComponent CreateInfoFrame(LocalizedString title, LocalizedString text, int width = 300, int height = 80, Anchor anchor = Anchor.TopRight, bool hasButton = false, Action onInfoBoxClosed = null, Action onVideoButtonClicked = null) + { + if (hasButton) + { + height += 60; + } + + width = (int)(width * GUI.Scale); + height = (int)(height * GUI.Scale); + + LocalizedString wrappedText = ToolBox.WrapText(text, width, GUIStyle.Font); + height += (int)GUIStyle.Font.MeasureString(wrappedText).Y; + + if (title.Length > 0) + { + height += (int)GUIStyle.Font.MeasureString(title).Y + (int)(150 * GUI.Scale); + } + + var background = new GUIFrame(new RectTransform(new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight), GUI.Canvas, Anchor.Center), style: "GUIBackgroundBlocker"); + + var infoBlock = new GUIFrame(new RectTransform(new Point(width, height), background.RectTransform, anchor)); + infoBlock.Flash(GUIStyle.Green); + + var infoContent = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), infoBlock.RectTransform, Anchor.Center)) + { + Stretch = true, + AbsoluteSpacing = 5 + }; + + if (title.Length > 0) + { + var titleBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), + title, font: GUIStyle.LargeFont, textAlignment: Alignment.Center, textColor: new Color(253, 174, 0)); + titleBlock.RectTransform.IsFixedSize = true; + } + + text = RichString.Rich(text); + GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), text, wrap: true); + + textBlock.RectTransform.IsFixedSize = true; + infoBoxClosedCallback = onInfoBoxClosed; + + if (hasButton) + { + var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), infoContent.RectTransform), isHorizontal: true) + { + RelativeSpacing = 0.1f + }; + buttonContainer.RectTransform.IsFixedSize = true; + + if (onVideoButtonClicked != null) + { + buttonContainer.Stretch = true; + var videoButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), buttonContainer.RectTransform), + TextManager.Get("Video"), style: "GUIButtonLarge") + { + OnClicked = (GUIButton button, object obj) => + { + onVideoButtonClicked(); + return true; + } + }; + } + else + { + buttonContainer.Stretch = false; + buttonContainer.ChildAnchor = Anchor.Center; + } + + var okButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), buttonContainer.RectTransform), + TextManager.Get("OK"), style: "GUIButtonLarge") + { + OnClicked = CloseInfoFrame + }; + } + + infoBlock.RectTransform.NonScaledSize = new Point(infoBlock.Rect.Width, (int)(infoContent.Children.Sum(c => c.Rect.Height + infoContent.AbsoluteSpacing) / infoContent.RectTransform.RelativeSize.Y)); + + SoundPlayer.PlayUISound(GUISoundType.UIMessage); + + return background; + } + + #endregion + + #region Video + + private static void LoadVideo(Segment segment) + { + if (segment.AutoPlayVideo == AutoPlayVideo.Yes) + { + VideoPlayer.LoadContent( + contentPath: segment.VideoContent.ContentPath, + videoSettings: new VideoPlayer.VideoSettings(segment.VideoContent.FileName), + textSettings: new VideoPlayer.TextSettings(segment.VideoContent.TextTag, segment.VideoContent.Width), + contentId: segment.Id, + startPlayback: true, + objective: segment.ObjectiveText, + onStop: StopCurrentContentSegment); + } + else + { + VideoPlayer.LoadContent( + contentPath: segment.VideoContent.ContentPath, + videoSettings: new VideoPlayer.VideoSettings(segment.VideoContent.FileName), + textSettings: null, + contentId: segment.Id, + startPlayback: true, + objective: string.Empty); + } + } + + private static void LoadActiveContentVideo() => LoadVideo(ActiveContentSegment); + + #endregion +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index ef7418390..714a96576 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -801,7 +801,7 @@ namespace Barotrauma } else if (character.HeldItems.Any(i => i.OwnInventory != null && - (i.OwnInventory.CanBePut(item) || (i.OwnInventory.Capacity == 1 && i.OwnInventory.AllowSwappingContainedItems && i.OwnInventory.Container.CanBeContained(item))))) + (i.OwnInventory.CanBePut(item) || ((i.OwnInventory.Capacity == 1 || i.OwnInventory.Container.HasSubContainers) && i.OwnInventory.AllowSwappingContainedItems && i.OwnInventory.Container.CanBeContained(item))))) { return QuickUseAction.PutToEquippedItem; } @@ -975,7 +975,7 @@ namespace Barotrauma heldItem.OwnInventory.GetItemAt(0)?.Prefab == item.Prefab && heldItem.OwnInventory.GetItemsAt(0).Count() > 1; if (heldItem.OwnInventory.TryPutItem(item, Character.Controlled) || - (heldItem.OwnInventory.Capacity == 1 && heldItem.OwnInventory.TryPutItem(item, 0, allowSwapping: !disallowSwapping, allowCombine: false, user: Character.Controlled))) + ((heldItem.OwnInventory.Capacity == 1 || heldItem.OwnInventory.Container.HasSubContainers) && heldItem.OwnInventory.TryPutItem(item, 0, allowSwapping: !disallowSwapping, allowCombine: false, user: Character.Controlled))) { success = true; for (int j = 0; j < capacity; j++) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs index f97cdc3be..539273366 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs @@ -224,68 +224,60 @@ namespace Barotrauma.Items.Components if (character == null) { return false; } if (character == Character.Controlled) { - if (targetSections.Count == 0) { return false; } - Spray(deltaTime); + if (targetSections.Count == 0) { return false; } + Spray(character, deltaTime, applyColors: true); return true; } else { //allow remote players to use the sprayer, but don't actually color the walls (we'll receive the data from the server) - return character.IsRemotePlayer; + Spray(character, deltaTime, applyColors: false); + return true; } } - public void Spray(float deltaTime) + public void Spray(Character user, float deltaTime, bool applyColors) { - if (targetSections.Count == 0) { return; } - Item liquidItem = liquidContainer?.Inventory.FirstOrDefault(); if (liquidItem == null) { return; } bool isCleaning = false; liquidColors.TryGetValue(liquidItem.Prefab.Identifier, out color); - // Ethanol or other cleaning solvent - if (color.A == 0) { isCleaning = true; } - - float sizeAdjustedSprayStrength = SprayStrength / targetSections.Count; - - if (!isCleaning) + if (applyColors && targetSections.Any()) { - for (int i = 0; i < targetSections.Count; i++) + // Ethanol or other cleaning solvent + if (color.A == 0) { isCleaning = true; } + float sizeAdjustedSprayStrength = SprayStrength / targetSections.Count; + if (!isCleaning) { - targetHull.IncreaseSectionColorOrStrength(targetSections[i], color, sizeAdjustedSprayStrength * deltaTime, true, false); + for (int i = 0; i < targetSections.Count; i++) + { + targetHull.IncreaseSectionColorOrStrength(targetSections[i], color, sizeAdjustedSprayStrength * deltaTime, true, false); + } + if (GameMain.GameSession != null) + { + GameMain.GameSession.TimeSpentCleaning += deltaTime; + } } - if (GameMain.GameSession != null) + else { - GameMain.GameSession.TimeSpentCleaning += deltaTime; - } - } - else - { - for (int i = 0; i < targetSections.Count; i++) - { - targetHull.CleanSection(targetSections[i], -sizeAdjustedSprayStrength * deltaTime, true); - } - if (GameMain.GameSession != null) - { - GameMain.GameSession.TimeSpentPainting += deltaTime; + for (int i = 0; i < targetSections.Count; i++) + { + targetHull.CleanSection(targetSections[i], -sizeAdjustedSprayStrength * deltaTime, true); + } + if (GameMain.GameSession != null) + { + GameMain.GameSession.TimeSpentPainting += deltaTime; + } } } Vector2 particleStartPos = item.WorldPosition + ConvertUnits.ToDisplayUnits(TransformedBarrelPos); - Vector2 particleEndPos = Vector2.Zero; - for (int i = 0; i < targetSections.Count; i++) - { - particleEndPos += new Vector2(targetSections[i].Rect.Center.X, targetSections[i].Rect.Y - targetSections[i].Rect.Height / 2) + targetHull.Rect.Location.ToVector2(); - } - particleEndPos /= targetSections.Count; - if (targetHull?.Submarine != null) - { - particleEndPos += targetHull.Submarine.Position; - } - float dist = Vector2.Distance(particleStartPos, particleEndPos); - + Vector2 particleEndPos = user.CursorWorldPosition; + //the cursor position is not exact for remote players, we only know the direction they're aiming at but not the distance + // -> use 50% range, looks good enough + float dist = Math.Min(Vector2.Distance(particleStartPos, particleEndPos), Range * 0.5f); foreach (ParticleEmitter particleEmitter in particleEmitters) { float particleAngle = item.body.Rotation + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index d43d947fd..d8c379c03 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -309,21 +309,53 @@ namespace Barotrauma.Items.Components Vector2 currentItemPos = transformedItemPos; - SpriteEffects spriteEffects = SpriteEffects.None; - if ((item.body != null && item.body.Dir == -1) || item.FlippedX) - { - spriteEffects |= MathUtils.NearlyEqual(ItemRotation % 180, 90.0f) ? SpriteEffects.FlipVertically : SpriteEffects.FlipHorizontally; - } - if (item.FlippedY) - { - spriteEffects |= MathUtils.NearlyEqual(ItemRotation % 180, 90.0f) ? SpriteEffects.FlipHorizontally : SpriteEffects.FlipVertically; - } - bool isWiringMode = SubEditorScreen.TransparentWiringMode && SubEditorScreen.IsWiringMode(); int i = 0; foreach (Item containedItem in Inventory.AllItems) { + Vector2 itemPos = currentItemPos; + var relatedItem = FindContainableItem(containedItem); + if (relatedItem != null) + { + if (relatedItem.Hide.HasValue && relatedItem.Hide.Value) { continue; } + if (relatedItem.ItemPos.HasValue) + { + Vector2 pos = relatedItem.ItemPos.Value; + if (item.body != null) + { + Matrix transform = Matrix.CreateRotationZ(item.body.DrawRotation); + pos.X *= item.body.Dir; + itemPos = Vector2.Transform(pos, transform) + item.body.DrawPosition; + } + else + { + itemPos = pos; + // This code is aped based on above. Not tested. + if (item.FlippedX) + { + itemPos.X = -itemPos.X; + itemPos.X += item.Rect.Width; + } + if (item.FlippedY) + { + itemPos.Y = -itemPos.Y; + itemPos.Y -= item.Rect.Height; + } + itemPos += new Vector2(item.Rect.X, item.Rect.Y); + if (item.Submarine != null) + { + itemPos += item.Submarine.DrawPosition; + } + if (Math.Abs(item.RotationRad) > 0.01f) + { + Matrix transform = Matrix.CreateRotationZ(-item.RotationRad); + itemPos = Vector2.Transform(itemPos - item.DrawPosition, transform) + item.DrawPosition; + } + } + } + } + if (containedItem?.Sprite == null) { continue; } if (AutoInteractWithContained) @@ -343,19 +375,34 @@ namespace Barotrauma.Items.Components } containedSpriteDepth = itemDepth + (containedSpriteDepth - (item.Sprite?.Depth ?? item.SpriteDepth)) / 10000.0f; + SpriteEffects spriteEffects = SpriteEffects.None; + float spriteRotation = ItemRotation; + if (relatedItem != null && relatedItem.Rotation != 0) + { + spriteRotation = relatedItem.Rotation; + } + if ((item.body != null && item.body.Dir == -1) || item.FlippedX) + { + spriteEffects |= MathUtils.NearlyEqual(spriteRotation % 180, 90.0f) ? SpriteEffects.FlipVertically : SpriteEffects.FlipHorizontally; + } + if (item.FlippedY) + { + spriteEffects |= MathUtils.NearlyEqual(spriteRotation % 180, 90.0f) ? SpriteEffects.FlipHorizontally : SpriteEffects.FlipVertically; + } + containedItem.Sprite.Draw( spriteBatch, - new Vector2(currentItemPos.X, -currentItemPos.Y), + new Vector2(itemPos.X, -itemPos.Y), isWiringMode ? containedItem.GetSpriteColor(withHighlight: true) * 0.15f : containedItem.GetSpriteColor(withHighlight: true), origin, - -(containedItem.body == null ? 0.0f : containedItem.body.DrawRotation ), + -(containedItem.body == null ? 0.0f : containedItem.body.DrawRotation), containedItem.Scale, spriteEffects, depth: containedSpriteDepth); foreach (ItemContainer ic in containedItem.GetComponents()) { - if (ic.hideItems) continue; + if (ic.hideItems) { continue; } ic.DrawContainedItems(spriteBatch, containedSpriteDepth); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index 332f924cd..1d4db25d2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -86,7 +86,7 @@ namespace Barotrauma.Items.Components public override void FlipX(bool relativeToSub) { - if (Light?.LightSprite != null && item.Prefab.CanSpriteFlipX && item.body == null) + if (Light?.LightSprite != null && item.Prefab.CanSpriteFlipX) { Light.LightSpriteEffect = Light.LightSpriteEffect == SpriteEffects.None ? SpriteEffects.FlipHorizontally : SpriteEffects.None; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index b6ff975de..90ff7ba05 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -811,7 +811,7 @@ namespace Barotrauma.Items.Components if (distSqr > t.SoundRange * t.SoundRange * 2) { continue; } float dist = (float)Math.Sqrt(distSqr); - if (dist > prevPassivePingRadius * Range && dist <= passivePingRadius * Range && Rand.Int(sonarBlips.Count) < 500) + if (dist > prevPassivePingRadius * Range && dist <= passivePingRadius * Range && Rand.Int(sonarBlips.Count) < 500 && t.IsWithinSector(transducerCenter)) { Ping(t.WorldPosition, transducerCenter, Math.Min(t.SoundRange, range * 0.5f) * displayScale, 0, displayScale, Math.Min(t.SoundRange, range * 0.5f), @@ -1276,7 +1276,7 @@ namespace Barotrauma.Items.Components float indicatorSector = sector * 0.75f; float indicatorSectorLength = (float)(midLength / Math.Cos(indicatorSector)); - bool withinSector = + bool withinSector = (Math.Abs(diff.X) < steering.ActiveDockingSource.DistanceTolerance.X && Math.Abs(diff.Y) < steering.ActiveDockingSource.DistanceTolerance.Y) || Vector2.Dot(normalizedDockingDir, MathUtils.RotatePoint(normalizedDockingDir, indicatorSector)) < Vector2.Dot(normalizedDockingDir, Vector2.Normalize(dockingDir)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index 1eff3054d..3759e9fd1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -54,7 +54,7 @@ namespace Barotrauma.Items.Components private bool? swapDestinationOrder; - private GUIMessageBox enterOutpostPrompt; + private GUIMessageBox enterOutpostPrompt, exitOutpostPrompt; private bool levelStartSelected; public bool LevelStartSelected @@ -382,9 +382,20 @@ namespace Barotrauma.Items.Components DockingSources.Any(d => d.Docked && (d.DockingTarget?.Item.Submarine?.Info?.IsOutpost ?? false))) { // Undocking from an outpost - campaign.ShowCampaignUI = true; - campaign.CampaignUI.SelectTab(CampaignMode.InteractionType.Map); - return false; + if (!ObjectiveManager.AllActiveObjectivesCompleted()) + { + exitOutpostPrompt = new GUIMessageBox("", + TextManager.GetWithVariable("CampaignExitTutorialOutpostPrompt", "[locationname]", campaign.Map.CurrentLocation.Name), + new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); + exitOutpostPrompt.Buttons[0].OnClicked += (_, _) => + { + exitOutpostPrompt.Close(); + return OpenMap(campaign); + }; + exitOutpostPrompt.Buttons[1].OnClicked += exitOutpostPrompt.Close; + return false; + } + return OpenMap(campaign); } else if (!Level.IsLoadedOutpost && DockingModeEnabled && ActiveDockingSource != null && !ActiveDockingSource.Docked && DockingTarget?.Item?.Submarine == Level.Loaded.StartOutpost && (DockingTarget?.Item?.Submarine?.Info.IsOutpost ?? false)) @@ -419,6 +430,14 @@ namespace Barotrauma.Items.Components return true; } }; + + bool OpenMap(CampaignMode campaign) + { + campaign.ShowCampaignUI = true; + campaign.CampaignUI.SelectTab(CampaignMode.InteractionType.Map); + return false; + } + void SendDockingSignal() { if (GameMain.Client == null) @@ -431,6 +450,7 @@ namespace Barotrauma.Items.Components item.CreateClientEvent(this); } } + dockingButton.Font = GUIStyle.SubHeadingFont; dockingButton.TextBlock.RectTransform.MaxSize = new Point((int)(dockingButton.Rect.Width * 0.7f), int.MaxValue); dockingButton.TextBlock.AutoScaleHorizontal = true; @@ -913,6 +933,7 @@ namespace Barotrauma.Items.Components maintainPosOriginIndicator?.Remove(); steeringIndicator?.Remove(); enterOutpostPrompt?.Close(); + exitOutpostPrompt?.Close(); pathFinder = null; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs index d6e5500c3..afe938020 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs @@ -293,7 +293,7 @@ namespace Barotrauma.Items.Components if (target.Bleeding > 0.0f) { - int bleedingTextIndex = MathHelper.Clamp((int)Math.Floor(target.Bleeding / 100.0f) * BleedingTexts.Length, 0, BleedingTexts.Length - 1); + int bleedingTextIndex = MathHelper.Clamp((int)Math.Floor(target.Bleeding / 100.0f * BleedingTexts.Length), 0, BleedingTexts.Length - 1); texts.Add(BleedingTexts[bleedingTextIndex]); textColors.Add(Color.Lerp(GUIStyle.Orange, GUIStyle.Red, target.Bleeding / 100.0f)); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs index 4d4893e4e..58093440a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using Barotrauma.Networking; @@ -6,7 +7,7 @@ namespace Barotrauma.Items.Components { partial class Wearable : Pickable, IServerSerializable { - private void GetDamageModifierText(ref LocalizedString description, DamageModifier damageModifier, Identifier afflictionIdentifier) + private static void GetDamageModifierText(ref LocalizedString description, DamageModifier damageModifier, Identifier afflictionIdentifier) { int roundedValue = (int)Math.Round((1 - damageModifier.DamageMultiplier * damageModifier.ProbabilityMultiplier) * 100); if (roundedValue == 0) { return; } @@ -19,8 +20,13 @@ namespace Barotrauma.Items.Components if (!description.IsNullOrWhiteSpace()) { description += '\n'; } description += $" ‖color:{colorStr}‖{roundedValue.ToString("-0;+#")}%‖color:end‖ {afflictionName}"; } - + public override void AddTooltipInfo(ref LocalizedString name, ref LocalizedString description) + { + AddTooltipInfo(damageModifiers, SkillModifiers, ref description); + } + + public static void AddTooltipInfo(IReadOnlyList damageModifiers, IReadOnlyDictionary skillModifiers, ref LocalizedString description) { if (damageModifiers.Any()) { @@ -41,9 +47,9 @@ namespace Barotrauma.Items.Components } } } - if (SkillModifiers.Any()) + if (skillModifiers.Any()) { - foreach (var skillModifier in SkillModifiers) + foreach (var skillModifier in skillModifiers) { string colorStr = XMLExtensions.ToStringHex(GUIStyle.Green); int roundedValue = (int)Math.Round(skillModifier.Value); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 7858e3154..d0a4414a1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -1610,15 +1610,19 @@ namespace Barotrauma } else if (itemContainer.ShowTotalStackCapacityInContainedStateIndicator) { - containedState = itemContainer.Inventory.AllItems.Count() / (float)(itemContainer.GetMaxStackSize(0) * itemContainer.Capacity); + int ignoredItems = itemContainer.AllSubContainableItems == null ? 0 : itemContainer.AllSubContainableItems.Count; + int itemCount = itemContainer.Inventory.AllItems.Count() - ignoredItems; + containedState = itemCount / (float)(itemContainer.GetMaxStackSize(0) * itemContainer.MainContainerCapacity); } else { var containedItem = itemContainer.Inventory.slots[Math.Max(itemContainer.ContainedStateIndicatorSlot, 0)].FirstOrDefault(); + containedState = itemContainer.Inventory.Capacity == 1 || itemContainer.ContainedStateIndicatorSlot > -1 ? (containedItem == null ? 0.0f : containedItem.Condition / containedItem.MaxCondition) : itemContainer.Inventory.slots.Count(i => !i.Empty()) / (float)itemContainer.Inventory.capacity; - if (containedItem != null && itemContainer.Inventory.Capacity == 1) + + if (containedItem != null && (itemContainer.Inventory.Capacity == 1 || itemContainer.HasSubContainers)) { int maxStackSize = Math.Min(containedItem.Prefab.MaxStackSize, itemContainer.GetMaxStackSize(0)); if (maxStackSize > 1 || containedItem.Prefab.HideConditionBar) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs index bc69b2e51..a39cd80e5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs @@ -1,5 +1,5 @@ -using Barotrauma.IO; -using Barotrauma.Extensions; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -7,7 +7,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -73,6 +72,9 @@ namespace Barotrauma public float UpgradePreviewScale = 1.0f; + private IReadOnlyList wearableDamageModifiers; + private IReadOnlyDictionary wearableSkillModifiers; + //only used to display correct color in the sub editor, item instances have their own property that can be edited on a per-item basis [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.No)] public Color InventoryIconColor { get; protected set; } @@ -101,6 +103,9 @@ namespace Barotrauma var containedSprites = new List(); var decorativeSpriteGroups = new Dictionary>(); + var wearableDamageModifiers = new List(); + var wearableSkillModifiers = new Dictionary(); + foreach (var subElement in element.Elements()) { switch (subElement.Name.LocalName.ToLowerInvariant()) @@ -198,8 +203,33 @@ namespace Barotrauma containedSprites.Add(containedSprite); } break; + case "wearable": + foreach (ContentXElement wearableSubElement in subElement.Elements()) + { + switch (wearableSubElement.Name.LocalName.ToLowerInvariant()) + { + case "damagemodifier": + wearableDamageModifiers.Add(new DamageModifier(wearableSubElement, Name.Value + ", Wearable", checkErrors: false)); + break; + case "skillmodifier": + Identifier skillIdentifier = wearableSubElement.GetAttributeIdentifier("skillidentifier", Identifier.Empty); + float skillValue = wearableSubElement.GetAttributeFloat("skillvalue", 0f); + if (wearableSkillModifiers.ContainsKey(skillIdentifier)) + { + wearableSkillModifiers[skillIdentifier] += skillValue; + } + else + { + wearableSkillModifiers.TryAdd(skillIdentifier, skillValue); + } + break; + } + } + break; } } + this.wearableDamageModifiers = wearableDamageModifiers.ToImmutableList(); + this.wearableSkillModifiers = wearableSkillModifiers.ToImmutableDictionary(); UpgradeOverrideSprites = upgradeOverrideSprites.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray())).ToImmutableDictionary(); BrokenSprites = brokenSprites.ToImmutableArray(); @@ -211,9 +241,21 @@ namespace Barotrauma public bool CanCharacterBuy() { if (!DefaultPrice.RequiresUnlock) { return true; } - return Character.Controlled is not null && Character.Controlled.HasStoreAccessForItem(this); } + public LocalizedString GetTooltip() + { + LocalizedString tooltip = $"‖color:{XMLExtensions.ToStringHex(GUIStyle.TextColorBright)}‖{Name}‖color:end‖"; + if (!Description.IsNullOrEmpty()) + { + tooltip += $"\n{Description}"; + } + if (wearableDamageModifiers.Any() || wearableSkillModifiers.Any()) + { + Wearable.AddTooltipInfo(wearableDamageModifiers, wearableSkillModifiers, ref tooltip); + } + return tooltip; + } public override void UpdatePlacing(Camera cam) { @@ -320,15 +362,7 @@ namespace Barotrauma } else { - Vector2 position = Submarine.MouseToWorldGrid(Screen.Selected.Cam, Submarine.MainSub); - Vector2 placeSize = Size * Scale; - if (placePosition != Vector2.Zero) - { - if (ResizeHorizontal) { placeSize.X = Math.Max(position.X - placePosition.X, placeSize.X); } - if (ResizeVertical) { placeSize.Y = Math.Max(placePosition.Y - position.Y, placeSize.Y); } - position = placePosition; - } - Sprite?.DrawTiled(spriteBatch, new Vector2(position.X, -position.Y), placeSize, color: SpriteColor); + Sprite.DrawTiled(spriteBatch, new Vector2(placeRect.X, -placeRect.Y), placeRect.Size.ToVector2(), SpriteColor * 0.8f); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs index bdf919218..c123c22f0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -1,4 +1,5 @@ -using Barotrauma.Networking; +using Barotrauma.Extensions; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; @@ -178,7 +179,7 @@ namespace Barotrauma activeSprite?.Draw( spriteBatch, new Vector2(obj.Position.X, -obj.Position.Y) - camDiff * obj.Position.Z / 10000.0f, - Color.Lerp(Color.White, Level.Loaded.BackgroundTextureColor, obj.Position.Z / 3000.0f), + Color.Lerp(obj.Prefab.SpriteColor, obj.Prefab.SpriteColor.Multiply(Level.Loaded.BackgroundTextureColor), obj.Position.Z / 3000.0f), activeSprite.Origin, obj.CurrentRotation, obj.CurrentScale, @@ -200,7 +201,7 @@ namespace Barotrauma obj.ActivePrefab.DeformableSprite.Origin, obj.CurrentRotation, obj.CurrentScale, - Color.Lerp(Color.White, Level.Loaded.BackgroundTextureColor, obj.Position.Z / 5000.0f)); + Color.Lerp(obj.Prefab.SpriteColor, obj.Prefab.SpriteColor.Multiply(Level.Loaded.BackgroundTextureColor), obj.Position.Z / 5000.0f)); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index c32f82187..db0dcb504 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -291,7 +291,7 @@ namespace Barotrauma if (!currentDisplayLocation.Discovered) { RemoveFogOfWar(currentDisplayLocation); - currentDisplayLocation.Discover(); + Discover(currentDisplayLocation); if (currentDisplayLocation.MapPosition.X > furthestDiscoveredLocation.MapPosition.X) { furthestDiscoveredLocation = currentDisplayLocation; @@ -452,7 +452,7 @@ namespace Barotrauma Level.Loaded.DebugSetStartLocation(CurrentLocation); Level.Loaded.DebugSetEndLocation(null); - CurrentLocation.Discover(); + Discover(CurrentLocation); OnLocationChanged?.Invoke(new LocationChangeInfo(prevLocation, CurrentLocation)); SelectLocation(-1); if (GameMain.Client == null) @@ -693,7 +693,22 @@ namespace Barotrauma pos.Y = (int)pos.Y; Vector2 nameSize = GUIStyle.LargeFont.MeasureString(HighlightedLocation.Name); Vector2 typeSize = HighlightedLocation.Type.Name.IsNullOrEmpty() ? Vector2.Zero : GUIStyle.Font.MeasureString(HighlightedLocation.Type.Name); - Vector2 size = new Vector2(Math.Max(nameSize.X, typeSize.X), nameSize.Y + typeSize.Y); + Vector2 descSize = HighlightedLocation.Type.Description.IsNullOrEmpty() ? Vector2.Zero : GUIStyle.SmallFont.MeasureString(HighlightedLocation.Type.Description); + Vector2 size = new Vector2(Math.Max(nameSize.X, Math.Max(typeSize.X, descSize.X)), nameSize.Y + typeSize.Y + descSize.Y); + + int highestSubTier = HighlightedLocation.HighestSubmarineTierAvailable(); + var overrideTiers = new List<(SubmarineClass subClass, int tier)>(); + foreach (SubmarineClass subClass in Enum.GetValues(typeof(SubmarineClass))) + { + if (subClass == SubmarineClass.Undefined) { continue; } + int highestClassTier = HighlightedLocation.HighestSubmarineTierAvailable(subClass); + if (highestClassTier > 0 && highestClassTier > highestSubTier) + { + overrideTiers.Add((subClass, highestClassTier)); + } + } + size.Y += ((highestSubTier > 0 ? 1 : 0) + overrideTiers.Count) * GUIStyle.SmallFont.MeasureString(TextManager.Get("advancedsub.all")).Y; + bool showReputation = hudVisibility > 0.0f && HighlightedLocation.Discovered && HighlightedLocation.Type.HasOutpost && HighlightedLocation.Reputation != null; LocalizedString repLabelText = null, repValueText = null; Vector2 repLabelSize = Vector2.Zero, repBarSize = Vector2.Zero; @@ -706,21 +721,51 @@ namespace Barotrauma repValueText = HighlightedLocation.Reputation.GetFormattedReputationText(addColorTags: false); size.X = Math.Max(size.X, repBarSize.X + GUIStyle.Font.MeasureString(repValueText).X + GUI.IntScale(10)); } + GUIStyle.GetComponentStyle("OuterGlow").Sprites[GUIComponent.ComponentState.None][0].Draw( - spriteBatch, new Rectangle((int)(pos.X - 60 * GUI.Scale), (int)(pos.Y - size.Y), (int)(size.X + 120 * GUI.Scale), (int)(size.Y * 2.2f)), Color.Black * hudVisibility); + spriteBatch, + new Rectangle( + (int)(pos.X - 60 * GUI.Scale), + (int)(pos.Y - size.Y), + (int)(size.X + 120 * GUI.Scale), + (int)(size.Y * 2.2f)), + Color.Black * hudVisibility); + var topLeftPos = pos - new Vector2(0.0f, size.Y / 2); GUI.DrawString(spriteBatch, topLeftPos, HighlightedLocation.Name, GUIStyle.TextColorNormal * hudVisibility * 1.5f, font: GUIStyle.LargeFont); topLeftPos += new Vector2(0.0f, nameSize.Y); - GUI.DrawString(spriteBatch, topLeftPos, HighlightedLocation.Type.Name, GUIStyle.TextColorNormal * hudVisibility * 1.5f); + DrawText(HighlightedLocation.Type.Name); + if (!HighlightedLocation.Type.Description.IsNullOrEmpty()) + { + topLeftPos += new Vector2(0.0f, descSize.Y); + DrawText(HighlightedLocation.Type.Description, font: GUIStyle.SmallFont); + } + + if (highestSubTier > 0) + { + DrawSubAvailabilityText("advancedsub.all", highestSubTier); + } + foreach (var (subClass, tier) in overrideTiers) + { + DrawSubAvailabilityText($"advancedsub.{subClass}", tier); + } + void DrawSubAvailabilityText(string tag, int tier) + { + topLeftPos += new Vector2(0.0f, typeSize.Y); + DrawText(TextManager.GetWithVariable(tag, "[tiernumber]", tier.ToString()), font: GUIStyle.SmallFont); + } + if (showReputation) { topLeftPos += new Vector2(0.0f, typeSize.Y + repLabelSize.Y); - GUI.DrawString(spriteBatch, topLeftPos, repLabelText.Value, GUIStyle.TextColorNormal * hudVisibility * 1.5f); + DrawText(repLabelText.Value); topLeftPos += new Vector2(0.0f, repLabelSize.Y + GUI.IntScale(10)); Rectangle repBarRect = new Rectangle(new Point((int)topLeftPos.X, (int)topLeftPos.Y), new Point((int)repBarSize.X, (int)repBarSize.Y)); RoundSummary.DrawReputationBar(spriteBatch, repBarRect, HighlightedLocation.Reputation.NormalizedValue); GUI.DrawString(spriteBatch, new Vector2(repBarRect.Right + GUI.IntScale(5), repBarRect.Top), repValueText.Value, Reputation.GetReputationColor(HighlightedLocation.Reputation.NormalizedValue)); } + + void DrawText(LocalizedString text, GUIFont font = null) => GUI.DrawString(spriteBatch, topLeftPos, text, GUIStyle.TextColorNormal * hudVisibility * 1.5f, font: font); } if (drawRadiationTooltip) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 76040fcd3..061191f03 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -311,6 +311,12 @@ namespace Barotrauma.Networking CoroutineManager.StartCoroutine(WaitForStartingInfo(), "WaitForStartingInfo"); } + public void SetLobbyPublic(bool isPublic) + { + GameMain.NetLobbyScreen.SetPublic(isPublic); + SteamManager.SetLobbyPublic(isPublic); + } + private ClientPeer CreateNetPeer() { Networking.ClientPeer.Callbacks callbacks = new ClientPeer.Callbacks( @@ -1319,6 +1325,7 @@ namespace Barotrauma.Networking ServerSettings.MaximumMoneyTransferRequest = inc.ReadInt32(); bool usingShuttle = GameMain.NetLobbyScreen.UsingShuttle = inc.ReadBoolean(); GameMain.LightManager.LosMode = (LosMode)inc.ReadByte(); + ServerSettings.ShowEnemyHealthBars = (EnemyHealthBarMode)inc.ReadByte(); bool includesFinalize = inc.ReadBoolean(); inc.ReadPadBits(); GameMain.LightManager.LightingEnabled = true; @@ -2521,7 +2528,7 @@ namespace Barotrauma.Networking public override void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData = null) { - if (!(entity is IClientSerializable clientSerializable)) + if (entity is not IClientSerializable clientSerializable) { throw new InvalidCastException($"Entity is not {nameof(IClientSerializable)}"); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index b6c1b2583..c6a97de44 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -140,7 +140,7 @@ namespace Barotrauma.Networking MaxPlayers = incMsg.ReadByte(); HasPassword = incMsg.ReadBoolean(); IsPublic = incMsg.ReadBoolean(); - GameMain.NetLobbyScreen.SetPublic(IsPublic); + GameMain.Client?.SetLobbyPublic(IsPublic); AllowFileTransfers = incMsg.ReadBoolean(); incMsg.ReadPadBits(); TickRate = incMsg.ReadRangedInteger(1, 60); @@ -367,6 +367,17 @@ namespace Barotrauma.Networking //*********************************************** + //changing server visibility on the fly is not supported in dedicated servers + if (GameMain.Client?.ClientPeer is not LidgrenClientPeer) + { + var isPublic = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), serverTab.RectTransform), + TextManager.Get("publicserver")) + { + ToolTip = TextManager.Get("publicservertooltip") + }; + GetPropertyData(nameof(IsPublic)).AssignGUIComponent(isPublic); + } + // Sub Selection new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverTab.RectTransform), TextManager.Get("ServerSettingsSubSelection"), font: GUIStyle.SubHeadingFont); var selectionFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.02f), serverTab.RectTransform), isHorizontal: true) @@ -475,9 +486,10 @@ namespace Barotrauma.Networking // game settings //-------------------------------------------------------------------------------- - var roundsTab = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), settingsTabs[(int)SettingsTab.Rounds].RectTransform, Anchor.Center)) { }; + var roundsTab = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), settingsTabs[(int)SettingsTab.Rounds].RectTransform, Anchor.Center)); + var roundsContent = new GUIListBox(new RectTransform(Vector2.One, roundsTab.RectTransform, Anchor.Center), style: "GUIListBoxNoBorder").Content; - GUILayoutGroup playStyleLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.2f), roundsTab.RectTransform)); + GUILayoutGroup playStyleLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.2f), roundsContent.RectTransform)); // Play Style Selection new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), playStyleLayout.RectTransform), TextManager.Get("ServerSettingsPlayStyle"), font: GUIStyle.SubHeadingFont); var playstyleList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.7f), playStyleLayout.RectTransform)) @@ -502,7 +514,7 @@ namespace Barotrauma.Networking GUITextBlock.AutoScaleAndNormalize(playStyleTickBoxes.Select(t => t.TextBlock)); playstyleList.RectTransform.MinSize = new Point(0, (int)(playstyleList.Content.Children.First().Rect.Height * 2.0f + playstyleList.Padding.Y + playstyleList.Padding.W)); - GUILayoutGroup sliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.35f), roundsTab.RectTransform)) + GUILayoutGroup sliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.35f), roundsContent.RectTransform)) { Stretch = true }; @@ -608,7 +620,7 @@ namespace Barotrauma.Networking }; slider.OnMoved(slider, slider.BarScroll); - GUILayoutGroup losModeLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.14f), roundsTab.RectTransform)); + GUILayoutGroup losModeLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.14f), roundsContent.RectTransform)); var losModeLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), losModeLayout.RectTransform), TextManager.Get("LosEffect")); @@ -629,7 +641,30 @@ namespace Barotrauma.Networking } GetPropertyData(nameof(LosMode)).AssignGUIComponent(losModeRadioButtonGroup); - GUILayoutGroup numberLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.3f), roundsTab.RectTransform)) + GUILayoutGroup healthBarModeLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.14f), roundsContent.RectTransform)); + + var healthBarModeLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), healthBarModeLayout.RectTransform), + TextManager.Get("ShowEnemyHealthBars")); + + var healthBarModeRadioButtonLayout + = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.6f), healthBarModeLayout.RectTransform), + isHorizontal: true) + { + Stretch = true + }; + + var healthBarModeRadioButtonGroup = new GUIRadioButtonGroup(); + EnemyHealthBarMode[] healthBarModeModes = Enum.GetValues(); + for (int i = 0; i < healthBarModeModes.Length; i++) + { + var losTick = new GUITickBox(new RectTransform(new Vector2(0.3f, 1.0f), healthBarModeRadioButtonLayout.RectTransform), + TextManager.Get($"ShowEnemyHealthBars.{healthBarModeModes[i]}"), + font: GUIStyle.SmallFont, style: "GUIRadioButton"); + healthBarModeRadioButtonGroup.AddRadioButton(i, losTick); + } + GetPropertyData(nameof(ShowEnemyHealthBars)).AssignGUIComponent(healthBarModeRadioButtonGroup); + + GUILayoutGroup numberLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.3f), roundsContent.RectTransform)) { Stretch = true }; @@ -651,7 +686,7 @@ namespace Barotrauma.Networking var disableBotConversationsBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), numberLayout.RectTransform), TextManager.Get("ServerSettingsDisableBotConversations")); GetPropertyData(nameof(DisableBotConversations)).AssignGUIComponent(disableBotConversationsBox); - GUILayoutGroup buttonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), roundsTab.RectTransform), isHorizontal: true) + GUILayoutGroup buttonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), roundsContent.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.05f diff --git a/Barotrauma/BarotraumaClient/ClientSource/Program.cs b/Barotrauma/BarotraumaClient/ClientSource/Program.cs index 436132dba..3da38f7df 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Program.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Program.cs @@ -112,10 +112,23 @@ namespace Barotrauma } StringBuilder sb = new StringBuilder(); + sb.AppendLine("Barotrauma Client crash report (generated on " + DateTime.Now + ")"); - sb.AppendLine("\n"); + sb.AppendLine(); sb.AppendLine("Barotrauma seems to have crashed. Sorry for the inconvenience! "); - sb.AppendLine("\n"); + sb.AppendLine(); + + string dxgiErrorHelpText = +#if WINDOWS + GetDXGIErrorHelpText(game, exception); +#else + string.Empty; +#endif + if (!string.IsNullOrEmpty(dxgiErrorHelpText)) + { + sb.AppendLine(dxgiErrorHelpText); + sb.AppendLine(); + } try { @@ -135,7 +148,7 @@ namespace Barotrauma XDocument newDoc = new XDocument(newElement); newDoc.Save(GameSettings.PlayerConfigPath); sb.AppendLine("To prevent further startup errors, installed mods will be disabled the next time you launch the game."); - sb.AppendLine("\n"); + sb.AppendLine(); } } } @@ -148,7 +161,7 @@ namespace Barotrauma { sb.AppendLine(exeHash.StringRepresentation); } - sb.AppendLine("\n"); + sb.AppendLine(); sb.AppendLine("Game version " + GameMain.Version + " (" + AssemblyInfo.BuildString + ", branch " + AssemblyInfo.GitBranch + ", revision " + AssemblyInfo.GitRevision + ")"); sb.AppendLine($"Graphics mode: {GameSettings.CurrentConfig.Graphics.Width}x{GameSettings.CurrentConfig.Graphics.Height} ({GameSettings.CurrentConfig.Graphics.DisplayMode})"); @@ -171,7 +184,7 @@ namespace Barotrauma sb.AppendLine("Client (" + (GameMain.Client.GameStarted ? "Round had started)" : "Round hadn't been started)")); } - sb.AppendLine("\n"); + sb.AppendLine(); sb.AppendLine("System info:"); sb.AppendLine(" Operating system: " + System.Environment.OSVersion + (System.Environment.Is64BitOperatingSystem ? " 64 bit" : " x86")); @@ -201,13 +214,14 @@ namespace Barotrauma } } - sb.AppendLine("\n"); - sb.AppendLine("Exception: " + exception.Message + " (" + exception.GetType().ToString() + ")"); + sb.AppendLine(); + sb.AppendLine($"Exception: {exception.Message} ({exception.GetType()})"); #if WINDOWS if (exception is SharpDXException sharpDxException && ((uint)sharpDxException.HResult) == 0x887A0005) { var dxDevice = (SharpDX.Direct3D11.Device)game.GraphicsDevice.Handle; - sb.AppendLine("Device removed reason: " + dxDevice.DeviceRemovedReason.ToString()); + var descriptor = ResultDescriptor.Find(dxDevice.DeviceRemovedReason)?.ApiCode ?? "UNKNOWN"; + sb.AppendLine($"Device removed reason: {descriptor} ({dxDevice.DeviceRemovedReason})"); } #endif if (exception.TargetSite != null) @@ -219,7 +233,7 @@ namespace Barotrauma { sb.AppendLine("Stack trace: "); sb.AppendLine(exception.StackTrace.CleanupStackTrace()); - sb.AppendLine("\n"); + sb.AppendLine(); } if (exception.InnerException != null) @@ -260,18 +274,43 @@ namespace Barotrauma if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.SaveLogs(); } - + + string msg = string.Empty; if (GameAnalyticsManager.SendUserStatistics) { - CrashMessageBox("A crash report (\"" + filePath + "\") was saved in the root folder of the game and sent to the developers.", filePath); + msg = "A crash report (\"" + filePath + "\") was saved in the root folder of the game and sent to the developers."; } else { - CrashMessageBox("A crash report (\"" + filePath + "\") was saved in the root folder of the game. The error was not sent to the developers because user statistics have been disabled, but" + - " if you'd like to help fix this bug, you may post it on Barotrauma's GitHub issue tracker: https://github.com/Regalis11/Barotrauma/issues/", filePath); + msg = "A crash report (\"" + filePath + "\") was saved in the root folder of the game. The error was not sent to the developers because user statistics have been disabled, but" + + " if you'd like to help fix this bug, you may post it on Barotrauma's GitHub issue tracker: https://github.com/Regalis11/Barotrauma/issues/"; } + if (string.IsNullOrEmpty(dxgiErrorHelpText)) + { + msg += "\n\n" + dxgiErrorHelpText; + } + CrashMessageBox(msg, filePath); } +#if WINDOWS + private static string GetDXGIErrorHelpText(GameMain game, Exception exception) + { + string text = string.Empty; + if (exception is SharpDXException sharpDxException && ((uint)sharpDxException.HResult) == 0x887A0005) + { + var dxDevice = (SharpDX.Direct3D11.Device)game.GraphicsDevice.Handle; + var descriptor = ResultDescriptor.Find(dxDevice.DeviceRemovedReason)?.ApiCode ?? "UNKNOWN"; + + text += + $"The crash was caused by the DirectX error {descriptor} ({dxDevice.DeviceRemovedReason}). " + + "This is a common DirectX error that can be related to various different issues, such as outdated drivers, RAM problems or an overclocked or otherwise overstressed GPU. " + + "There are several potential ways to fix the issue: ensuring your graphics drivers and DirectX installation are up-to-date, disabling overclocking and adjusting various GPU-specific settings. " + + $"You may also be able to find potential solutions to the problem by using the error code {descriptor} ({dxDevice.DeviceRemovedReason}) and your GPU manufacturer as search terms."; + } + return text; + } +#endif + private static IntPtr nvApi64Dll = IntPtr.Zero; private static void EnableNvOptimus() { @@ -287,11 +326,11 @@ namespace Barotrauma private static void FreeNvOptimus() { - #warning TODO: determine if we can do this safely +#warning TODO: determine if we can do this safely //NativeLibrary.Free(nvApi64Dll); } } #endif - + } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs index 46dfef1ef..5083a49c3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs @@ -104,6 +104,7 @@ namespace Barotrauma public struct CampaignSettingElements { + public SettingValue TutorialEnabled; public SettingValue RadiationEnabled; public SettingValue MaxMissionCount; public SettingValue StartingFunds; @@ -114,6 +115,7 @@ namespace Barotrauma { return new CampaignSettings(element: null) { + TutorialEnabled = TutorialEnabled.GetValue(), RadiationEnabled = RadiationEnabled.GetValue(), MaxMissionCount = MaxMissionCount.GetValue(), StartingBalanceAmount = StartingFunds.GetValue(), @@ -159,7 +161,7 @@ namespace Barotrauma } } - protected static CampaignSettingElements CreateCampaignSettingList(GUIComponent parent, CampaignSettings prevSettings) + protected static CampaignSettingElements CreateCampaignSettingList(GUIComponent parent, CampaignSettings prevSettings, bool isSinglePlayer) { const float verticalSize = 0.14f; @@ -180,6 +182,9 @@ namespace Barotrauma Spacing = GUI.IntScale(5) }; + SettingValue tutorialEnabled = isSinglePlayer ? + CreateTickbox(settingsList.Content, TextManager.Get("CampaignOption.EnableTutorial"), TextManager.Get("campaignoption.enabletutorial.tooltip"), prevSettings.TutorialEnabled, verticalSize) : + new SettingValue(() => false, b => { }); SettingValue radiationEnabled = CreateTickbox(settingsList.Content, TextManager.Get("CampaignOption.EnableRadiation"), TextManager.Get("campaignoption.enableradiation.tooltip"), prevSettings.RadiationEnabled, verticalSize); ImmutableArray> startingSetOptions = StartItemSet.Sets.OrderBy(s => s.Order).Select(set => new SettingCarouselElement(set.Identifier, $"startitemset.{set.Identifier}")).ToImmutableArray(); @@ -214,6 +219,7 @@ namespace Barotrauma { if (o is CampaignSettings settings) { + tutorialEnabled.SetValue(isSinglePlayer && settings.TutorialEnabled); radiationEnabled.SetValue(settings.RadiationEnabled); maxMissionCountInput.SetValue(settings.MaxMissionCount); startingFundsInput.SetValue(settings.StartingBalanceAmount); @@ -226,6 +232,7 @@ namespace Barotrauma return new CampaignSettingElements { + TutorialEnabled = tutorialEnabled, RadiationEnabled = radiationEnabled, MaxMissionCount = maxMissionCountInput, StartingFunds = startingFundsInput, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs index 70a8bbd8c..66f0877d6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs @@ -46,7 +46,7 @@ namespace Barotrauma nameSeedLayout.RectTransform.MinSize = new Point(0, nameSeedLayout.Children.Sum(c => c.RectTransform.MinSize.Y)); - CampaignSettingElements elements = CreateCampaignSettingList(campaignSettingLayout, CampaignSettings.Empty); + CampaignSettingElements elements = CreateCampaignSettingList(campaignSettingLayout, CampaignSettings.Empty, false); var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), verticalLayout.RectTransform) { MaxSize = new Point(int.MaxValue, 60) }, childAnchor: Anchor.BottomRight, isHorizontal: true); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs index 5688ea044..1b8f74e3c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs @@ -370,7 +370,7 @@ namespace Barotrauma GUILayoutGroup campaignSettingContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.8f), CampaignCustomizeSettings.Content.RectTransform, Anchor.TopCenter)); - CampaignSettingElements elements = CreateCampaignSettingList(campaignSettingContent, prevSettings); + CampaignSettingElements elements = CreateCampaignSettingList(campaignSettingContent, prevSettings, true); CampaignCustomizeSettings.Buttons[0].OnClicked += (button, o) => { @@ -608,15 +608,16 @@ namespace Barotrauma { OnClicked = (btn, userdata) => { + var saveFolder = SaveUtil.GetSaveFolder(SaveUtil.SaveType.Singleplayer); try { - ToolBox.OpenFileWithShell(SaveUtil.SaveFolder); + ToolBox.OpenFileWithShell(saveFolder); } catch (Exception e) { new GUIMessageBox( - TextManager.Get("error"), - TextManager.GetWithVariables("showinfoldererror", ("[folder]", SaveUtil.SaveFolder), ("[errormessage]", e.Message))); + TextManager.Get("error"), + TextManager.GetWithVariables("showinfoldererror", ("[folder]", saveFolder), ("[errormessage]", e.Message))); } return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index 668226715..ede525285 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -39,6 +39,7 @@ namespace Barotrauma.CharacterEditor private bool ShowExtraRagdollControls => editLimbs || editJoints; + public Character SpawnedCharacter => character; private Character character; private Vector2 spawnPosition; @@ -1513,7 +1514,7 @@ namespace Barotrauma.CharacterEditor } } - private Character SpawnCharacter(Identifier speciesName, RagdollParams ragdoll = null) + public Character SpawnCharacter(Identifier speciesName, RagdollParams ragdoll = null) { DebugConsole.NewMessage(GetCharacterEditorTranslation("TryingToSpawnCharacter").Replace("[config]", speciesName.ToString()), Color.HotPink); OnPreSpawn(); @@ -3181,10 +3182,7 @@ namespace Barotrauma.CharacterEditor OnClicked = (button, data) => { ResetView(); - CharacterParams.Serialize(); - RagdollParams.Serialize(); - AnimParams.ForEach(a => a.Serialize()); - Wizard.Instance.CopyExisting(CharacterParams, RagdollParams, AnimParams); + PrepareCharacterCopy(); Wizard.Instance.SelectTab(Wizard.Tab.Character); return true; } @@ -3209,9 +3207,17 @@ namespace Barotrauma.CharacterEditor fileEditPanel.RectTransform.MinSize = new Point(0, (int)(layoutGroup.RectTransform.Children.Sum(c => c.MinSize.Y + layoutGroup.AbsoluteSpacing) * 1.2f)); } -#endregion + #endregion -#region ToggleButtons + public void PrepareCharacterCopy() + { + CharacterParams.Serialize(); + RagdollParams.Serialize(); + AnimParams.ForEach(a => a.Serialize()); + Wizard.Instance.CopyExisting(CharacterParams, RagdollParams, AnimParams); + } + + #region ToggleButtons private enum Direction { Left, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs index 642648f1b..64a6b65c9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs @@ -101,7 +101,7 @@ namespace Barotrauma.CharacterEditor { bool isSamePackage = contentPackage.GetFiles().Any(f => Path.GetFileNameWithoutExtension(f.Path.Value) == name); LocalizedString verificationText = isSamePackage ? GetCharacterEditorTranslation("existingcharacterfoundreplaceverification") : GetCharacterEditorTranslation("existingcharacterfoundoverrideverification"); - var msgBox = new GUIMessageBox("", verificationText, new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }) + var msgBox = new GUIMessageBox("", verificationText, new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }, type: GUIMessageBox.Type.Warning) { UserData = "verificationprompt" }; @@ -356,7 +356,7 @@ namespace Barotrauma.CharacterEditor } if (ContentPackageManager.AllPackages.Any(cp => cp.Name.ToLower() == contentPackageNameElement.Text.ToLower())) { - new GUIMessageBox("", TextManager.Get("charactereditor.contentpackagenameinuse", "leveleditorlevelobjnametaken")); + new GUIMessageBox("", TextManager.Get("charactereditor.contentpackagenameinuse", "leveleditorlevelobjnametaken"), type: GUIMessageBox.Type.Warning); return false; } string modName = contentPackageNameElement.Text; @@ -428,17 +428,26 @@ namespace Barotrauma.CharacterEditor texturePathElement.Flash(useRectangleFlash: true); return false; } + if (Name == CharacterPrefab.HumanSpeciesName && !IsCopy) + { + // Force a copy when trying to override a human, because handling the crash would be very difficult (we require humans to have certain definitions). + if (!CharacterEditorScreen.Instance.SpawnedCharacter.IsHuman) + { + CharacterEditorScreen.Instance.SpawnCharacter(CharacterPrefab.HumanSpeciesName); + } + CharacterEditorScreen.Instance.PrepareCharacterCopy(); + } if (IsCopy) { SourceRagdoll.Texture = evaluatedTexturePath; SourceRagdoll.CanEnterSubmarine = CanEnterSubmarine; SourceRagdoll.CanWalk = CanWalk; SourceRagdoll.Serialize(); - Wizard.Instance.CreateCharacter(SourceRagdoll.MainElement, SourceCharacter.MainElement, SourceAnimations); + Instance.CreateCharacter(SourceRagdoll.MainElement, SourceCharacter.MainElement, SourceAnimations); } else { - Wizard.Instance.SelectTab(Tab.Ragdoll); + Instance.SelectTab(Tab.Ragdoll); } return true; }; @@ -470,9 +479,6 @@ namespace Barotrauma.CharacterEditor Stretch = true, RelativeSpacing = 0.02f }; - // HTML - GUIMessageBox htmlBox = null; - var loadHtmlButton = new GUIButton(new RectTransform(new Point(content.Rect.Width / 3, elementSize), content.RectTransform), GetCharacterEditorTranslation("LoadFromHTML")); // Limbs var limbsElement = new GUIFrame(new RectTransform(new Vector2(1, 0.05f), content.RectTransform), style: null) { CanBeFocused = false }; @@ -689,69 +695,6 @@ namespace Barotrauma.CharacterEditor return true; } }; - loadHtmlButton.OnClicked = (b, d) => - { - if (htmlBox == null) - { - htmlBox = new GUIMessageBox(GetCharacterEditorTranslation("LoadHTML"), string.Empty, new LocalizedString[] { TextManager.Get("Close"), TextManager.Get("Load") }, new Vector2(0.65f, 1f)); - htmlBox.Header.Font = GUIStyle.LargeFont; - var element = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.05f), htmlBox.Content.RectTransform), style: null, color: Color.Gray * 0.25f); - //new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform), GetCharacterEditorTranslation("HTMLPath")); - var htmlPathElement = new GUITextBox(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.TopRight), GetCharacterEditorTranslation("HTMLPath").Value); - LocalizedString title = GetCharacterEditorTranslation("SelectFile"); - new GUIButton(new RectTransform(new Vector2(0.3f, 1), element.RectTransform), title) - { - OnClicked = (button, data) => - { - FileSelection.OnFileSelected = (file) => - { - htmlPathElement.Text = file; - }; - FileSelection.ClearFileTypeFilters(); - FileSelection.AddFileTypeFilter("HTML", "*.html, *.htm"); - FileSelection.AddFileTypeFilter("All files", "*.*"); - FileSelection.SelectFileTypeFilter("*.html, *.htm"); - FileSelection.Open = true; - return true; - } - }; - var list = new GUIListBox(new RectTransform(new Vector2(1, 0.8f), htmlBox.Content.RectTransform)); - var htmlOutput = new GUITextBlock(new RectTransform(Vector2.One, list.Content.RectTransform), string.Empty) { CanBeFocused = false }; - htmlBox.Buttons[0].OnClicked += (_b, _d) => - { - htmlBox.Close(); - return true; - }; - htmlBox.Buttons[1].OnClicked += (_b, _d) => - { - LimbGUIElements.ForEach(l => l.RectTransform.Parent = null); - LimbGUIElements.Clear(); - JointGUIElements.ForEach(j => j.RectTransform.Parent = null); - JointGUIElements.Clear(); - LimbXElements.Clear(); - JointXElements.Clear(); - ParseRagdollFromHTML(htmlPathElement.Text, (id, limbName, limbType, rect) => - { - CreateLimbGUIElement(limbsList.Content.RectTransform, elementSize, id, limbName, limbType, rect); - }, (id1, id2, anchor1, anchor2, jointName) => - { - CreateJointGUIElement(jointsList.Content.RectTransform, elementSize, id1, id2, anchor1, anchor2, jointName); - }); - htmlOutput.Text = new XDocument(new XElement("Ragdoll", new object[] - { - new XAttribute("type", Name), LimbXElements.Values, JointXElements - })).ToString(); - htmlOutput.CalculateHeightFromText(); - list.UpdateScrollBarSize(); - return true; - }; - } - else - { - GUIMessageBox.MessageBoxes.Add(htmlBox); - } - return true; - }; // Previous box.Buttons[0].OnClicked += (b, d) => { @@ -1070,7 +1013,6 @@ namespace Barotrauma.CharacterEditor // Rectangles colliderAttributes.Add(new XAttribute("height", (int)(height * 0.85f))); colliderAttributes.Add(new XAttribute("width", (int)(width * 0.85f))); - idToCodeName.TryGetValue(id, out string notes); LimbXElements.Add(id.ToString(), new XElement("limb", new XAttribute("id", id), new XAttribute("name", limbName), @@ -1107,188 +1049,6 @@ namespace Barotrauma.CharacterEditor } } - Dictionary idToCodeName = new Dictionary(); - protected void ParseRagdollFromHTML(string path, Action limbCallback = null, Action jointCallback = null) - { - // TODO: parse as xml files -> allows to load ragdolls onto the wizard. - //XDocument doc = XMLExtensions.TryLoadXml(path); - //var xElements = doc.Elements().ToArray(); - string html = string.Empty; - try - { - html = File.ReadAllText(path); - } - catch (Exception e) - { - DebugConsole.ThrowError(GetCharacterEditorTranslation("FailedToReadHTML").Replace("[path]", path), e); - return; - } - - var lines = html.Split(new string[] { "", Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries) - .Where(s => s.Contains("left") && s.Contains("top") && s.Contains("width") && s.Contains("height")); - int id = 0; - Dictionary hierarchyToID = new Dictionary(); - Dictionary idToHierarchy = new Dictionary(); - Dictionary idToPositionCode = new Dictionary(); - Dictionary idToName = new Dictionary(); - idToCodeName.Clear(); - foreach (var line in lines) - { - var codeNames = new string(line.SkipWhile(c => c != '>').Skip(1).ToArray()).Split(','); - for (int i = 0; i < codeNames.Length; i++) - { - string codeName = codeNames[i].Trim(); - if (string.IsNullOrWhiteSpace(codeName)) { continue; } - idToCodeName.Add(id, codeName); - string limbName = new string(codeName.SkipWhile(c => c != '_').Skip(1).ToArray()); - if (string.IsNullOrWhiteSpace(limbName)) { continue; } - idToName.Add(id, limbName); - var parts = line.Split(' '); - int ParseToInt(string selector) - { - string part = parts.First(p => p.Contains(selector)); - string s = new string(part.SkipWhile(c => c != ':').Skip(1).TakeWhile(c => char.IsNumber(c)).ToArray()); - int.TryParse(s, out int v); - return v; - }; - // example: 111311cr -> 111311 - string hierarchy = new string(codeName.TakeWhile(c => char.IsNumber(c)).ToArray()); - if (hierarchyToID.ContainsKey(hierarchy)) - { - DebugConsole.ThrowError(GetCharacterEditorTranslation("MultipleItemsWithSameHierarchy").Replace("[hierarchy]", hierarchy).Replace("[name]", codeName)); - return; - } - hierarchyToID.Add(hierarchy, id); - idToHierarchy.Add(id, hierarchy); - string positionCode = new string(codeName.SkipWhile(c => char.IsNumber(c)).TakeWhile(c => c != '_').ToArray()); - idToPositionCode.Add(id, positionCode.ToLowerInvariant()); - int x = ParseToInt("left"); - int y = ParseToInt("top"); - int width = ParseToInt("width"); - int height = ParseToInt("height"); - // This is overridden when the data is loaded from the gui fields. - LimbXElements.Add(hierarchy, new XElement("limb", - new XAttribute("id", id), - new XAttribute("name", limbName), - new XAttribute("type", ParseLimbType(limbName).ToString()), - new XElement("sprite", - new XAttribute("texture", ""), - new XAttribute("sourcerect", $"{x}, {y}, {width}, {height}")) - )); - limbCallback?.Invoke(id, limbName, ParseLimbType(limbName), new Rectangle(x, y, width, height)); - id++; - } - } - for (int i = 0; i < id; i++) - { - if (idToHierarchy.TryGetValue(i, out string hierarchy)) - { - if (hierarchy != "0") - { - // NEW LOGIC: if hierarchy length == 1, parent to 0 - // Else parent to the last bone in the current hierarchy (11 is parented to 1, 212 is parented to 21 etc) - string parent = hierarchy.Length > 1 ? hierarchy.Remove(hierarchy.Length - 1, 1) : "0"; - if (hierarchyToID.TryGetValue(parent, out int parentID)) - { - Vector2 anchor1 = Vector2.Zero; - Vector2 anchor2 = Vector2.Zero; - idToName.TryGetValue(parentID, out string parentName); - idToName.TryGetValue(i, out string limbName); - string jointName = $"{GetCharacterEditorTranslation("Joint")} {parentName} - {limbName}"; - if (idToPositionCode.TryGetValue(i, out string positionCode)) - { - float scalar = 0.8f; - if (LimbXElements.TryGetValue(parent, out XElement parentElement)) - { - Rectangle parentSourceRect = parentElement.Element("sprite").GetAttributeRect("sourcerect", Rectangle.Empty); - float parentWidth = parentSourceRect.Width / 2 * scalar; - float parentHeight = parentSourceRect.Height / 2 * scalar; - switch (positionCode) - { - case "tl": // -1, 1 - anchor1 = new Vector2(-parentWidth, parentHeight); - break; - case "tc": // 0, 1 - anchor1 = new Vector2(0, parentHeight); - break; - case "tr": // -1, 1 - anchor1 = new Vector2(-parentWidth, parentHeight); - break; - case "cl": // -1, 0 - anchor1 = new Vector2(-parentWidth, 0); - break; - case "cr": // 1, 0 - anchor1 = new Vector2(parentWidth, 0); - break; - case "bl": // -1, -1 - anchor1 = new Vector2(-parentWidth, -parentHeight); - break; - case "bc": // 0, -1 - anchor1 = new Vector2(0, -parentHeight); - break; - case "br": // 1, -1 - anchor1 = new Vector2(parentWidth, -parentHeight); - break; - } - if (LimbXElements.TryGetValue(hierarchy, out XElement element)) - { - Rectangle sourceRect = element.Element("sprite").GetAttributeRect("sourcerect", Rectangle.Empty); - float width = sourceRect.Width / 2 * scalar; - float height = sourceRect.Height / 2 * scalar; - switch (positionCode) - { - // Inverse - case "tl": - // br - anchor2 = new Vector2(-width, -height); - break; - case "tc": - // bc - anchor2 = new Vector2(0, -height); - break; - case "tr": - // bl - anchor2 = new Vector2(-width, -height); - break; - case "cl": - // cr - anchor2 = new Vector2(width, 0); - break; - case "cr": - // cl - anchor2 = new Vector2(-width, 0); - break; - case "bl": - // tr - anchor2 = new Vector2(-width, height); - break; - case "bc": - // tc - anchor2 = new Vector2(0, height); - break; - case "br": - // tl - anchor2 = new Vector2(-width, height); - break; - } - } - } - } - // This is overridden when the data is loaded from the gui fields. - JointXElements.Add(new XElement("joint", - new XAttribute("name", jointName), - new XAttribute("limb1", parentID), - new XAttribute("limb2", i), - new XAttribute("limb1anchor", $"{anchor1.X.Format(2)}, {anchor1.Y.Format(2)}"), - new XAttribute("limb2anchor", $"{anchor2.X.Format(2)}, {anchor2.Y.Format(2)}") - )); - jointCallback?.Invoke(parentID, i, anchor1, anchor2, jointName); - } - } - } - } - } - protected LimbType ParseLimbType(string limbName) { var limbType = LimbType.None; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index e89550ec1..0ae5127d6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -269,7 +269,7 @@ namespace Barotrauma }; #if USE_STEAM - steamWorkshopButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), customizeList.RectTransform), TextManager.Get("SteamWorkshopButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") + steamWorkshopButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), customizeList.RectTransform), TextManager.Get("settingstab.mods"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { ForceUpperCase = ForceUpperCase.Yes, Enabled = true, @@ -463,13 +463,17 @@ namespace Barotrauma } }; var tutorialPreview = new GUILayoutGroup(new RectTransform(new Vector2(0.6f, 1.0f), tutorialContent.RectTransform)) { RelativeSpacing = 0.05f, Stretch = true }; - var imageContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.6f), tutorialPreview.RectTransform), style: "InnerFrame"); + var imageContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), tutorialPreview.RectTransform), style: "InnerFrame"); tutorialBanner = new GUIImage(new RectTransform(Vector2.One, imageContainer.RectTransform), style: null, scaleToFit: true); - var infoContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.4f), tutorialPreview.RectTransform), style: "GUIFrameListBox"); - var infoContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), infoContainer.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter); + var infoContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), tutorialPreview.RectTransform), style: "GUIFrameListBox"); + var infoContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), infoContainer.RectTransform, Anchor.Center), childAnchor: Anchor.TopLeft) + { + AbsoluteSpacing = GUI.IntScale(10) + }; - tutorialHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.75f), infoContent.RectTransform), string.Empty, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center); + tutorialHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), string.Empty, font: GUIStyle.SubHeadingFont); + tutorialDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), string.Empty, wrap: true); var startButton = new GUIButton(new RectTransform(new Vector2(0.5f, 0.0f), infoContent.RectTransform, Anchor.BottomRight), text: TextManager.Get("startgamebutton")) { @@ -500,6 +504,10 @@ namespace Barotrauma private void SelectTutorial(Tutorial tutorial) { tutorialHeader.Text = tutorial.DisplayName; + tutorialHeader.CalculateHeightFromText(); + tutorialDescription.Text = tutorial.Description; + tutorialDescription.CalculateHeightFromText(); + (tutorialDescription.Parent as GUILayoutGroup)?.Recalculate(); tutorial.TutorialPrefab.Banner?.EnsureLazyLoaded(); tutorialBanner.Sprite = tutorial.TutorialPrefab.Banner; tutorialBanner.Color = tutorial.TutorialPrefab.Banner == null ? Color.Black : Color.White; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index aa6c37369..c8366b171 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -2487,7 +2487,7 @@ namespace Barotrauma { IntValue = MainSub.Info.Tier, MinValueInt = 1, - MaxValueInt = 3, + MaxValueInt = SubmarineInfo.HighestTier, OnValueChanged = (numberInput) => { MainSub.Info.Tier = numberInput.IntValue; @@ -2495,7 +2495,7 @@ namespace Barotrauma }; if (MainSub?.Info != null) { - MainSub.Info.Tier = Math.Clamp(MainSub.Info.Tier, 1, 3); + MainSub.Info.Tier = Math.Clamp(MainSub.Info.Tier, 1, SubmarineInfo.HighestTier); } var crewSizeArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true) @@ -3228,7 +3228,7 @@ namespace Barotrauma } string pathWithoutUserName = Path.GetFullPath(sub.FilePath); - string saveFolder = Path.GetFullPath(SaveUtil.SaveFolder); + string saveFolder = Path.GetFullPath(SaveUtil.DefaultSaveFolder); if (pathWithoutUserName.StartsWith(saveFolder)) { pathWithoutUserName = "..." + pathWithoutUserName[saveFolder.Length..]; @@ -4217,7 +4217,8 @@ namespace Barotrauma GUIListBox listBox = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.9f), frame.RectTransform, Anchor.Center)) { PlaySoundOnSelect = true, - OnSelected = SelectWire + OnSelected = SelectWire, + CanTakeKeyBoardFocus = false }; List wirePrefabs = new List(); @@ -5866,7 +5867,7 @@ namespace Barotrauma decimal realWorldDistance = decimal.Round((decimal) (Vector2.Distance(startPos, mouseWorldPos) * Physics.DisplayToRealWorldRatio), 2); Vector2 offset = new Vector2(GUI.IntScale(24)); - GUI.DrawString(spriteBatch, PlayerInput.MousePosition + offset, $"{realWorldDistance}m", GUIStyle.TextColorNormal, font: GUIStyle.SubHeadingFont, backgroundColor: Color.Black, backgroundPadding: 4); + GUI.DrawString(spriteBatch, PlayerInput.MousePosition + offset, $"{realWorldDistance} m", GUIStyle.TextColorNormal, font: GUIStyle.Font, backgroundColor: Color.Black, backgroundPadding: 4); } spriteBatch.End(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs index 65a6e87bc..594e36ad4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs @@ -176,7 +176,7 @@ namespace Barotrauma Action setter) where T : Enum => Dropdown(parent, textFunc, tooltipFunc, (T[])Enum.GetValues(typeof(T)), currentValue, setter); - private static void Dropdown(GUILayoutGroup parent, Func textFunc, Func? tooltipFunc, IReadOnlyList values, T currentValue, Action setter) + private static GUIDropDown Dropdown(GUILayoutGroup parent, Func textFunc, Func? tooltipFunc, IReadOnlyList values, T currentValue, Action setter) { var dropdown = new GUIDropDown(NewItemRectT(parent)); values.ForEach(v => dropdown.AddItem(text: textFunc(v), userData: v, toolTip: tooltipFunc?.Invoke(v) ?? null)); @@ -189,9 +189,10 @@ namespace Barotrauma setter((T)obj); return true; }; + return dropdown; } - private void Slider(GUILayoutGroup parent, Vector2 range, int steps, Func labelFunc, float currentValue, Action setter, LocalizedString? tooltip = null) + private static (GUIScrollBar slider, GUITextBlock label) Slider(GUILayoutGroup parent, Vector2 range, int steps, Func labelFunc, float currentValue, Action setter, LocalizedString? tooltip = null) { var layout = new GUILayoutGroup(NewItemRectT(parent), isHorizontal: true); var slider = new GUIScrollBar(new RectTransform((0.72f, 1.0f), layout.RectTransform), style: "GUISlider") @@ -213,11 +214,12 @@ namespace Barotrauma setter(sb.BarScrollValue); return true; }; + return (slider, label); } - private void Tickbox(GUILayoutGroup parent, LocalizedString label, LocalizedString tooltip, bool currentValue, Action setter) + private static GUITickBox Tickbox(GUILayoutGroup parent, LocalizedString label, LocalizedString tooltip, bool currentValue, Action setter) { - var tickbox = new GUITickBox(NewItemRectT(parent), label) + return new GUITickBox(NewItemRectT(parent), label) { Selected = currentValue, ToolTip = tooltip, @@ -231,7 +233,7 @@ namespace Barotrauma private string Percentage(float v) => ToolBox.GetFormattedPercentage(v); - private int Round(float v) => (int)MathF.Round(v); + private static int Round(float v) => MathUtils.RoundToInt(v); private void CreateGraphicsTab() { @@ -262,30 +264,30 @@ namespace Barotrauma Spacer(left); Label(left, TextManager.Get("DisplayMode"), GUIStyle.SubHeadingFont); - DropdownEnum(left, (m) => TextManager.Get($"{m}"), null, unsavedConfig.Graphics.DisplayMode, (v) => unsavedConfig.Graphics.DisplayMode = v); + DropdownEnum(left, (m) => TextManager.Get($"{m}"), null, unsavedConfig.Graphics.DisplayMode, v => unsavedConfig.Graphics.DisplayMode = v); Spacer(left); - Tickbox(left, TextManager.Get("EnableVSync"), TextManager.Get("EnableVSyncTooltip"), unsavedConfig.Graphics.VSync, (v) => unsavedConfig.Graphics.VSync = v); - Tickbox(left, TextManager.Get("EnableTextureCompression"), TextManager.Get("EnableTextureCompressionTooltip"), unsavedConfig.Graphics.CompressTextures, (v) => unsavedConfig.Graphics.CompressTextures = v); + Tickbox(left, TextManager.Get("EnableVSync"), TextManager.Get("EnableVSyncTooltip"), unsavedConfig.Graphics.VSync, v => unsavedConfig.Graphics.VSync = v); + Tickbox(left, TextManager.Get("EnableTextureCompression"), TextManager.Get("EnableTextureCompressionTooltip"), unsavedConfig.Graphics.CompressTextures, v => unsavedConfig.Graphics.CompressTextures = v); Label(right, TextManager.Get("LOSEffect"), GUIStyle.SubHeadingFont); - DropdownEnum(right, (m) => TextManager.Get($"LosMode{m}"), null, unsavedConfig.Graphics.LosMode, (v) => unsavedConfig.Graphics.LosMode = v); + DropdownEnum(right, (m) => TextManager.Get($"LosMode{m}"), null, unsavedConfig.Graphics.LosMode, v => unsavedConfig.Graphics.LosMode = v); Spacer(right); Label(right, TextManager.Get("LightMapScale"), GUIStyle.SubHeadingFont); - Slider(right, (0.5f, 1.0f), 11, (v) => TextManager.GetWithVariable("percentageformat", "[value]", Round(v * 100).ToString()).Value, unsavedConfig.Graphics.LightMapScale, (v) => unsavedConfig.Graphics.LightMapScale = v, TextManager.Get("LightMapScaleTooltip")); + Slider(right, (0.5f, 1.0f), 11, v => TextManager.GetWithVariable("percentageformat", "[value]", Round(v * 100).ToString()).Value, unsavedConfig.Graphics.LightMapScale, v => unsavedConfig.Graphics.LightMapScale = v, TextManager.Get("LightMapScaleTooltip")); Spacer(right); Label(right, TextManager.Get("VisibleLightLimit"), GUIStyle.SubHeadingFont); - Slider(right, (10, 210), 21, (v) => v > 200 ? TextManager.Get("unlimited").Value : Round(v).ToString(), unsavedConfig.Graphics.VisibleLightLimit, - (v) => unsavedConfig.Graphics.VisibleLightLimit = v > 200 ? int.MaxValue : Round(v), TextManager.Get("VisibleLightLimitTooltip")); + Slider(right, (10, 210), 21, v => v > 200 ? TextManager.Get("unlimited").Value : Round(v).ToString(), unsavedConfig.Graphics.VisibleLightLimit, + v => unsavedConfig.Graphics.VisibleLightLimit = v > 200 ? int.MaxValue : Round(v), TextManager.Get("VisibleLightLimitTooltip")); Spacer(right); - Tickbox(right, TextManager.Get("RadialDistortion"), TextManager.Get("RadialDistortionTooltip"), unsavedConfig.Graphics.RadialDistortion, (v) => unsavedConfig.Graphics.RadialDistortion = v); - Tickbox(right, TextManager.Get("ChromaticAberration"), TextManager.Get("ChromaticAberrationTooltip"), unsavedConfig.Graphics.ChromaticAberration, (v) => unsavedConfig.Graphics.ChromaticAberration = v); + Tickbox(right, TextManager.Get("RadialDistortion"), TextManager.Get("RadialDistortionTooltip"), unsavedConfig.Graphics.RadialDistortion, v => unsavedConfig.Graphics.RadialDistortion = v); + Tickbox(right, TextManager.Get("ChromaticAberration"), TextManager.Get("ChromaticAberrationTooltip"), unsavedConfig.Graphics.ChromaticAberration, v => unsavedConfig.Graphics.ChromaticAberration = v); Label(right, TextManager.Get("ParticleLimit"), GUIStyle.SubHeadingFont); - Slider(right, (100, 1500), 15, (v) => Round(v).ToString(), unsavedConfig.Graphics.ParticleLimit, (v) => unsavedConfig.Graphics.ParticleLimit = Round(v)); + Slider(right, (100, 1500), 15, v => Round(v).ToString(), unsavedConfig.Graphics.ParticleLimit, v => unsavedConfig.Graphics.ParticleLimit = Round(v)); Spacer(right); } @@ -399,23 +401,23 @@ namespace Barotrauma Spacer(audio); Label(audio, TextManager.Get("SoundVolume"), GUIStyle.SubHeadingFont); - Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.SoundVolume, (v) => unsavedConfig.Audio.SoundVolume = v); + Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.SoundVolume, v => unsavedConfig.Audio.SoundVolume = v); Label(audio, TextManager.Get("MusicVolume"), GUIStyle.SubHeadingFont); - Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.MusicVolume, (v) => unsavedConfig.Audio.MusicVolume = v); + Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.MusicVolume, v => unsavedConfig.Audio.MusicVolume = v); Label(audio, TextManager.Get("UiSoundVolume"), GUIStyle.SubHeadingFont); - Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.UiVolume, (v) => unsavedConfig.Audio.UiVolume = v); + Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.UiVolume, v => unsavedConfig.Audio.UiVolume = v); - Tickbox(audio, TextManager.Get("MuteOnFocusLost"), TextManager.Get("MuteOnFocusLostTooltip"), unsavedConfig.Audio.MuteOnFocusLost, (v) => unsavedConfig.Audio.MuteOnFocusLost = v); - Tickbox(audio, TextManager.Get("DynamicRangeCompression"), TextManager.Get("DynamicRangeCompressionTooltip"), unsavedConfig.Audio.DynamicRangeCompressionEnabled, (v) => unsavedConfig.Audio.DynamicRangeCompressionEnabled = v); + Tickbox(audio, TextManager.Get("MuteOnFocusLost"), TextManager.Get("MuteOnFocusLostTooltip"), unsavedConfig.Audio.MuteOnFocusLost, v => unsavedConfig.Audio.MuteOnFocusLost = v); + Tickbox(audio, TextManager.Get("DynamicRangeCompression"), TextManager.Get("DynamicRangeCompressionTooltip"), unsavedConfig.Audio.DynamicRangeCompressionEnabled, v => unsavedConfig.Audio.DynamicRangeCompressionEnabled = v); Spacer(audio); Label(audio, TextManager.Get("VoiceChatVolume"), GUIStyle.SubHeadingFont); - Slider(audio, (0, 2), 201, Percentage, unsavedConfig.Audio.VoiceChatVolume, (v) => unsavedConfig.Audio.VoiceChatVolume = v); + Slider(audio, (0, 2), 201, Percentage, unsavedConfig.Audio.VoiceChatVolume, v => unsavedConfig.Audio.VoiceChatVolume = v); - Tickbox(audio, TextManager.Get("DirectionalVoiceChat"), TextManager.Get("DirectionalVoiceChatTooltip"), unsavedConfig.Audio.UseDirectionalVoiceChat, (v) => unsavedConfig.Audio.UseDirectionalVoiceChat = v); - Tickbox(audio, TextManager.Get("VoipAttenuation"), TextManager.Get("VoipAttenuationTooltip"), unsavedConfig.Audio.VoipAttenuationEnabled, (v) => unsavedConfig.Audio.VoipAttenuationEnabled = v); + Tickbox(audio, TextManager.Get("DirectionalVoiceChat"), TextManager.Get("DirectionalVoiceChatTooltip"), unsavedConfig.Audio.UseDirectionalVoiceChat, v => unsavedConfig.Audio.UseDirectionalVoiceChat = v); + Tickbox(audio, TextManager.Get("VoipAttenuation"), TextManager.Get("VoipAttenuationTooltip"), unsavedConfig.Audio.VoipAttenuationEnabled, v => unsavedConfig.Audio.VoipAttenuationEnabled = v); Label(voiceChat, TextManager.Get("AudioInputDevice"), GUIStyle.SubHeadingFont); @@ -424,7 +426,7 @@ namespace Barotrauma Spacer(voiceChat); Label(voiceChat, TextManager.Get("VCInputMode"), GUIStyle.SubHeadingFont); - DropdownEnum(voiceChat, (v) => TextManager.Get($"VoiceMode.{v}"), (v) => TextManager.Get($"VoiceMode.{v}Tooltip"), unsavedConfig.Audio.VoiceSetting, (v) => unsavedConfig.Audio.VoiceSetting = v); + DropdownEnum(voiceChat, v => TextManager.Get($"VoiceMode.{v}"), v => TextManager.Get($"VoiceMode.{v}Tooltip"), unsavedConfig.Audio.VoiceSetting, v => unsavedConfig.Audio.VoiceSetting = v); Spacer(voiceChat); var noiseGateThresholdLabel = Label(voiceChat, TextManager.Get("NoiseGateThreshold"), GUIStyle.SubHeadingFont); @@ -464,11 +466,11 @@ namespace Barotrauma Spacer(voiceChat); Label(voiceChat, TextManager.Get("MicrophoneVolume"), GUIStyle.SubHeadingFont); - Slider(voiceChat, (0, 10), 101, Percentage, unsavedConfig.Audio.MicrophoneVolume, (v) => unsavedConfig.Audio.MicrophoneVolume = v); + Slider(voiceChat, (0, 10), 101, Percentage, unsavedConfig.Audio.MicrophoneVolume, v => unsavedConfig.Audio.MicrophoneVolume = v); Spacer(voiceChat); Label(voiceChat, TextManager.Get("CutoffPrevention"), GUIStyle.SubHeadingFont); - Slider(voiceChat, (0, 500), 26, (v) => $"{Round(v)} ms", unsavedConfig.Audio.VoiceChatCutoffPrevention, (v) => unsavedConfig.Audio.VoiceChatCutoffPrevention = Round(v), TextManager.Get("CutoffPreventionTooltip")); + Slider(voiceChat, (0, 500), 26, v => $"{Round(v)} ms", unsavedConfig.Audio.VoiceChatCutoffPrevention, v => unsavedConfig.Audio.VoiceChatCutoffPrevention = Round(v), TextManager.Get("CutoffPreventionTooltip")); } private readonly Dictionary> inputButtonValueNameGetters = new Dictionary>(); @@ -481,8 +483,10 @@ namespace Barotrauma GUILayoutGroup layout = CreateCenterLayout(content); Label(layout, TextManager.Get("AimAssist"), GUIStyle.SubHeadingFont); - Slider(layout, (0, 1), 101, Percentage, unsavedConfig.AimAssistAmount, (v) => unsavedConfig.AimAssistAmount = v, TextManager.Get("AimAssistTooltip")); - Tickbox(layout, TextManager.Get("EnableMouseLook"), TextManager.Get("EnableMouseLookTooltip"), unsavedConfig.EnableMouseLook, (v) => unsavedConfig.EnableMouseLook = v); + + var aimAssistSlider = Slider(layout, (0, 1), 101, Percentage, unsavedConfig.AimAssistAmount, v => unsavedConfig.AimAssistAmount = v, TextManager.Get("AimAssistTooltip")); + Tickbox(layout, TextManager.Get("EnableMouseLook"), TextManager.Get("EnableMouseLookTooltip"), unsavedConfig.EnableMouseLook, v => unsavedConfig.EnableMouseLook = v); + Spacer(layout); GUIListBox keyMapList = @@ -523,7 +527,7 @@ namespace Barotrauma if (willBeSelected) { inputBoxSelectedThisFrame = true; - currentSetter = (v) => + currentSetter = v => { valueSetter(v); btn.Text = valueNameGetter(); @@ -626,7 +630,7 @@ namespace Barotrauma currRow, TextManager.Get($"InputType.{input}"), () => unsavedConfig.KeyMap.Bindings[input].Name, - (v) => unsavedConfig.KeyMap = unsavedConfig.KeyMap.WithBinding(input, v), + v => unsavedConfig.KeyMap = unsavedConfig.KeyMap.WithBinding(input, v), LegacyInputTypes.Contains(input)); } } @@ -644,7 +648,7 @@ namespace Barotrauma currRow, TextManager.GetWithVariable("inventoryslotkeybind", "[slotnumber]", (currIndex + 1).ToString(CultureInfo.InvariantCulture)), () => unsavedConfig.InventoryKeyMap.Bindings[currIndex].Name, - (v) => unsavedConfig.InventoryKeyMap = unsavedConfig.InventoryKeyMap.WithBinding(currIndex, v)); + v => unsavedConfig.InventoryKeyMap = unsavedConfig.InventoryKeyMap.WithBinding(currIndex, v)); } } @@ -663,6 +667,8 @@ namespace Barotrauma { unsavedConfig.InventoryKeyMap = GameSettings.Config.InventoryKeyMapping.GetDefault(); unsavedConfig.KeyMap = GameSettings.Config.KeyMapping.GetDefault(); + aimAssistSlider.slider.BarScrollValue = GameSettings.Config.DefaultAimAssist; + aimAssistSlider.label.Text = Percentage(GameSettings.Config.DefaultAimAssist); foreach (var btn in inputButtonValueNameGetters.Keys) { btn.Text = inputButtonValueNameGetters[btn](); @@ -683,13 +689,13 @@ namespace Barotrauma .OrderBy(l => TextManager.GetTranslatedLanguageName(l).ToIdentifier()) .ToArray(); Label(layout, TextManager.Get("Language"), GUIStyle.SubHeadingFont); - Dropdown(layout, (v) => TextManager.GetTranslatedLanguageName(v), null, languages, unsavedConfig.Language, (v) => unsavedConfig.Language = v); + Dropdown(layout, v => TextManager.GetTranslatedLanguageName(v), null, languages, unsavedConfig.Language, v => unsavedConfig.Language = v); Spacer(layout); - Tickbox(layout, TextManager.Get("PauseOnFocusLost"), TextManager.Get("PauseOnFocusLostTooltip"), unsavedConfig.PauseOnFocusLost, (v) => unsavedConfig.PauseOnFocusLost = v); + Tickbox(layout, TextManager.Get("PauseOnFocusLost"), TextManager.Get("PauseOnFocusLostTooltip"), unsavedConfig.PauseOnFocusLost, v => unsavedConfig.PauseOnFocusLost = v); Spacer(layout); - Tickbox(layout, TextManager.Get("DisableInGameHints"), TextManager.Get("DisableInGameHintsTooltip"), unsavedConfig.DisableInGameHints, (v) => unsavedConfig.DisableInGameHints = v); + Tickbox(layout, TextManager.Get("DisableInGameHints"), TextManager.Get("DisableInGameHintsTooltip"), unsavedConfig.DisableInGameHints, v => unsavedConfig.DisableInGameHints = v); var resetInGameHintsButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), layout.RectTransform), TextManager.Get("ResetInGameHints"), style: "GUIButtonSmall") @@ -710,13 +716,17 @@ namespace Barotrauma } }; Spacer(layout); - + + Label(layout, TextManager.Get("ShowEnemyHealthBars"), GUIStyle.SubHeadingFont); + DropdownEnum(layout, v => TextManager.Get($"ShowEnemyHealthBars.{v}"), null, unsavedConfig.ShowEnemyHealthBars, v => unsavedConfig.ShowEnemyHealthBars = v); + Spacer(layout); + Label(layout, TextManager.Get("HUDScale"), GUIStyle.SubHeadingFont); - Slider(layout, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.HUDScale, (v) => unsavedConfig.Graphics.HUDScale = v); + Slider(layout, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.HUDScale, v => unsavedConfig.Graphics.HUDScale = v); Label(layout, TextManager.Get("InventoryScale"), GUIStyle.SubHeadingFont); - Slider(layout, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.InventoryScale, (v) => unsavedConfig.Graphics.InventoryScale = v); + Slider(layout, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.InventoryScale, v => unsavedConfig.Graphics.InventoryScale = v); Label(layout, TextManager.Get("TextScale"), GUIStyle.SubHeadingFont); - Slider(layout, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.TextScale, (v) => unsavedConfig.Graphics.TextScale = v); + Slider(layout, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.TextScale, v => unsavedConfig.Graphics.TextScale = v); #if !OSX Spacer(layout); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs index 54cab99e7..258594f3d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs @@ -50,20 +50,25 @@ namespace Barotrauma.Steam lobbyState = LobbyState.Owner; lobbyID = (currentLobby?.Id).Value; - if (serverSettings.IsPublic) - { - currentLobby?.SetPublic(); - } - else - { - currentLobby?.SetFriendsOnly(); - } + SetLobbyPublic(serverSettings.IsPublic); currentLobby?.SetJoinable(true); UpdateLobby(serverSettings); }); } + public static void SetLobbyPublic(bool isPublic) + { + if (isPublic) + { + currentLobby?.SetPublic(); + } + else + { + currentLobby?.SetFriendsOnly(); + } + } + public static void UpdateLobby(ServerSettings serverSettings) { if (GameMain.Client == null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs index 2d4a1ffb2..67bc81568 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs @@ -104,7 +104,7 @@ namespace Barotrauma } } - List successEffects = statusEffects.Where(se => se.type == ActionType.OnUse).ToList(); + List successEffects = statusEffects.Where(se => se.type == ActionType.OnSuccess).ToList(); List failureEffects = statusEffects.Where(se => se.type == ActionType.OnFailure).ToList(); foreach (StatusEffect statusEffect in successEffects) diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 41e27b185..92a7c8a82 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.20.0.0 + 0.20.4.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma @@ -100,6 +100,8 @@ PreserveNewest + + Icon.bmp diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 24e07ca66..2a8218f30 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.20.0.0 + 0.20.4.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma @@ -99,6 +99,7 @@ PreserveNewest + Icon.bmp diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 4b80f7c8d..77fedbce5 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.20.0.0 + 0.20.4.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma @@ -117,6 +117,7 @@ + @@ -126,6 +127,7 @@ + diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 7e7a57032..38f6eccc6 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.20.0.0 + 0.20.4.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 34e6882e6..56a08b7b4 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.20.0.0 + 0.20.4.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index a80e80caa..eedb1f10b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -269,10 +269,13 @@ namespace Barotrauma case EventType.UpdateTalents: if (c.Character != this) { + if (!IsBot || !c.HasPermission(ClientPermissions.ManageBotTalents)) + { #if DEBUG - DebugConsole.Log("Received a character update message from a client who's not controlling the character"); + DebugConsole.Log("Received a character update message from a client who's not controlling the character"); #endif - return; + return; + } } // get the full list of talents from the player, only give the ones diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs index dfa4bc4c0..5d9dde87c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs @@ -31,7 +31,7 @@ namespace Barotrauma if (convAction.SelectedOption > -1) { //someone else already chose an option for this conversation: interrupt for this client - convAction.ServerWrite(convAction.speaker, sender, interrupt: true); + convAction.ServerWrite(convAction.Speaker, sender, interrupt: true); } else { diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs index e30b1148f..e46134ca6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs @@ -2,7 +2,9 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; +using Barotrauma.Extensions; using Barotrauma.Networking; namespace Barotrauma @@ -30,6 +32,9 @@ namespace Barotrauma switch (header) { + case NetworkHeader.ADD_EVERYTHING_TO_PENDING: + ProcessAddEverything(sender); + break; case NetworkHeader.REQUEST_AFFLICTIONS: ProcessRequestedAfflictions(inc, sender); break; @@ -57,7 +62,14 @@ namespace Barotrauma NetCrewMember newCrewMember = INetSerializableStruct.Read(inc); InsertPendingCrewMember(newCrewMember); - ServerSend(newCrewMember, NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable, reponseClient: client); + ServerSend(new NetCollection(newCrewMember), NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable, reponseClient: client); + } + + private void ProcessAddEverything(Client client) + { + if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + AddEverythingToPending(); + ServerSend(PendingHeals.ToNetCollection(), NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable, reponseClient: client); } private void ProcessNewRemoval(IReadMessage inc, Client client) @@ -73,12 +85,7 @@ namespace Barotrauma { if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } - INetSerializableStruct writeCrewMember = new NetPendingCrew - { - CrewMembers = PendingHeals.ToArray() - }; - - ServerSend(writeCrewMember, NetworkHeader.REQUEST_PENDING, DeliveryMethod.Reliable, targetClient: client); + ServerSend(PendingHeals.ToNetCollection(), NetworkHeader.REQUEST_PENDING, DeliveryMethod.Reliable, targetClient: client); } private void ProcessHealing(Client client) @@ -107,10 +114,10 @@ namespace Barotrauma CharacterInfo? foundInfo = crewMember.FindCharacterInfo(GetCrewCharacters()); - NetAffliction[] pendingAfflictions = Array.Empty(); + ImmutableArray pendingAfflictions = ImmutableArray.Empty; int infoId = 0; - if (foundInfo is { Character: { CharacterHealth: { } health } }) + if (foundInfo is { Character.CharacterHealth: { } health }) { pendingAfflictions = GetAllAfflictions(health); infoId = foundInfo.GetIdentifierUsingOriginalName(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index e48d786f1..666cd3039 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -2516,6 +2516,7 @@ namespace Barotrauma.Networking msg.WriteInt32(ServerSettings.MaximumMoneyTransferRequest); msg.WriteBoolean(IsUsingRespawnShuttle()); msg.WriteByte((byte)ServerSettings.LosMode); + msg.WriteByte((byte)ServerSettings.ShowEnemyHealthBars); msg.WriteBoolean(includesFinalize); msg.WritePadBits(); ServerSettings.WriteMonsterEnabled(msg); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index 603683348..7e348523a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -272,7 +272,6 @@ namespace Barotrauma.Networking XDocument doc = new XDocument(new XElement("serversettings")); doc.Root.SetAttributeValue("name", ServerName); - doc.Root.SetAttributeValue("public", IsPublic); doc.Root.SetAttributeValue("port", Port); #if USE_STEAM doc.Root.SetAttributeValue("queryport", QueryPort); diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index f69fca38a..575b0e435 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.20.0.0 + 0.20.4.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/Data/campaignsettings.xml b/Barotrauma/BarotraumaShared/Data/campaignsettings.xml index d88b556aa..40605ddf4 100644 --- a/Barotrauma/BarotraumaShared/Data/campaignsettings.xml +++ b/Barotrauma/BarotraumaShared/Data/campaignsettings.xml @@ -7,18 +7,21 @@ + permissions="ManageRound,Kick,SelectSub,SelectMode,ManageCampaign,ConsoleCommands,ServerLog,ManageSettings,ManageMoney,ManageBotTalents"> diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 4cf5983f2..f3ac3917c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -1836,8 +1836,7 @@ namespace Barotrauma float GetTargetMaxSpeed() => Character.ApplyTemporarySpeedLimits(Character.AnimController.CurrentSwimParams.MovementSpeed * 0.3f); } } - - if (AttackLimb is Limb attackLimb && attackLimb.attack.Ranged) + if (selectedTargetingParams.AttackPattern == AttackPattern.Straight && AttackLimb is Limb attackLimb && attackLimb.attack.Ranged) { bool advance = !canAttack && Character.InWater || distance > attackLimb.attack.Range * 0.9f; bool fallBack = canAttack && distance < Math.Min(250, attackLimb.attack.Range * 0.25f); @@ -1881,19 +1880,11 @@ namespace Barotrauma } } } - IDamageable damageTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget?.Entity as IDamageable; - if (AttackLimb?.attack is Attack { Ranged: true} attack) + Entity targetEntity = wallTarget?.Structure ?? SelectedAiTarget?.Entity; + IDamageable damageTarget = targetEntity as IDamageable; + if (AttackLimb?.attack is Attack { Ranged: true} attack && targetEntity != null) { - Limb limb = GetLimbToRotate(attack); - if (limb != null) - { - Vector2 toTarget = damageTarget.WorldPosition - limb.WorldPosition; - float offset = limb.Params.GetSpriteOrientation() - MathHelper.PiOver2; - limb.body.SuppressSmoothRotationCalls = false; - float angle = MathUtils.VectorToAngle(toTarget); - limb.body.SmoothRotate(angle + offset, attack.AimRotationTorque); - limb.body.SuppressSmoothRotationCalls = true; - } + AimRangedAttack(attack, targetEntity); } if (canAttack) { @@ -1908,6 +1899,22 @@ namespace Barotrauma } } + public void AimRangedAttack(Attack attack, Entity targetEntity) + { + if (attack == null || attack.Ranged == false || targetEntity == null) { return; } + Character.SetInput(InputType.Aim, false, true); + Limb limb = GetLimbToRotate(attack); + if (limb != null) + { + Vector2 toTarget = targetEntity.WorldPosition - limb.WorldPosition; + float offset = limb.Params.GetSpriteOrientation() - MathHelper.PiOver2; + limb.body.SuppressSmoothRotationCalls = false; + float angle = MathUtils.VectorToAngle(toTarget); + limb.body.SmoothRotate(angle + offset, attack.AimRotationTorque); + limb.body.SuppressSmoothRotationCalls = true; + } + } + private bool IsValidAttack(Limb attackingLimb, IEnumerable currentContexts, Entity target) { if (attackingLimb == null) { return false; } @@ -2190,11 +2197,11 @@ namespace Barotrauma if (!ActiveAttack.IsRunning) { #if SERVER - GameMain.NetworkMember.CreateEntityEvent(Character, new Character.SetAttackTargetEventData( - AttackLimb, - damageTarget, - targetLimb, - SimPosition)); + GameMain.NetworkMember.CreateEntityEvent(Character, new Character.SetAttackTargetEventData( + AttackLimb, + damageTarget, + targetLimb, + SimPosition)); #else Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3); #endif @@ -3599,7 +3606,10 @@ namespace Barotrauma { // 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; + // Pets should always check the visibility, unless the pet and the target are both outside the submarine -> shouldn't target when they can't perceive (= no wall hack) + checkVisibility = + Character.IsPet && (Character.Submarine == null) != (target.Entity.Submarine == null) || + target.Entity.Submarine != null && target.Entity.Submarine == Character.Submarine && target.Entity.Submarine.TeamID == CharacterTeamType.None; } if (dist > 0) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 3ed35dd0a..a50997a87 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -395,6 +395,10 @@ namespace Barotrauma } objectiveManager.UpdateObjectives(deltaTime); + if (reportProblemsTimer > 0) + { + reportProblemsTimer -= deltaTime; + } if (reactTimer > 0.0f) { reactTimer -= deltaTime; @@ -407,7 +411,6 @@ namespace Barotrauma else { Character.UpdateTeam(); - if (Character.CurrentHull != null) { if (Character.IsOnPlayerTeam) @@ -425,19 +428,15 @@ namespace Barotrauma } } } - if (Character.SpeechImpediment < 100.0f) + if (reportProblemsTimer <= 0.0f) { - reportProblemsTimer -= deltaTime; - if (reportProblemsTimer <= 0.0f) + if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck) { - if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck) - { - ReportProblems(); - } - reportProblemsTimer = reportProblemsInterval; + ReportProblems(); } - UpdateSpeaking(); + reportProblemsTimer = reportProblemsInterval; } + UpdateSpeaking(); UnequipUnnecessaryItems(); reactTimer = GetReactionTime(); } @@ -912,7 +911,7 @@ namespace Barotrauma { Order newOrder = null; Hull targetHull = null; - bool speak = true; + bool speak = Character.SpeechImpediment < 100; if (Character.CurrentHull != null) { bool isFighting = ObjectiveManager.HasActiveObjective(); @@ -1063,17 +1062,15 @@ namespace Barotrauma private void UpdateSpeaking() { if (!Character.IsOnPlayerTeam) { return; } - + if (Character.SpeechImpediment >= 100) { return; } if (Character.Oxygen < 20.0f) { Character.Speak(TextManager.Get("DialogLowOxygen").Value, null, Rand.Range(0.5f, 5.0f), "lowoxygen".ToIdentifier(), 30.0f); } - if (Character.Bleeding > 2.0f) { Character.Speak(TextManager.Get("DialogBleeding").Value, null, Rand.Range(0.5f, 5.0f), "bleeding".ToIdentifier(), 30.0f); } - if (Character.PressureTimer > 50.0f && Character.CurrentHull?.DisplayName != null) { Character.Speak(TextManager.GetWithVariable("DialogPressure", "[roomname]", Character.CurrentHull.DisplayName, FormatCapitals.Yes).Value, null, Rand.Range(0.5f, 5.0f), "pressure".ToIdentifier(), 30.0f); @@ -1860,6 +1857,7 @@ namespace Barotrauma bool targetAdded = false; DoForEachCrewMember(caller, humanAI => { + if (caller != humanAI.Character && caller.SpeechImpediment >= 100) { return; } var objective = humanAI.ObjectiveManager.GetObjective(); if (objective != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index 30d687a2a..7af3858b9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -502,10 +502,12 @@ namespace Barotrauma public static IEnumerable GetTreatableAfflictions(Character character) { - foreach (Affliction affliction in character.CharacterHealth.GetAllAfflictions()) + var allAfflictions = character.CharacterHealth.GetAllAfflictions(); + foreach (Affliction affliction in allAfflictions) { if (affliction.Prefab.IsBuff || affliction.Strength < affliction.Prefab.TreatmentThreshold) { continue; } if (!affliction.Prefab.TreatmentSuitability.Any(kvp => kvp.Value > 0)) { continue; } + if (allAfflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Identifier))) { continue; } yield return affliction; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 99b8a56dc..3a9a063ac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -1836,8 +1836,6 @@ namespace Barotrauma { heldItem.FlipX(relativeToSub: false); } - // TODO: was this added by a mistake? - //heldItem.FlipX(relativeToSub: false); } foreach (Limb limb in Limbs) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index 387472cde..abc470dcd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Barotrauma.Items.Components; namespace Barotrauma { @@ -530,7 +531,7 @@ namespace Barotrauma effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); - targets.AddRange(effect.GetNearbyTargets(worldPosition, targets)); + effect.AddNearbyTargets(worldPosition, targets); effect.Apply(effectType, deltaTime, targetEntity, targets); } if (effect.HasTargetType(StatusEffect.TargetType.UseTarget)) @@ -569,7 +570,15 @@ namespace Barotrauma DamageParticles(deltaTime, worldPosition); - var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, playSound, targetLimb, penetration: Penetration); + float penetration = Penetration; + + float? penetrationValue = SourceItem?.GetComponent()?.Penetration; + if (penetrationValue.HasValue) + { + penetration += penetrationValue.Value; + } + + var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, playSound, targetLimb, penetration); var effectType = attackResult.Damage > 0.0f ? ActionType.OnUse : ActionType.OnFailure; foreach (StatusEffect effect in statusEffects) @@ -599,7 +608,7 @@ namespace Barotrauma effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); - targets.AddRange(effect.GetNearbyTargets(worldPosition, targets)); + effect.AddNearbyTargets(worldPosition, targets); effect.Apply(effectType, deltaTime, targetLimb.character, targets); } if (effect.HasTargetType(StatusEffect.TargetType.UseTarget)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 78d43ee78..caad557b6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -1601,6 +1601,15 @@ namespace Barotrauma if (Info?.Job == null) { return 0.0f; } float skillLevel = Info.Job.GetSkillLevel(skillIdentifier); + if (overrideStatTypes.TryGetValue(skillIdentifier, out StatTypes statType)) + { + float skillOverride = GetStatValue(statType); + if (skillOverride > skillLevel) + { + skillLevel = skillOverride; + } + } + // apply multipliers first so that multipliers only affect base skill value foreach (Affliction affliction in CharacterHealth.GetAllAfflictions()) { @@ -1631,15 +1640,6 @@ namespace Barotrauma skillLevel += GetStatValue(GetSkillStatType(skillIdentifier)); - if (overrideStatTypes.TryGetValue(skillIdentifier, out StatTypes statType)) - { - float skillOverride = GetStatValue(statType); - if (skillOverride > skillLevel) - { - skillLevel = skillOverride; - } - } - return skillLevel; } @@ -1972,6 +1972,15 @@ namespace Barotrauma } } #endif + + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Controlled != this && IsKeyDown(InputType.Aim)) + { + if (currentAttackTarget.AttackLimb?.attack is Attack { Ranged: true } attack && AIController is EnemyAIController enemyAi) + { + enemyAi.AimRangedAttack(attack, currentAttackTarget.DamageTarget as Entity); + } + } + if (attackCoolDown > 0.0f) { attackCoolDown -= deltaTime; @@ -1982,7 +1991,7 @@ namespace Barotrauma { if ((currentAttackTarget.DamageTarget as Entity)?.Removed ?? false) { - currentAttackTarget = default(AttackTargetData); + currentAttackTarget = default; } currentAttackTarget.AttackLimb?.UpdateAttack(deltaTime, currentAttackTarget.AttackPos, currentAttackTarget.DamageTarget, out _); } @@ -2077,19 +2086,22 @@ namespace Barotrauma } } - bool CanUseItemsWhenSelected(Item item) => item == null || !item.Prefab.DisableItemUsageWhenSelected; - if (CanUseItemsWhenSelected(SelectedItem) && CanUseItemsWhenSelected(SelectedSecondaryItem)) + if (Inventory != null) { - foreach (Item item in HeldItems) + bool CanUseItemsWhenSelected(Item item) => item == null || !item.Prefab.DisableItemUsageWhenSelected; + if (CanUseItemsWhenSelected(SelectedItem) && CanUseItemsWhenSelected(SelectedSecondaryItem)) { - tryUseItem(item, deltaTime); - } - foreach (Item item in Inventory.AllItems) - { - if (item.GetComponent() is { AllowUseWhenWorn: true } && HasEquippedItem(item)) + foreach (Item item in HeldItems) { tryUseItem(item, deltaTime); } + foreach (Item item in Inventory.AllItems) + { + if (item.GetComponent() is { AllowUseWhenWorn: true } && HasEquippedItem(item)) + { + tryUseItem(item, deltaTime); + } + } } } @@ -2170,6 +2182,8 @@ namespace Barotrauma private AttackTargetData currentAttackTarget; public void SetAttackTarget(Limb attackLimb, IDamageable damageTarget, Vector2 attackPos) { + DebugConsole.NewMessage($"SetAttackTarget {this.ToString()}: " + (damageTarget?.ToString() ?? null)); + currentAttackTarget = new AttackTargetData() { AttackLimb = attackLimb, @@ -3824,7 +3838,7 @@ namespace Barotrauma return attackResult; } - public void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability, float damage, bool allowBeheading, Character attacker = null) + public void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability, float damage, bool allowBeheading, bool ignoreSeveranceProbabilityModifier = false, Character attacker = null) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } #if DEBUG @@ -3852,7 +3866,7 @@ namespace Barotrauma var referenceLimb = targetLimb.type == LimbType.Head && targetLimb.Params.ID == 0 ? joint.LimbA : joint.LimbB; if (referenceLimb != targetLimb) { continue; } float probability = severLimbsProbability; - if (!IsDead && probability < 1) + if (!IsDead && !ignoreSeveranceProbabilityModifier) { probability *= joint.Params.SeveranceProbabilityModifier; } @@ -4111,7 +4125,7 @@ namespace Barotrauma statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); - targets.AddRange(statusEffect.GetNearbyTargets(WorldPosition, targets)); + statusEffect.AddNearbyTargets(WorldPosition, targets); statusEffect.Apply(actionType, deltaTime, this, targets); } else if (statusEffect.targetLimbs != null) @@ -4745,6 +4759,8 @@ namespace Barotrauma public bool HasJob(string identifier) => Info?.Job?.Prefab.Identifier == identifier; + public bool HasJob(Identifier identifier) => Info?.Job?.Prefab.Identifier == identifier; + public bool IsProtectedFromPressure() { return HasAbilityFlag(AbilityFlags.ImmuneToPressure) || PressureProtection >= (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f); @@ -4975,7 +4991,7 @@ namespace Barotrauma /// private readonly Dictionary wearableStatValues = new Dictionary(); - public float GetStatValue(StatTypes statType) + public float GetStatValue(StatTypes statType, bool includeSaved = true) { if (!IsHuman) { return 0f; } @@ -4988,7 +5004,7 @@ namespace Barotrauma { statValue += CharacterHealth.GetStatValue(statType); } - if (Info != null) + if (Info != null && includeSaved) { // could be optimized by instead updating the Character.cs statvalues dictionary whenever the CharacterInfo.cs values change statValue += Info.GetSavedStatValue(statType); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index 393db009a..24a96e3fe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -1233,10 +1233,6 @@ namespace Barotrauma int prevAmount = ExperiencePoints; var experienceGainMultiplier = new AbilityExperienceGainMultiplier(1f); - if (isMissionExperience) - { - Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplier); - } experienceGainMultiplier.Value += Character?.GetStatValue(StatTypes.ExperienceGainMultiplier) ?? 0; amount = (int)(amount * experienceGainMultiplier.Value); @@ -1808,7 +1804,7 @@ namespace Barotrauma { if (SavedStatValues.TryGetValue(statType, out var statValues)) { - return statValues.Where(s => s.StatIdentifier == statIdentifier).Sum(v => v.StatValue); + return statValues.Where(value => ToolBox.StatIdentifierMatches(value.StatIdentifier, statIdentifier)).Sum(static v => v.StatValue); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index 4aebc88c4..a1a28bd20 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -429,7 +429,7 @@ namespace Barotrauma statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); - targets.AddRange(statusEffect.GetNearbyTargets(characterHealth.Character.WorldPosition, targets)); + statusEffect.AddNearbyTargets(characterHealth.Character.WorldPosition, targets); statusEffect.Apply(type, deltaTime, characterHealth.Character, targets); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 91c1998b7..93bbe63d7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Reflection; using System.Xml.Linq; using Barotrauma.Extensions; +using System.Collections.Immutable; namespace Barotrauma { @@ -214,7 +215,6 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.No)] public Identifier DialogFlag { get; private set; } - [Serialize("", IsPropertySaveable.No)] public Identifier Tag { get; private set; } @@ -276,6 +276,47 @@ namespace Barotrauma } } + public class Description + { + public enum TargetType + { + Any, + Self, + OtherCharacter + } + + public readonly LocalizedString Text; + public readonly Identifier TextTag; + public readonly float MinStrength, MaxStrength; + public readonly TargetType Target; + + public Description(ContentXElement element, AfflictionPrefab affliction) + { + TextTag = element.GetAttributeIdentifier("textidentifier", Identifier.Empty); + if (!TextTag.IsEmpty) + { + Text = TextManager.Get(TextTag); + } + string text = element.GetAttributeString("text", string.Empty); + if (!text.IsNullOrEmpty()) + { + Text = Text?.Fallback(text) ?? text; + } + else if (TextTag.IsEmpty) + { + DebugConsole.ThrowError($"Error in affliction \"{affliction.Identifier}\" - no text defined for one of the descriptions."); + } + + MinStrength = element.GetAttributeFloat(nameof(MinStrength), 0.0f); + MaxStrength = element.GetAttributeFloat(nameof(MaxStrength), 100.0f); + if (MinStrength >= MaxStrength) + { + DebugConsole.ThrowError($"Error in affliction \"{affliction.Identifier}\" - max strength is not larger than min."); + } + Target = element.GetAttributeEnum(nameof(Target), TargetType.Any); + } + } + public class PeriodicEffect { public readonly List StatusEffects = new List(); @@ -313,7 +354,6 @@ namespace Barotrauma public static readonly PrefabCollection Prefabs = new PrefabCollection(); - private bool disposed = false; public override void Dispose() { } public static IEnumerable List => Prefabs; @@ -330,15 +370,21 @@ namespace Barotrauma //(e.g. mental health problems on head, lack of oxygen on torso...) public readonly LimbType IndicatorLimb; - public readonly LocalizedString Name, Description; + public readonly LocalizedString Name; public readonly Identifier TranslationIdentifier; public readonly bool IsBuff; + public readonly bool AffectMachines; public readonly bool HealableInMedicalClinic; public readonly float HealCostMultiplier; public readonly int BaseHealCost; public readonly LocalizedString CauseOfDeathDescription, SelfCauseOfDeathDescription; + private readonly LocalizedString defaultDescription; + public readonly ImmutableList Descriptions; + + public readonly bool HideIconAfterDelay; + //how high the strength has to be for the affliction to take affect public readonly float ActivationThreshold = 0.0f; //how high the strength has to be for the affliction icon to be shown in the UI @@ -355,6 +401,11 @@ namespace Barotrauma //how strong the affliction needs to be before bots attempt to treat it public readonly float TreatmentThreshold = 5.0f; + /// + /// Bots will not try to treat the affliction if the character has any of these afflictions + /// + public ImmutableHashSet IgnoreTreatmentIfAfflictedBy; + /// /// The affliction is automatically removed after this time. 0 = unlimited /// @@ -413,13 +464,14 @@ namespace Barotrauma { Name = Name.Fallback(fallbackName); } - Description = TextManager.Get($"AfflictionDescription.{TranslationIdentifier}"); + defaultDescription = TextManager.Get($"AfflictionDescription.{TranslationIdentifier}"); string fallbackDescription = element.GetAttributeString("description", ""); if (!string.IsNullOrEmpty(fallbackDescription)) { - Description = Description.Fallback(fallbackDescription); + defaultDescription = defaultDescription.Fallback(fallbackDescription); } - IsBuff = element.GetAttributeBool("isbuff", false); + IsBuff = element.GetAttributeBool(nameof(IsBuff), false); + AffectMachines = element.GetAttributeBool(nameof(AffectMachines), true); HealableInMedicalClinic = element.GetAttributeBool("healableinmedicalclinic", !IsBuff && @@ -428,6 +480,8 @@ namespace Barotrauma HealCostMultiplier = element.GetAttributeFloat(nameof(HealCostMultiplier), 1f); BaseHealCost = element.GetAttributeInt(nameof(BaseHealCost), 0); + IgnoreTreatmentIfAfflictedBy = element.GetAttributeIdentifierArray(nameof(IgnoreTreatmentIfAfflictedBy), Array.Empty()).ToImmutableHashSet(); + Duration = element.GetAttributeFloat(nameof(Duration), 0.0f); if (element.GetAttribute("nameidentifier") != null) @@ -445,30 +499,33 @@ namespace Barotrauma } } - ActivationThreshold = element.GetAttributeFloat("activationthreshold", 0.0f); - ShowIconThreshold = element.GetAttributeFloat("showiconthreshold", Math.Max(ActivationThreshold, 0.05f)); - ShowIconToOthersThreshold = element.GetAttributeFloat("showicontoothersthreshold", ShowIconThreshold); - MaxStrength = element.GetAttributeFloat("maxstrength", 100.0f); - GrainBurst = element.GetAttributeFloat(nameof(GrainBurst).ToLowerInvariant(), 0.0f); + HideIconAfterDelay = element.GetAttributeBool(nameof(HideIconAfterDelay), false); - ShowInHealthScannerThreshold = element.GetAttributeFloat("showinhealthscannerthreshold", + ActivationThreshold = element.GetAttributeFloat(nameof(ActivationThreshold), 0.0f); + ShowIconThreshold = element.GetAttributeFloat(nameof(ShowIconThreshold), Math.Max(ActivationThreshold, 0.05f)); + ShowIconToOthersThreshold = element.GetAttributeFloat(nameof(ShowIconToOthersThreshold), ShowIconThreshold); + MaxStrength = element.GetAttributeFloat(nameof(MaxStrength), 100.0f); + GrainBurst = element.GetAttributeFloat(nameof(GrainBurst), 0.0f); + + ShowInHealthScannerThreshold = element.GetAttributeFloat(nameof(ShowInHealthScannerThreshold), Math.Max(ActivationThreshold, AfflictionType == "talentbuff" ? float.MaxValue : ShowIconToOthersThreshold)); - TreatmentThreshold = element.GetAttributeFloat("treatmentthreshold", Math.Max(ActivationThreshold, 5.0f)); + TreatmentThreshold = element.GetAttributeFloat(nameof(TreatmentThreshold), Math.Max(ActivationThreshold, 5.0f)); - DamageOverlayAlpha = element.GetAttributeFloat("damageoverlayalpha", 0.0f); - BurnOverlayAlpha = element.GetAttributeFloat("burnoverlayalpha", 0.0f); + DamageOverlayAlpha = element.GetAttributeFloat(nameof(DamageOverlayAlpha), 0.0f); + BurnOverlayAlpha = element.GetAttributeFloat(nameof(BurnOverlayAlpha), 0.0f); - KarmaChangeOnApplied = element.GetAttributeFloat("karmachangeonapplied", 0.0f); + KarmaChangeOnApplied = element.GetAttributeFloat(nameof(KarmaChangeOnApplied), 0.0f); CauseOfDeathDescription = TextManager.Get($"AfflictionCauseOfDeath.{TranslationIdentifier}").Fallback(element.GetAttributeString("causeofdeathdescription", "")); SelfCauseOfDeathDescription = TextManager.Get($"AfflictionCauseOfDeathSelf.{TranslationIdentifier}").Fallback(element.GetAttributeString("selfcauseofdeathdescription", "")); - IconColors = element.GetAttributeColorArray("iconcolors", null); - AfflictionOverlayAlphaIsLinear = element.GetAttributeBool("afflictionoverlayalphaislinear", false); - AchievementOnRemoved = element.GetAttributeIdentifier("achievementonremoved", ""); + IconColors = element.GetAttributeColorArray(nameof(IconColors), null); + AfflictionOverlayAlphaIsLinear = element.GetAttributeBool(nameof(AfflictionOverlayAlphaIsLinear), false); + AchievementOnRemoved = element.GetAttributeIdentifier(nameof(AchievementOnRemoved), ""); ResetBetweenRounds = element.GetAttributeBool("resetbetweenrounds", false); + List descriptions = new List(); foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -485,15 +542,38 @@ namespace Barotrauma case "effect": case "periodiceffect": break; + case "description": + descriptions.Add(new Description(subElement, this)); + break; default: DebugConsole.AddWarning($"Unrecognized element in affliction \"{Identifier}\" ({subElement.Name})"); break; } } + Descriptions = descriptions.ToImmutableList(); constructor = type.GetConstructor(new[] { typeof(AfflictionPrefab), typeof(float) }); } + public LocalizedString GetDescription(float strength, Description.TargetType targetType) + { + foreach (var description in Descriptions) + { + if (strength < description.MinStrength || strength > description.MaxStrength) { continue; } + switch (targetType) + { + case Description.TargetType.Self: + if (description.Target == Description.TargetType.OtherCharacter) { continue; } + break; + case Description.TargetType.OtherCharacter: + if (description.Target == Description.TargetType.Self) { continue; } + break; + } + return description.Text; + } + return defaultDescription; + } + public static void LoadAllEffects() { Prefabs.ForEach(p => p.LoadEffects()); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index b7825be29..a4774e7a0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -104,7 +104,7 @@ namespace Barotrauma public bool DoesBleed { - get => Character.Params.Health.DoesBleed; + get => Character.Params.Health.DoesBleed && !Character.Params.IsMachine; private set => Character.Params.Health.DoesBleed = value; } @@ -550,7 +550,7 @@ namespace Barotrauma amount -= reduceAmount; if (treatmentAction != null) { - if (treatmentAction.Value == ActionType.OnUse) + if (treatmentAction.Value == ActionType.OnUse || treatmentAction.Value == ActionType.OnSuccess) { matchingAffliction.AppliedAsSuccessfulTreatmentTime = Timing.TotalTime; } @@ -690,6 +690,7 @@ namespace Barotrauma private void AddLimbAffliction(LimbHealth limbHealth, Affliction newAffliction, bool allowStacking = true) { + if (Character.Params.IsMachine && !newAffliction.Prefab.AffectMachines) { return; } if (!DoesBleed && newAffliction is AfflictionBleeding) { return; } if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; } if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == "stun") { return; } @@ -1076,6 +1077,7 @@ namespace Barotrauma } if (strength <= affliction.Prefab.TreatmentThreshold) { continue; } + if (afflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Key.Identifier))) { continue; } if (ignoreHiddenAfflictions) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs index a71d1e73e..03fb3ad7d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs @@ -79,27 +79,35 @@ namespace Barotrauma public ref readonly ImmutableArray ParsedAfflictionTypes => ref parsedAfflictionTypes; - public DamageModifier(XElement element, string parentDebugName) + public DamageModifier(XElement element, string parentDebugName, bool checkErrors = true) { Deserialize(element); if (element.Attribute("afflictionnames") != null) { DebugConsole.ThrowError("Error in DamageModifier config (" + parentDebugName + ") - define afflictions using identifiers or types instead of names."); } - foreach (var afflictionType in parsedAfflictionTypes) + if (checkErrors) { - if (!AfflictionPrefab.Prefabs.Any(p => p.AfflictionType == afflictionType)) + foreach (var afflictionType in parsedAfflictionTypes) { - createWarningOrError($"Potentially invalid damage modifier in \"{parentDebugName}\". Could not find any afflictions of the type \"{afflictionType}\". Did you mean to use an affliction identifier instead?"); - } - } - foreach (var afflictionIdentifier in parsedAfflictionIdentifiers) - { - if (!AfflictionPrefab.Prefabs.ContainsKey(afflictionIdentifier)) - { - createWarningOrError($"Potentially invalid damage modifier in \"{parentDebugName}\". Could not find any afflictions with the identifier \"{afflictionIdentifier}\". Did you mean to use an affliction type instead?"); + if (!AfflictionPrefab.Prefabs.Any(p => p.AfflictionType == afflictionType)) + { + createWarningOrError($"Potentially invalid damage modifier in \"{parentDebugName}\". Could not find any afflictions of the type \"{afflictionType}\". Did you mean to use an affliction identifier instead?"); + } + } + foreach (var afflictionIdentifier in parsedAfflictionIdentifiers) + { + if (!AfflictionPrefab.Prefabs.ContainsKey(afflictionIdentifier)) + { + createWarningOrError($"Potentially invalid damage modifier in \"{parentDebugName}\". Could not find any afflictions with the identifier \"{afflictionIdentifier}\". Did you mean to use an affliction type instead?"); + } + } + if (!parsedAfflictionTypes.Any() && !parsedAfflictionIdentifiers.Any()) + { + createWarningOrError($"Potentially invalid damage modifier in \"{parentDebugName}\". Neither affliction types of identifiers defined."); } } + static void createWarningOrError(string msg) { #if DEBUG diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index 03afe476f..b8bc40ecf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -1196,7 +1196,7 @@ namespace Barotrauma statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); - targets.AddRange(statusEffect.GetNearbyTargets(WorldPosition, targets)); + statusEffect.AddNearbyTargets(WorldPosition, targets); statusEffect.Apply(actionType, deltaTime, character, targets); } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index b2323c54d..b29d6c2a3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -50,6 +50,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes, description: "Can the creature live without water or does it die on dry land?"), Editable] public bool NeedsWater { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "Is this creature an artificial creature, like robot or machine that shouldn't be affected by afflictions that affect only organic creatures? Overrides DoesBleed."), Editable] + public bool IsMachine { get; set; } + [Serialize(false, IsPropertySaveable.No), Editable] public bool CanSpeak { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs index bfdb89205..c3d2e872e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs @@ -27,7 +27,8 @@ namespace Barotrauma.Abilities { if (isPositiveReputation) { - if (abilityLocation.Location.Reputation.Faction.Reputation.Value <= 0) { return false; } + if (abilityLocation.Location?.Reputation is not { } reputation) { return false; } + if (reputation.Value <= 0) { return false; } } if (locationIdentifiers.Any()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs index fd1d55682..4ed81b82f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs @@ -33,13 +33,20 @@ namespace Barotrauma.Abilities { if (abilityObject is IAbilityMission { Mission: { } mission }) { - if (isAffiliated && GameMain.GameSession?.Campaign?.Factions.MaxBy(static f => f.Reputation.Value) is { } highestFaction) + if (isAffiliated) { - if (highestFaction.Reputation.Value < 0 || !mission.ReputationRewards.ContainsKey(highestFaction.Reputation.Identifier)) + if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return false; } + + // FIXME there's probably a better way to check the faction affiliated with the mission later + foreach (Identifier factionIdentifier in mission.ReputationRewards.Keys) { - return false; + if (factions.Where(faction => factionIdentifier == faction.Prefab.Identifier).Any(static faction => faction.GetPlayerAffiliationStatus() != FactionAffiliation.Affiliated)) + { + return false; + } } } + return missionType.Contains(mission.Prefab.Type); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs index e4458d78b..8b20847b1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs @@ -1,9 +1,5 @@ -using System; -using Barotrauma.Items.Components; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; -using Barotrauma.Extensions; +using Barotrauma.Extensions; +using System; namespace Barotrauma.Abilities { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNearbyCharacterCount.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNearbyCharacterCount.cs index e8b084f07..5d0acdbf0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNearbyCharacterCount.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNearbyCharacterCount.cs @@ -24,7 +24,7 @@ internal sealed class AbilityConditionNearbyCharacterCount : AbilityConditionDat foreach (Character otherCharacter in Character.CharacterList) { if (character.Submarine != otherCharacter.Submarine) { continue; } - if (!IsViableTarget(targetTypes, otherCharacter)) { return false; } + if (!IsViableTarget(targetTypes, otherCharacter)) { continue; } if (Vector2.DistanceSquared(character.WorldPosition, otherCharacter.WorldPosition) < distance * distance) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs index d408fbc83..969c9c5bd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs @@ -50,7 +50,7 @@ namespace Barotrauma.Abilities else if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); - targets.AddRange(statusEffect.GetNearbyTargets(targetCharacter.WorldPosition, targets)); + statusEffect.AddNearbyTargets(targetCharacter.WorldPosition, targets); if (!nearbyCharactersAppliesToSelf) { targets.RemoveAll(c => c == Character); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs index 707663745..1b4f880f8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs @@ -27,6 +27,8 @@ namespace Barotrauma.Abilities protected override void ApplyEffect() { + if (Character?.Submarine is null) { return; } + foreach (Item item in Character.Submarine.GetItems(true)) { if (item.HasTag(tags) || tags.Contains(item.Prefab.Identifier)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPointsToAllies.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPointsToAllies.cs new file mode 100644 index 000000000..130b5b988 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPointsToAllies.cs @@ -0,0 +1,25 @@ +#nullable enable + +namespace Barotrauma.Abilities +{ + internal sealed class CharacterAbilityGiveTalentPointsToAllies : CharacterAbility + { + private readonly int amount; + + public CharacterAbilityGiveTalentPointsToAllies(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + amount = abilityElement.GetAttributeInt("amount", 0); + } + + public override void InitializeAbility(bool addingFirstTime) + { + if (!addingFirstTime) { return; } + + foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) + { + if (character.Info is null) { return; } + character.Info.AdditionalTalentPoints += amount; + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs index a6141b79f..686aa6d48 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Xml.Linq; namespace Barotrauma.Abilities { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySetMetadataInt.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySetMetadataInt.cs index d24a201f3..a8da25c74 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySetMetadataInt.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySetMetadataInt.cs @@ -13,6 +13,11 @@ namespace Barotrauma.Abilities value = abilityElement.GetAttributeInt("value", 0); } + public override void InitializeAbility(bool addingFirstTime) + { + ApplyEffect(); + } + protected override void ApplyEffect() { if (identifier == Identifier.Empty) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs index 9994e51e6..96bdd50a9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs @@ -12,6 +12,8 @@ namespace Barotrauma.Abilities public override void InitializeAbility(bool addingFirstTime) { + if (!addingFirstTime) { return; } + JobPrefab? apprentice = CharacterAbilityApplyStatusEffectsToApprenticeship.GetApprenticeJob(Character, JobPrefab.Prefabs.ToImmutableHashSet()); if (apprentice is null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs index 18725da29..cf1100db6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs @@ -27,39 +27,40 @@ namespace Barotrauma.Abilities TimeSinceLastUpdate += deltaTime; if (TimeSinceLastUpdate < interval) { return; } - bool shouldApplyDelayedEffect; - bool conditionsDidntMatch; + bool conditionsMatched; if (AllConditionsMatched()) { effectDelayTimer += TimeSinceLastUpdate; - shouldApplyDelayedEffect = effectDelayTimer >= effectDelay; - conditionsDidntMatch = false; + bool shouldApplyDelayedEffect = effectDelayTimer >= effectDelay; + conditionsMatched = shouldApplyDelayedEffect; } else { effectDelayTimer = 0f; - shouldApplyDelayedEffect = false; - conditionsDidntMatch = true; + conditionsMatched = false; } bool hasFallbacks = fallbackAbilities.Count > 0; List abilitiesToRun = - conditionsDidntMatch && hasFallbacks + !conditionsMatched && hasFallbacks ? fallbackAbilities : characterAbilities; + if (hasFallbacks) + { + conditionsMatched = true; + } + foreach (var characterAbility in abilitiesToRun) { if (!characterAbility.IsViable()) { continue; } - characterAbility.UpdateCharacterAbility( - shouldApplyDelayedEffect || conditionsDidntMatch, - TimeSinceLastUpdate); + characterAbility.UpdateCharacterAbility(conditionsMatched, TimeSinceLastUpdate); } - if (shouldApplyDelayedEffect || (conditionsDidntMatch && hasFallbacks)) + if (conditionsMatched) { timesTriggered++; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs index 396244f9a..2702f649f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs @@ -7,7 +7,7 @@ namespace Barotrauma { internal sealed class TalentTree : Prefab { - public enum TalentTreeStageState + public enum TalentStages { Invalid, Locked, @@ -72,30 +72,30 @@ namespace Barotrauma // i hate this function - markus // me too - joonas - public static TalentTreeStageState GetTalentOptionStageState(Character character, Identifier subTreeIdentifier, int index, IReadOnlyCollection selectedTalents) + public static TalentStages GetTalentOptionStageState(Character character, Identifier subTreeIdentifier, int index, IReadOnlyCollection selectedTalents) { - if (character?.Info?.Job.Prefab is null) { return TalentTreeStageState.Invalid; } + if (character?.Info?.Job.Prefab is null) { return TalentStages.Invalid; } - if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return TalentTreeStageState.Invalid; } + if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return TalentStages.Invalid; } TalentSubTree subTree = talentTree!.TalentSubTrees.FirstOrDefault(tst => tst.Identifier == subTreeIdentifier); - if (subTree is null) { return TalentTreeStageState.Invalid; } + if (subTree is null) { return TalentStages.Invalid; } if (!TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents)) { - return TalentTreeStageState.Locked; + return TalentStages.Locked; } TalentOption targetTalentOption = subTree.TalentOptionStages[index]; if (targetTalentOption.HasEnoughTalents(character.Info)) { - return TalentTreeStageState.Unlocked; + return TalentStages.Unlocked; } if (targetTalentOption.HasSelectedTalent(selectedTalents)) { - return TalentTreeStageState.Highlighted; + return TalentStages.Highlighted; } bool hasTalentInLastTier = true; @@ -111,17 +111,17 @@ namespace Barotrauma if (!hasTalentInLastTier) { - return TalentTreeStageState.Locked; + return TalentStages.Locked; } bool hasPointsForNewTalent = character.Info.GetTotalTalentPoints() - selectedTalents.Count > 0; if (hasPointsForNewTalent) { - return isLastTalentPurchased ? TalentTreeStageState.Highlighted : TalentTreeStageState.Available; + return isLastTalentPurchased ? TalentStages.Highlighted : TalentStages.Available; } - return TalentTreeStageState.Locked; + return TalentStages.Locked; } @@ -207,7 +207,7 @@ namespace Barotrauma { nameIdentifier = $"talenttree.{Identifier}"; } - DisplayName = TextManager.Get($"talenttree.{nameIdentifier}").Fallback(Identifier.Value); + DisplayName = TextManager.Get(nameIdentifier).Fallback(Identifier.Value); Type = subTreeElement.GetAttributeEnum("type", TalentTreeType.Specialization); RequiredTrees = subTreeElement.GetAttributeIdentifierImmutableHashSet("requires", ImmutableHashSet.Empty); BlockedTrees = subTreeElement.GetAttributeIdentifierImmutableHashSet("blocks", ImmutableHashSet.Empty); @@ -234,7 +234,7 @@ namespace Barotrauma /// When specified the talent option will show talent with this identifier /// and clicking on it will expand the talent option to show the talents /// - public readonly Option ShowcaseTalent; + public readonly Dictionary> ShowCaseTalents = new Dictionary>(); public bool HasEnoughTalents(CharacterInfo character) => CountMatchingTalents(character.UnlockedTalents) >= MaxChosenTalents; public bool HasEnoughTalents(IReadOnlyCollection selectedTalents) => CountMatchingTalents(selectedTalents) >= MaxChosenTalents; @@ -269,19 +269,30 @@ namespace Barotrauma { MaxChosenTalents = talentOptionsElement.GetAttributeInt("maxchosentalents", 1); - Identifier showcaseTalent = talentOptionsElement.GetAttributeIdentifier("showcasetalent", Identifier.Empty); - ShowcaseTalent = !showcaseTalent.IsEmpty - ? Option.Some(showcaseTalent) - : Option.None(); + HashSet identifiers = new HashSet(); - var talentIdentifiers = new HashSet(); - foreach (var talentOptionElement in talentOptionsElement.GetChildElements("talentoption")) + foreach (ContentXElement talentOptionElement in talentOptionsElement.Elements()) { - Identifier identifier = talentOptionElement.GetAttributeIdentifier("identifier", Identifier.Empty); - talentIdentifiers.Add(identifier); + Identifier elementName = talentOptionElement.Name.ToIdentifier(); + if (elementName == "talentoption") + { + identifiers.Add(talentOptionElement.GetAttributeIdentifier("identifier", Identifier.Empty)); + } + else if (elementName == "showcasetalent") + { + Identifier showCaseIdentifier = talentOptionElement.GetAttributeIdentifier("identifier", Identifier.Empty); + HashSet showCaseTalentIdentifiers = new HashSet(); + foreach (ContentXElement subElement in talentOptionElement.Elements()) + { + Identifier identifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); + showCaseTalentIdentifiers.Add(identifier); + identifiers.Add(identifier); + } + ShowCaseTalents.Add(showCaseIdentifier, showCaseTalentIdentifiers.ToImmutableHashSet()); + } } - this.talentIdentifiers = talentIdentifiers.ToImmutableHashSet(); + talentIdentifiers = identifiers.ToImmutableHashSet(); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs index d99d29b57..1e2dccfad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs @@ -18,7 +18,7 @@ namespace Barotrauma public const string LocalModsDir = "LocalMods"; public static readonly string WorkshopModsDir = Barotrauma.IO.Path.Combine( - SaveUtil.SaveFolder, + SaveUtil.DefaultSaveFolder, "WorkshopMods", "Installed"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/Identifier.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/Identifier.cs index 824924101..7fb3453ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/Identifier.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/Identifier.cs @@ -125,6 +125,7 @@ namespace Barotrauma internal int IndexOf(char c) => Value.IndexOf(c); internal Identifier this[Range range] => Value[range].ToIdentifier(); + internal Char this[int i] => Value[i]; } public static class IdentifierExtensions diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 050230110..7425edd16 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -2199,7 +2199,7 @@ namespace Barotrauma //Dont do a thing, random is basically Human points anyways - its in the help description. break; default: - var matchingCharacter = FindMatchingCharacter(args.Skip(1).ToArray()); + var matchingCharacter = FindMatchingCharacter(args.Skip(1).Take(1).ToArray()); if (matchingCharacter != null){ spawnInventory = matchingCharacter.Inventory; } break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index f23d4a02c..2eafaf60e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -154,6 +154,7 @@ namespace Barotrauma BallastFloraDamageMultiplier, HoldBreathMultiplier, Apprenticeship, + Affiliation, CPRBoost } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs index 9413193d1..4269be922 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs @@ -25,6 +25,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes)] public bool RequireEquipped { get; set; } + [Serialize(true, IsPropertySaveable.Yes)] + public bool Recursive { get; set; } + [Serialize(-1, IsPropertySaveable.Yes)] public int ItemContainerIndex { get; set; } @@ -97,7 +100,7 @@ namespace Barotrauma { if (inventory == null) { return false; } int count = 0; - foreach (Item item in inventory.FindAllItems(it => itemTags.Any(it.HasTag) || itemIdentifierSplit.Contains(it.Prefab.Identifier))) + foreach (Item item in inventory.FindAllItems(it => itemTags.Any(it.HasTag) || itemIdentifierSplit.Contains(it.Prefab.Identifier), recursive: Recursive)) { if (!ConditionalsMatch(item, character)) { continue; } count++; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMissionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMissionAction.cs new file mode 100644 index 000000000..530a63429 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMissionAction.cs @@ -0,0 +1,58 @@ +using System; +using System.Linq; + +namespace Barotrauma; + +class CheckMissionAction : BinaryOptionAction +{ + public enum MissionType + { + Current, + Selected, + Available + } + + [Serialize(MissionType.Current, IsPropertySaveable.Yes)] + public MissionType Type { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier MissionIdentifier { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier MissionTag { get; set; } + + [Serialize(1, IsPropertySaveable.Yes)] + public int MissionCount { get; set; } + + public CheckMissionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + MissionCount = Math.Max(MissionCount, 0); + } + + protected override bool? DetermineSuccess() + { + var missions = Type switch + { + MissionType.Current => GameMain.GameSession?.Missions, + MissionType.Selected => GameMain.GameSession?.Campaign?.Missions, + MissionType.Available => GameMain.GameSession?.Map?.CurrentLocation?.AvailableMissions, + _ => null + }; + if (missions is not null) + { + if (!MissionIdentifier.IsEmpty) + { + return missions.Any(m => m.Prefab.Identifier == MissionIdentifier); + } + else if (!MissionTag.IsEmpty) + { + return missions.Count(m => m.Prefab.Tags.Contains(MissionTag.Value)) >= MissionCount; + } + else + { + return missions.Count() >= MissionCount; + } + } + return MissionIdentifier.IsEmpty && MissionTag.IsEmpty && MissionCount == 0; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckObjectiveAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckObjectiveAction.cs new file mode 100644 index 000000000..15e5e90a9 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckObjectiveAction.cs @@ -0,0 +1,15 @@ +namespace Barotrauma; + +partial class CheckObjectiveAction : BinaryOptionAction +{ + public CheckObjectiveAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } + + protected override bool? DetermineSuccess() + { + bool success = false; + DetermineSuccessProjSpecific(ref success); + return success; + } + + partial void DetermineSuccessProjSpecific(ref bool success); +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckOrderAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckOrderAction.cs index 6723d3abb..bb8b8e44d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckOrderAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckOrderAction.cs @@ -1,7 +1,15 @@ +using Barotrauma.Extensions; + namespace Barotrauma { class CheckOrderAction : BinaryOptionAction { + public enum OrderPriority + { + Top, + Any + } + [Serialize("", IsPropertySaveable.Yes)] public Identifier TargetTag { get; set; } @@ -14,35 +22,58 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes)] public Identifier OrderTargetTag { get; set; } + [Serialize(OrderPriority.Top, IsPropertySaveable.Yes)] + public OrderPriority Priority { get; set; } + public CheckOrderAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } protected override bool? DetermineSuccess() { - Character targetCharacter = null; - if (!TargetTag.IsEmpty) + var targetCharacters = ParentEvent.GetTargets(TargetTag); + if (targetCharacters.None()) { - foreach (var t in ParentEvent.GetTargets(TargetTag)) - { - if (t is Character c) - { - targetCharacter = c; - break; - } - } - } - if (targetCharacter == null) - { - DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckOrderAction but no valid target character was found for tag \"{TargetTag}\"! This will cause the check to automatically fail."); + DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckOrderAction but no valid target characters were found for tag \"{TargetTag}\"! This will cause the check to automatically fail."); return false; } - var currentOrderInfo = targetCharacter.GetCurrentOrderWithTopPriority(); - if (currentOrderInfo?.Identifier == OrderIdentifier) + foreach (var t in targetCharacters) { - if (!OrderTargetTag.IsEmpty) + if (t is not Character c) { - if (currentOrderInfo.TargetEntity is not Item targetItem || !targetItem.HasTag(OrderTargetTag)) { return false; } + continue; + } + if (Priority == OrderPriority.Top) + { + if (c.GetCurrentOrderWithTopPriority() is Order topPrioOrder && IsMatch(topPrioOrder)) + { + return true; + } + } + else if (Priority == OrderPriority.Any) + { + foreach (var order in c.CurrentOrders) + { + if (IsMatch(order)) + { + return true; + } + } + } + + bool IsMatch(Order order) + { + if (order?.Identifier == OrderIdentifier) + { + if (!OrderTargetTag.IsEmpty && (order.TargetEntity is not Item targetItem || !targetItem.HasTag(OrderTargetTag))) + { + return false; + } + if (OrderOption.IsEmpty || order?.Option == OrderOption) + { + return true; + } + } + return false; } - return OrderOption.IsEmpty || currentOrderInfo?.Option == OrderOption; } return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckPurchasedItemsAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckPurchasedItemsAction.cs new file mode 100644 index 000000000..b0ac35616 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckPurchasedItemsAction.cs @@ -0,0 +1,87 @@ +using System; +using System.Linq; + +namespace Barotrauma; + +class CheckPurchasedItemsAction : BinaryOptionAction +{ + public enum TransactionType + { + Purchased, + Sold + } + + [Serialize(TransactionType.Purchased, IsPropertySaveable.Yes)] + public TransactionType Type { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier ItemIdentifier { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier ItemTag { get; set; } + + [Serialize(1, IsPropertySaveable.Yes)] + public int MinCount { get; set; } + + public CheckPurchasedItemsAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + MinCount = Math.Max(MinCount, 1); + } + + protected override bool? DetermineSuccess() + { + if (ItemIdentifier.IsEmpty && ItemTag.IsEmpty) + { + return false; + } + if (GameMain.GameSession?.Campaign?.CargoManager is not CargoManager cargoManager) + { + return false; + } + if (Type == TransactionType.Purchased) + { + int totalPurchased = 0; + foreach ((Identifier id, var items) in cargoManager.PurchasedItems) + { + if (!ItemIdentifier.IsEmpty) + { + totalPurchased += items.Find(i => i.ItemPrefabIdentifier == ItemIdentifier)?.Quantity ?? 0; + } + else if (!ItemTag.IsEmpty) + { + foreach (var item in items) + { + if (item.ItemPrefab.Tags.Contains(ItemTag)) + { + totalPurchased += item.Quantity; + } + } + } + if (totalPurchased >= MinCount) + { + return true; + } + } + } + else + { + int totalSold = 0; + foreach ((Identifier id, var items) in cargoManager.SoldItems) + { + if (!ItemIdentifier.IsEmpty) + { + totalSold += items.Count(i => i.ItemPrefab.Identifier == ItemIdentifier); + } + else if (!ItemTag.IsEmpty) + { + totalSold += items.Count(i => i.ItemPrefab.Tags.Contains(ItemTag)); + } + if (totalSold >= MinCount) + { + return true; + } + } + } + return false; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs index a438db9a5..1c5c8f0b5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs @@ -59,7 +59,10 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes)] public bool ContinueConversation { get; set; } - public Character speaker + [Serialize(false, IsPropertySaveable.Yes)] + public bool IgnoreInterruptDistance { get; set; } + + public Character Speaker { get; private set; @@ -124,7 +127,7 @@ namespace Barotrauma #else foreach (Client c in GameMain.Server.ConnectedClients) { - if (c.InGame && c.Character != null) { ServerWrite(speaker, c, interrupt); } + if (c.InGame && c.Character != null) { ServerWrite(Speaker, c, interrupt); } } #endif ResetSpeaker(); @@ -160,7 +163,7 @@ namespace Barotrauma selectedOption = -1; interrupt = false; dialogOpened = false; - speaker = null; + Speaker = null; } public override bool SetGoToTarget(string goTo) @@ -181,15 +184,14 @@ namespace Barotrauma private void ResetSpeaker() { - if (speaker == null) { return; } - speaker.CampaignInteractionType = CampaignMode.InteractionType.None; - speaker.ActiveConversation = null; - speaker.SetCustomInteract(null, null); + if (Speaker == null) { return; } + Speaker.CampaignInteractionType = CampaignMode.InteractionType.None; + Speaker.ActiveConversation = null; + Speaker.SetCustomInteract(null, null); #if SERVER - GameMain.NetworkMember.CreateEntityEvent(speaker, new Character.AssignCampaignInteractionEventData()); + GameMain.NetworkMember.CreateEntityEvent(Speaker, new Character.AssignCampaignInteractionEventData()); #endif - var humanAI = speaker.AIController as HumanAIController; - if (humanAI != null && !speaker.IsDead && !speaker.Removed) + if (Speaker.AIController is HumanAIController humanAI && !Speaker.IsDead && !Speaker.Removed) { humanAI.ClearForcedOrder(); if (prevIdleObjective != null) { humanAI.ObjectiveManager.AddObjective(prevIdleObjective); } @@ -207,7 +209,6 @@ namespace Barotrauma public override void Update(float deltaTime) { - lastActiveTime = Timing.TotalTime; if (interrupt) { Interrupted?.Update(deltaTime); @@ -216,6 +217,7 @@ namespace Barotrauma { if (dialogOpened) { + lastActiveTime = Timing.TotalTime; #if CLIENT if (GUIMessageBox.MessageBoxes.Any(mb => mb.UserData as string == "ConversationAction")) { @@ -226,7 +228,7 @@ namespace Barotrauma Reset(); } #endif - if (ShouldInterrupt()) + if (ShouldInterrupt(requireTarget: true)) { ResetSpeaker(); interrupt = true; @@ -236,34 +238,34 @@ namespace Barotrauma if (!SpeakerTag.IsEmpty) { - if (speaker != null && !speaker.Removed && speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk && speaker.ActiveConversation?.ParentEvent != this.ParentEvent) { return; } - speaker = ParentEvent.GetTargets(SpeakerTag).FirstOrDefault(e => e is Character) as Character; - if (speaker == null || speaker.Removed) + if (Speaker != null && !Speaker.Removed && Speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk && Speaker.ActiveConversation?.ParentEvent != this.ParentEvent) { return; } + Speaker = ParentEvent.GetTargets(SpeakerTag).FirstOrDefault(e => e is Character) as Character; + if (Speaker == null || Speaker.Removed) { return; } //some conversation already assigned to the speaker, wait for it to be removed - if (speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk && speaker.ActiveConversation?.ParentEvent != this.ParentEvent) + if (Speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk && Speaker.ActiveConversation?.ParentEvent != this.ParentEvent) { return; } else if (!WaitForInteraction) { - TryStartConversation(speaker); + TryStartConversation(Speaker); } - else if (speaker.ActiveConversation != this) + else if (Speaker.ActiveConversation != this) { - speaker.CampaignInteractionType = CampaignMode.InteractionType.Talk; - speaker.ActiveConversation = this; + Speaker.CampaignInteractionType = CampaignMode.InteractionType.Talk; + Speaker.ActiveConversation = this; #if CLIENT - speaker.SetCustomInteract( + Speaker.SetCustomInteract( TryStartConversation, TextManager.GetWithVariable("CampaignInteraction.Talk", "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Use))); #else - speaker.SetCustomInteract( + Speaker.SetCustomInteract( TryStartConversation, TextManager.Get("CampaignInteraction.Talk")); - GameMain.NetworkMember.CreateEntityEvent(speaker, new Character.AssignCampaignInteractionEventData()); + GameMain.NetworkMember.CreateEntityEvent(Speaker, new Character.AssignCampaignInteractionEventData()); #endif } return; @@ -275,7 +277,9 @@ namespace Barotrauma } else { - if (ShouldInterrupt()) + //after the conversation has been finished and the target character assigned, + //we no longer care if we still have a target + if (ShouldInterrupt(requireTarget: false)) { ResetSpeaker(); interrupt = true; @@ -287,35 +291,36 @@ namespace Barotrauma } } - private bool ShouldInterrupt() + private bool ShouldInterrupt(bool requireTarget) { IEnumerable targets = Enumerable.Empty(); - if (!TargetTag.IsEmpty) + if (!TargetTag.IsEmpty && requireTarget) { - targets = ParentEvent.GetTargets(TargetTag).Where(e => IsValidTarget(e)); + targets = ParentEvent.GetTargets(TargetTag).Where(e => IsValidTarget(e, requireTarget)); if (!targets.Any()) { return true; } } - if (speaker != null) + if (Speaker != null) { - if (!TargetTag.IsEmpty) + if (!TargetTag.IsEmpty && requireTarget && !IgnoreInterruptDistance) { - if (targets.All(t => Vector2.DistanceSquared(t.WorldPosition, speaker.WorldPosition) > InterruptDistance * InterruptDistance)) { return true; } + if (targets.All(t => Vector2.DistanceSquared(t.WorldPosition, Speaker.WorldPosition) > InterruptDistance * InterruptDistance)) { return true; } } - if (speaker.AIController is HumanAIController humanAI && !humanAI.AllowCampaignInteraction()) + if (Speaker.AIController is HumanAIController humanAI && !humanAI.AllowCampaignInteraction()) { return true; } - return speaker.Removed || speaker.IsDead || speaker.IsIncapacitated; + return Speaker.Removed || Speaker.IsDead || Speaker.IsIncapacitated; } return false; } - private bool IsValidTarget(Entity e) + private bool IsValidTarget(Entity e, bool requirePlayerControlled = true) { - bool isValid = e is Character character && !character.Removed && !character.IsDead && !character.IsIncapacitated && - (e == Character.Controlled || character.IsRemotePlayer); + bool isValid = + e is Character character && !character.Removed && !character.IsDead && !character.IsIncapacitated && + (character == Character.Controlled || character.IsRemotePlayer || !requirePlayerControlled); #if SERVER if (!dialogOpened) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MessageBoxAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MessageBoxAction.cs index 2834b8b84..6ac082e66 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MessageBoxAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MessageBoxAction.cs @@ -49,6 +49,12 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes)] public Identifier ObjectiveTag { get; set; } + [Serialize(true, IsPropertySaveable.Yes)] + public bool ObjectiveCanBeCompleted { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier ParentObjectiveId { get; set; } + private bool isFinished = false; public MessageBoxAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs index 1db3cdd56..ee26b9283 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs @@ -67,7 +67,7 @@ namespace Barotrauma void ChangeItemTeam(Submarine sub, bool allowStealing) { - foreach (Item item in npc.Inventory.AllItems) + foreach (Item item in npc.Inventory.FindAllItems(recursive: true)) { item.AllowStealing = allowStealing; if (item.GetComponent() is { } wifiComponent) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs index 77661bdbc..ea2851339 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -1,9 +1,8 @@ -using System; -using Barotrauma.Extensions; +using Barotrauma.Extensions; using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -11,6 +10,7 @@ namespace Barotrauma { public enum SpawnLocationType { + Any, MainSub, Outpost, MainPath, @@ -40,7 +40,7 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes, description: "Tag of an entity with an inventory to spawn the item into.")] public Identifier TargetInventory { get; set; } - [Serialize(SpawnLocationType.MainSub, IsPropertySaveable.Yes)] + [Serialize(SpawnLocationType.Any, IsPropertySaveable.Yes)] public SpawnLocationType SpawnLocation { get; set; } [Serialize(SpawnType.Human, IsPropertySaveable.Yes)] @@ -177,7 +177,7 @@ namespace Barotrauma } else if (!ItemIdentifier.IsEmpty) { - if (!(MapEntityPrefab.FindByIdentifier(ItemIdentifier) is ItemPrefab itemPrefab)) + if (MapEntityPrefab.FindByIdentifier(ItemIdentifier) is not ItemPrefab itemPrefab) { DebugConsole.ThrowError("Error in SpawnAction (item prefab \"" + ItemIdentifier + "\" not found)"); } @@ -275,6 +275,7 @@ namespace Barotrauma { return spawnLocation switch { + SpawnLocationType.Any => true, SpawnLocationType.MainSub => submarine == Submarine.MainSub, SpawnLocationType.MainPath => submarine == null, SpawnLocationType.Outpost => submarine is { Info: { IsOutpost: true } }, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs index 208a7fbfa..89eff4f39 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs @@ -32,11 +32,13 @@ namespace Barotrauma ("bot", v => TagBots(playerCrewOnly: false)), ("crew", v => TagCrew()), ("humanprefabidentifier", TagHumansByIdentifier), + ("jobidentifier", TagHumansByJobIdentifier), ("structureidentifier", TagStructuresByIdentifier), ("structurespecialtag", TagStructuresBySpecialTag), ("itemidentifier", TagItemsByIdentifier), ("itemtag", TagItemsByTag), - ("hullname", TagHullsByName) + ("hullname", TagHullsByName), + ("submarine", TagSubmarinesByType), }.Select(t => (t.k.ToIdentifier(), t.v)).ToImmutableDictionary(); } @@ -93,6 +95,18 @@ namespace Barotrauma } } } + + private void TagHumansByJobIdentifier(Identifier jobIdentifier) + { + foreach (Character c in Character.CharacterList) + { + if (c.HasJob(jobIdentifier)) + { + ParentEvent.AddTarget(Tag, c); + } + } + } + private void TagStructuresByIdentifier(Identifier identifier) { ParentEvent.AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.Prefab.Identifier == identifier); @@ -118,6 +132,11 @@ namespace Barotrauma ParentEvent.AddTargetPredicate(Tag, e => e is Hull h && SubmarineTypeMatches(h.Submarine) && h.RoomName.Contains(name.Value, StringComparison.OrdinalIgnoreCase)); } + private void TagSubmarinesByType(Identifier type) + { + ParentEvent.AddTargetPredicate(Tag, e => e is Submarine s && SubmarineTypeMatches(s) && (type.IsEmpty || type == s.Info?.Type.ToIdentifier())); + } + private bool SubmarineTypeMatches(Submarine sub) { if (SubmarineType == SubType.Any) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs index 385dc13ae..8f7a849ac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs @@ -8,6 +8,12 @@ namespace Barotrauma { class TriggerAction : EventAction { + public enum TriggerType + { + Inside, + Outside + } + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the first entity that will be used for trigger checks.")] public Identifier Target1Tag { get; set; } @@ -23,7 +29,10 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the second entity when the trigger check succeeds.")] public Identifier ApplyToTarget2 { get; set; } - [Serialize(0.0f, IsPropertySaveable.Yes, description: "Range both entities must be within to activate the trigger.")] + [Serialize(TriggerType.Inside, IsPropertySaveable.Yes, description: "Determines if the targets must be inside or outside of the radius.")] + public TriggerType Type { get; set; } + + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Range to activate the trigger.")] public float Radius { get; set; } [Serialize(true, IsPropertySaveable.Yes, description: "If true, characters who are being targeted by some enemy cannot trigger the action.")] @@ -38,6 +47,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes, description: "If true, the action can be triggered by interacting with any matching target (not just the 1st one).")] public bool AllowMultipleTargets { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "If true and using multiple targets, all targets must be inside/outside the radius.")] + public bool CheckAllTargets { get; set; } + private float distance; public TriggerAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } @@ -57,6 +69,8 @@ namespace Barotrauma public bool isRunning = false; private readonly List> npcsOrItems = new List>(); + + private readonly List<(Entity e1, Entity e2)> triggerers = new List<(Entity e1, Entity e2)>(); public override void Update(float deltaTime) { @@ -66,18 +80,44 @@ namespace Barotrauma var targets1 = ParentEvent.GetTargets(Target1Tag); if (!targets1.Any()) { return; } - + + triggerers.Clear(); foreach (Entity e1 in targets1) { - if (DisableInCombat && IsInCombat(e1)) { continue; } - if (DisableIfTargetIncapacitated && e1 is Character character1 && (character1.IsDead || character1.IsIncapacitated)) { continue; } + if (DisableInCombat && IsInCombat(e1)) + { + if (CheckAllTargets) + { + return; + } + continue; + } + if (DisableIfTargetIncapacitated && e1 is Character character1 && (character1.IsDead || character1.IsIncapacitated)) + { + if (CheckAllTargets) + { + return; + } + continue; + } if (!TargetModuleType.IsEmpty) { - if (IsCloseEnoughToHull(e1, out Hull hull)) + if (!CheckAllTargets && CheckDistanceToHull(e1, out Hull hull)) { Trigger(e1, hull); return; } + else if (CheckAllTargets) + { + if (CheckDistanceToHull(e1, out hull)) + { + triggerers.Add((e1, hull)); + } + else + { + return; + } + } continue; } @@ -85,9 +125,26 @@ namespace Barotrauma foreach (Entity e2 in targets2) { - if (e1 == e2) { continue; } - if (DisableInCombat && IsInCombat(e2)) { continue; } - if (DisableIfTargetIncapacitated && e2 is Character character2 && (character2.IsDead || character2.IsIncapacitated)) { continue; } + if (e1 == e2) + { + continue; + } + if (DisableInCombat && IsInCombat(e2)) + { + if (CheckAllTargets) + { + return; + } + continue; + } + if (DisableIfTargetIncapacitated && e2 is Character character2 && (character2.IsDead || character2.IsIncapacitated)) + { + if (CheckAllTargets) + { + return; + } + continue; + } if (WaitForInteraction) { @@ -173,16 +230,35 @@ namespace Barotrauma Vector2 pos1 = e1.WorldPosition; Vector2 pos2 = e2.WorldPosition; distance = Vector2.Distance(pos1, pos2); - if (((e1 is MapEntity m1) && Submarine.RectContains(m1.WorldRect, pos2)) || - ((e2 is MapEntity m2) && Submarine.RectContains(m2.WorldRect, pos1)) || - Vector2.DistanceSquared(pos1, pos2) < Radius * Radius) + if ((Type == TriggerType.Inside) == IsWithinRadius()) + { + if (!CheckAllTargets) + { + Trigger(e1, e2); + return; + } + else + { + triggerers.Add((e1, e2)); + } + } + else if (CheckAllTargets) { - Trigger(e1, e2); return; } + + bool IsWithinRadius() => + ((e1 is MapEntity m1) && Submarine.RectContains(m1.WorldRect, pos2)) || + ((e2 is MapEntity m2) && Submarine.RectContains(m2.WorldRect, pos1)) || + Vector2.DistanceSquared(pos1, pos2) < Radius * Radius; } } - } + } + + foreach (var (e1, e2) in triggerers) + { + Trigger(e1, e2); + } } private void ResetTargetIcons() @@ -205,7 +281,7 @@ namespace Barotrauma } } - private bool IsCloseEnoughToHull(Entity e, out Hull hull) + private bool CheckDistanceToHull(Entity e, out Hull hull) { hull = null; if (Radius <= 0) @@ -213,36 +289,35 @@ namespace Barotrauma if (e is Character character && character.CurrentHull != null && character.CurrentHull.OutpostModuleTags.Contains(TargetModuleType)) { hull = character.CurrentHull; - return true; + return Type == TriggerType.Inside; } else if (e is Item item && item.CurrentHull != null && item.CurrentHull.OutpostModuleTags.Contains(TargetModuleType)) { hull = item.CurrentHull; - return true; + return Type == TriggerType.Inside; } - return false; + return Type == TriggerType.Outside; } else { foreach (Hull potentialHull in Hull.HullList) { if (!potentialHull.OutpostModuleTags.Contains(TargetModuleType)) { continue; } - Rectangle hullRect = potentialHull.WorldRect; hullRect.Inflate(Radius, Radius); if (Submarine.RectContains(hullRect, e.WorldPosition)) { hull = potentialHull; - return true; + return Type == TriggerType.Inside; } } - return false; + return Type == TriggerType.Outside; } } - private bool IsInCombat(Entity entity) + private static bool IsInCombat(Entity entity) { - if (!(entity is Character character)) { return false; } + if (entity is not Character character) { return false; } foreach (Character c in Character.CharacterList) { if (c.IsDead || c.Removed || c.IsIncapacitated || !c.Enabled) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialSegmentAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialSegmentAction.cs index 587fb20a7..f2128f82e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialSegmentAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialSegmentAction.cs @@ -13,6 +13,12 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes)] public Identifier ObjectiveTag { get; set; } + [Serialize(true, IsPropertySaveable.Yes)] + public bool CanBeCompleted { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier ParentObjectiveId { get; set; } + [Serialize(false, IsPropertySaveable.Yes)] public bool AutoPlayVideo { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index 8ecedce89..481a4a40a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -73,6 +73,7 @@ namespace Barotrauma private readonly HashSet finishedEvents = new HashSet(); private readonly HashSet nonRepeatableEvents = new HashSet(); + private readonly HashSet usedUniqueSets = new HashSet(); #if DEBUG && SERVER @@ -155,12 +156,19 @@ namespace Barotrauma } rand = new MTRandom(seed); - EventSet initialEventSet = SelectRandomEvents(EventSet.Prefabs.ToList(), requireCampaignSet: GameMain.GameSession?.GameMode is CampaignMode, rand); + bool playingCampaign = GameMain.GameSession?.GameMode is CampaignMode; + EventSet initialEventSet = SelectRandomEvents( + EventSet.Prefabs.ToList(), + requireCampaignSet: playingCampaign, + random: rand); EventSet additiveSet = null; if (initialEventSet != null && initialEventSet.Additive) { additiveSet = initialEventSet; - initialEventSet = SelectRandomEvents(EventSet.Prefabs.Where(e => !e.Additive).ToList(), requireCampaignSet: GameMain.GameSession?.GameMode is CampaignMode, rand); + initialEventSet = SelectRandomEvents( + EventSet.Prefabs.Where(e => !e.Additive).ToList(), + requireCampaignSet: playingCampaign, + random: rand); } if (initialEventSet != null) { @@ -201,6 +209,7 @@ namespace Barotrauma } AddChildEvents(initialEventSet); + void AddChildEvents(EventSet eventSet) { if (eventSet == null) { return; } @@ -351,6 +360,7 @@ namespace Barotrauma QueuedEvents.Clear(); finishedEvents.Clear(); nonRepeatableEvents.Clear(); + usedUniqueSets.Clear(); preloadedSprites.ForEach(s => s.Remove()); preloadedSprites.Clear(); @@ -364,15 +374,25 @@ namespace Barotrauma /// public void RegisterEventHistory() { - level.LevelData.EventsExhausted = true; - if (level?.LevelData != null && level.LevelData.Type == LevelData.LevelType.Outpost) + if (level?.LevelData != null) { - level.LevelData.EventHistory.AddRange(selectedEvents.Values.SelectMany(v => v).Select(e => e.Prefab).Where(e => !level.LevelData.EventHistory.Contains(e))); - if (level.LevelData.EventHistory.Count > MaxEventHistory) + level.LevelData.EventsExhausted = true; + if (level.LevelData.Type == LevelData.LevelType.Outpost) { - level.LevelData.EventHistory.RemoveRange(0, level.LevelData.EventHistory.Count - MaxEventHistory); + level.LevelData.EventHistory.AddRange(selectedEvents.Values.SelectMany(v => v).Select(e => e.Prefab).Where(e => !level.LevelData.EventHistory.Contains(e))); + if (level.LevelData.EventHistory.Count > MaxEventHistory) + { + level.LevelData.EventHistory.RemoveRange(0, level.LevelData.EventHistory.Count - MaxEventHistory); + } + level.LevelData.NonRepeatableEvents.AddRange(nonRepeatableEvents.Where(e => !level.LevelData.NonRepeatableEvents.Contains(e))); + } + foreach (var usedUniqueSet in usedUniqueSets) + { + if (!level.LevelData.UsedUniqueSets.Contains(usedUniqueSet.Identifier)) + { + level.LevelData.UsedUniqueSets.Add(usedUniqueSet.Identifier); + } } - level.LevelData.NonRepeatableEvents.AddRange(nonRepeatableEvents.Where(e => !level.LevelData.NonRepeatableEvents.Contains(e))); } } @@ -398,6 +418,11 @@ namespace Barotrauma DebugConsole.NewMessage($"Loading event set {eventSet.Identifier}", Color.LightBlue, debugOnly: true); + if (eventSet.Unique && !usedUniqueSets.Contains(eventSet)) + { + usedUniqueSets.Add(eventSet); + } + int applyCount = 1; List> spawnPosFilter = new List>(); if (eventSet.PerRuin) @@ -496,12 +521,12 @@ namespace Barotrauma selectedEvents[eventSet].Add(newEvent); } - Location location = (GameMain.GameSession?.GameMode as CampaignMode)?.Map?.CurrentLocation ?? level?.StartLocation; + var location = GetEventLocation(); foreach (EventSet childEventSet in eventSet.ChildSets) { if (!IsValidForLevel(childEventSet, level)) { continue; } - if (location != null && !IsValidForLocation(childEventSet, location)) { continue; } - CreateEvents(childEventSet); + if (!IsValidForLocation(childEventSet, location)) { continue; } + CreateEvents(childEventSet); } } } @@ -536,10 +561,32 @@ namespace Barotrauma } } - Location location = (GameMain.GameSession?.GameMode as CampaignMode)?.Map?.CurrentLocation ?? level?.StartLocation; - if (location != null) + var location = GetEventLocation(); + allowedEventSets = allowedEventSets.Where(set => IsValidForLocation(set, location)); + + allowedEventSets = allowedEventSets.Where(set => !set.CampaignTutorialOnly || + (GameMain.IsSingleplayer && GameMain.GameSession?.Campaign?.Settings is { TutorialEnabled: true })); + + int? discoveryIndex = GameMain.GameSession?.Map?.GetDiscoveryIndex(location); + int? visitIndex = GameMain.GameSession?.Map?.GetVisitIndex(location); + if (discoveryIndex is not null && discoveryIndex >= 0 && allowedEventSets.Any(set => set.ForceAtDiscoveredNr == discoveryIndex)) { - allowedEventSets = allowedEventSets.Where(set => IsValidForLocation(set, location)); + allowedEventSets = allowedEventSets.Where(set => set.ForceAtDiscoveredNr == discoveryIndex); + } + else if (visitIndex is not null && visitIndex >= 0 && allowedEventSets.Any(set => set.ForceAtVisitedNr == visitIndex)) + { + allowedEventSets = allowedEventSets.Where(set => set.ForceAtVisitedNr == visitIndex); + } + else + { + // When there are no forced sets, only allow sets that aren't forced at any specific location + allowedEventSets = allowedEventSets.Where(set => set.ForceAtDiscoveredNr < 0 && set.ForceAtVisitedNr < 0); + } + + if (allowedEventSets.Count() == 1) + { + // When there's only a single set available, just select it directly + return allowedEventSets.First(); } float totalCommonness = allowedEventSets.Sum(e => e.GetCommonness(level)); @@ -558,18 +605,27 @@ namespace Barotrauma return null; } - private bool IsValidForLevel(EventSet eventSet, Level level) + private static bool IsValidForLevel(EventSet eventSet, Level level) { return level.Difficulty >= eventSet.MinLevelDifficulty && level.Difficulty <= eventSet.MaxLevelDifficulty && level.LevelData.Type == eventSet.LevelType && - (eventSet.BiomeIdentifier.IsEmpty || eventSet.BiomeIdentifier == level.LevelData.Biome.Identifier); + (eventSet.BiomeIdentifier.IsEmpty || eventSet.BiomeIdentifier == level.LevelData.Biome.Identifier) && + (!eventSet.Unique || !level.LevelData.UsedUniqueSets.Contains(eventSet.Identifier)); } private bool IsValidForLocation(EventSet eventSet, Location location) { - return eventSet.LocationTypeIdentifiers == null || - eventSet.LocationTypeIdentifiers.Any(identifier => identifier == location.GetLocationType().Identifier); + if (location is null) { return true; } + var locationType = location.GetLocationType(); + bool includeGenericEvents = level.Type == LevelData.LevelType.LocationConnection || !locationType.IgnoreGenericEvents; + if (includeGenericEvents && eventSet.LocationTypeIdentifiers == null) { return true; } + return eventSet.LocationTypeIdentifiers != null && eventSet.LocationTypeIdentifiers.Any(identifier => identifier == locationType.Identifier); + } + + private Location GetEventLocation() + { + return GameMain.GameSession?.Campaign?.Map?.CurrentLocation ?? level?.StartLocation; } private bool CanStartEventSet(EventSet eventSet) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index cd6a3cae6..bb8bbd1aa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -113,8 +113,17 @@ namespace Barotrauma public readonly bool PerRuin, PerCave, PerWreck; public readonly bool DisableInHuntingGrounds; + /// + /// If true, events from this set shouldn't be selected again as long as they remain in which has a limited size. + /// Use to prevent selecting the whole set again altogether. + /// public readonly bool OncePerOutpost; + /// + /// If true, the whole set can only be selected once for a level. + /// + public readonly bool Unique; + public readonly bool DelayWhenCrewAway; public readonly bool TriggerEventCooldown; @@ -126,6 +135,18 @@ namespace Barotrauma public readonly float ResetTime; + /// + /// Used to force an event set based on how many other locations have been discovered before this. (Used for campaign tutorial event sets.) + /// + public readonly int ForceAtDiscoveredNr; + + /// + /// Used to force an event set based on how many other outposts have been visited before this. (Used for campaign tutorial event sets.) + /// + public readonly int ForceAtVisitedNr; + + public readonly bool CampaignTutorialOnly; + public readonly struct SubEventPrefab { public SubEventPrefab(Either prefabOrIdentifiers, float? commonness, float? probability) @@ -269,9 +290,18 @@ namespace Barotrauma IgnoreCoolDown = element.GetAttributeBool("ignorecooldown", parentSet?.IgnoreCoolDown ?? (PerRuin || PerCave || PerWreck)); DelayWhenCrewAway = element.GetAttributeBool("delaywhencrewaway", !PerRuin && !PerCave && !PerWreck); OncePerOutpost = element.GetAttributeBool("onceperoutpost", false); + Unique = element.GetAttributeBool("unique", false); TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true); IsCampaignSet = element.GetAttributeBool("campaign", LevelType == LevelData.LevelType.Outpost || (parentSet?.IsCampaignSet ?? false)); ResetTime = element.GetAttributeFloat("resettime", 0); + CampaignTutorialOnly = element.GetAttributeBool(nameof(CampaignTutorialOnly), false); + + ForceAtDiscoveredNr = element.GetAttributeInt(nameof(ForceAtDiscoveredNr), -1); + ForceAtVisitedNr = element.GetAttributeInt(nameof(ForceAtVisitedNr), -1); + if (ForceAtDiscoveredNr >= 0 && ForceAtVisitedNr >= 0) + { + DebugConsole.ThrowError($"Error with event set \"{Identifier}\" - both ForceAtDiscoveredNr and ForceAtVisitedNr are defined, this could lead to unexpected behavior"); + } DefaultCommonness = element.GetAttributeFloat("commonness", 1.0f); foreach (var subElement in element.Elements()) @@ -489,6 +519,11 @@ namespace Barotrauma } } + public override string ToString() + { + return $"{base.ToString()} ({Identifier.Value})"; + } + public override void Dispose() { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index f203441b6..e826d0897 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -378,7 +378,7 @@ namespace Barotrauma IEnumerable crewCharacters = GameSession.GetSessionCrewCharacters(CharacterType.Both); // use multipliers here so that we can easily add them together without introducing multiplicative XP stacking - var experienceGainMultiplier = new AbilityExperienceGainMultiplier(1f); + var experienceGainMultiplier = new AbilityMissionExperienceGainMultiplier(this, 1f); crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnAllyGainMissionExperience, experienceGainMultiplier)); crewCharacters.ForEach(c => experienceGainMultiplier.Value += c.GetStatValue(StatTypes.MissionExperienceGainMultiplier)); @@ -386,13 +386,20 @@ namespace Barotrauma #if CLIENT foreach (Character character in crewCharacters) { + var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f); + character.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual); character.Info?.GiveExperience(experienceGain, isMissionExperience: true); } #else foreach (Barotrauma.Networking.Client c in GameMain.Server.ConnectedClients) { //give the experience to the stored characterinfo if the client isn't currently controlling a character - (c.Character?.Info ?? c.CharacterInfo)?.GiveExperience(experienceGain, isMissionExperience: true); + CharacterInfo info = c.Character?.Info ?? c.CharacterInfo; + + var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f); + info?.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual); + + info?.GiveExperience((int)(experienceGain * experienceGainMultiplier.Value), isMissionExperience: true); } #endif @@ -619,4 +626,16 @@ namespace Barotrauma public Mission Mission { get; set; } } + class AbilityMissionExperienceGainMultiplier : AbilityObject, IAbilityValue, IAbilityMission + { + public AbilityMissionExperienceGainMultiplier(Mission mission, float missionExperienceGainMultiplier) + { + Value = missionExperienceGainMultiplier; + Mission = mission; + } + + public float Value { get; set; } + public Mission Mission { get; set; } + } + } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs index 9ea8ff2a0..4efce93a4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs @@ -218,6 +218,8 @@ namespace Barotrauma.Extensions return new Dictionary(immutableDictionary); } + public static NetCollection ToNetCollection(this IEnumerable enumerable) => new NetCollection(enumerable.ToImmutableArray()); + /// /// Returns whether a given collection has at least a certain amount /// of elements for which the predicate returns true. diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs index 76ba0fc8b..32dc12d72 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs @@ -1,10 +1,15 @@ #nullable enable using Microsoft.Xna.Framework; using System; -using System.Linq; namespace Barotrauma { + public enum FactionAffiliation + { + Affiliated, + Neutral + } + class Faction { public Reputation Reputation { get; } @@ -16,11 +21,25 @@ namespace Barotrauma Reputation = new Reputation(metadata, this, prefab.MinReputation, prefab.MaxReputation, prefab.InitialReputation); } - public bool IsAffiliated() + /// + /// Get what kind of affiliation this faction has towards the player depending on who they chose to side with via talents + /// + /// + public FactionAffiliation GetPlayerAffiliationStatus() { - if (GameMain.GameSession?.Campaign?.Factions.MaxBy(static f => f.Reputation.Value) is not { } highestFaction) { return false; } + float affiliation = 1f; + foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) + { + if (character.Info is not { } info) { continue; } - return highestFaction.Reputation.Value < 0 || Prefab.Identifier == highestFaction.Prefab.Identifier; + affiliation *= 1f + info.GetSavedStatValue(StatTypes.Affiliation, Prefab.Identifier); + } + + return affiliation switch + { + >= 1f => FactionAffiliation.Affiliated, + _ => FactionAffiliation.Neutral + }; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 4409e74f7..dfe0205af 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -749,6 +749,7 @@ namespace Barotrauma location.LevelData = new LevelData(location, location.Biome.AdjustedMaxDifficulty); location.Reset(); } + Map.ClearLocationHistory(); Map.SetLocation(Map.Locations.IndexOf(Map.StartLocation)); Map.SelectLocation(-1); if (Map.Radiation != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs index 15c864bd7..791d852b4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs @@ -18,6 +18,9 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes)] public string PresetName { get; set; } = string.Empty; + [Serialize(true, IsPropertySaveable.Yes)] + public bool TutorialEnabled { get; set; } + [Serialize(false, IsPropertySaveable.Yes), NetworkSerialize] public bool RadiationEnabled { get; set; } @@ -104,7 +107,9 @@ namespace Barotrauma private static int GetAddedMissionCount() { - return GameSession.GetSessionCrewCharacters(CharacterType.Both).Max(static character => (int)character.GetStatValue(StatTypes.ExtraMissionCount)); + var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); + if (!characters.Any()) { return 0; } + return characters.Max(static character => (int)character.GetStatValue(StatTypes.ExtraMissionCount)); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index 9e1e19aa6..a84b1a7c5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -133,13 +133,13 @@ namespace Barotrauma } partial void InitProjSpecific(); - + public static string GetCharacterDataSavePath(string savePath) { - return Path.Combine(SaveUtil.MultiplayerSaveFolder, Path.GetFileNameWithoutExtension(savePath) + "_CharacterData.xml"); + return Path.Combine(Path.GetDirectoryName(savePath), Path.GetFileNameWithoutExtension(savePath) + "_CharacterData.xml"); } - public string GetCharacterDataSavePath() + public static string GetCharacterDataSavePath() { return GetCharacterDataSavePath(GameMain.GameSession.SavePath); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/Tutorials/TutorialPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/Tutorials/TutorialPrefab.cs index a9942470b..bb9438b28 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/Tutorials/TutorialPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/Tutorials/TutorialPrefab.cs @@ -29,6 +29,14 @@ namespace Barotrauma public readonly Sprite Banner; + public readonly EndMessageInfo EndMessage; + + public enum EndType { None, Continue, Restart } + + public readonly record struct EndMessageInfo( + EndType EndType, + Identifier NextTutorialIdentifier); + public TutorialPrefab(ContentFile file, ContentXElement element) : base(file, element.GetAttributeIdentifier("identifier", "")) { Order = element.GetAttributeInt("order", int.MaxValue); @@ -59,6 +67,13 @@ namespace Barotrauma } EventIdentifier = element.GetChildElement("scriptedevent")?.GetAttributeIdentifier("identifier", "") ?? Identifier.Empty; + + if (element.GetChildElement("endmessage") is ContentXElement endMessageElement) + { + EndMessage = new EndMessageInfo( + EndType: endMessageElement.GetAttributeEnum("type", EndType.None), + NextTutorialIdentifier: endMessageElement.GetAttributeIdentifier("nexttutorial", Identifier.Empty)); + } } public CharacterInfo GetTutorialCharacterInfo() diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 26bb6fc58..75e39a27f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -582,6 +582,9 @@ namespace Barotrauma } } +#if CLIENT + ObjectiveManager.ResetObjectives(); +#endif EventManager?.StartRound(Level.Loaded); SteamAchievementManager.OnStartRound(); @@ -847,6 +850,7 @@ namespace Barotrauma if (GameMain.NetLobbyScreen != null) { GameMain.NetLobbyScreen.OnRoundEnded(); } TabMenu.OnRoundEnded(); GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData as string == "ConversationAction" || ReadyCheck.IsReadyCheck(mb)); + ObjectiveManager.ResetUI(); #endif SteamAchievementManager.OnRoundEnded(this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs index 91b63855f..2d25da2dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs @@ -9,7 +9,7 @@ using Barotrauma.Networking; namespace Barotrauma { - internal partial class MedicalClinic + internal sealed partial class MedicalClinic { public enum NetworkHeader { @@ -18,7 +18,8 @@ namespace Barotrauma ADD_PENDING, REMOVE_PENDING, CLEAR_PENDING, - HEAL_PENDING + HEAL_PENDING, + ADD_EVERYTHING_TO_PENDING } public enum AfflictionSeverity @@ -43,23 +44,10 @@ namespace Barotrauma } [NetworkSerialize] - public struct NetHealRequest : INetSerializableStruct - { - public HealRequestResult Result; - } + public readonly record struct NetHealRequest(HealRequestResult Result) : INetSerializableStruct; [NetworkSerialize] - public struct NetRemovedAffliction : INetSerializableStruct - { - public NetCrewMember CrewMember; - public NetAffliction Affliction; - } - - public struct NetPendingCrew : INetSerializableStruct - { - [NetworkSerialize(ArrayMaxSize = CrewManager.MaxCrewSize)] - public NetCrewMember[] CrewMembers; - } + public readonly record struct NetRemovedAffliction(NetCrewMember CrewMember, NetAffliction Affliction) : INetSerializableStruct; public struct NetAffliction : INetSerializableStruct { @@ -87,7 +75,7 @@ namespace Barotrauma } // between 0.1 and 0.5 - if (normalizedStrength > 0.1f && normalizedStrength < 0.5f) + if (normalizedStrength is > 0.1f and < 0.5f) { return AfflictionSeverity.Medium; } @@ -146,17 +134,23 @@ namespace Barotrauma } } - public struct NetCrewMember : INetSerializableStruct + public record struct NetCrewMember : INetSerializableStruct { [NetworkSerialize] public int CharacterInfoID; [NetworkSerialize] - public NetAffliction[] Afflictions; + public ImmutableArray Afflictions; - public CharacterInfo CharacterInfo + public NetCrewMember(CharacterInfo info) { - set => CharacterInfoID = value.GetIdentifierUsingOriginalName(); + CharacterInfoID = info.GetIdentifierUsingOriginalName(); + Afflictions = ImmutableArray.Empty; + } + + public NetCrewMember(CharacterInfo info, ImmutableArray afflictions): this(info) + { + Afflictions = afflictions; } public readonly CharacterInfo? FindCharacterInfo(ImmutableArray crew) @@ -194,11 +188,11 @@ namespace Barotrauma private static bool IsOutpostInCombat() { - if (!(Level.Loaded is { Type: LevelData.LevelType.Outpost })) { return false; } + if (Level.Loaded is not { Type: LevelData.LevelType.Outpost }) { return false; } - IEnumerable crew = GetCrewCharacters().Where(c => c.Character != null).Select(c => c.Character).ToImmutableHashSet(); + IEnumerable crew = GetCrewCharacters().Where(static c => c.Character != null).Select(static c => c.Character).ToImmutableHashSet(); - foreach (Character npc in Character.CharacterList.Where(c => c.TeamID == CharacterTeamType.FriendlyNPC)) + foreach (Character npc in Character.CharacterList.Where(static 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; } @@ -238,6 +232,20 @@ namespace Barotrauma PendingHeals.Clear(); } + private void AddEverythingToPending() + { + foreach (CharacterInfo info in GetCrewCharacters()) + { + if (info.Character?.CharacterHealth is not { } health) { continue; } + + var afflictions = GetAllAfflictions(health); + + if (afflictions.Length is 0) { continue; } + + InsertPendingCrewMember(new NetCrewMember(info, afflictions)); + } + } + private void RemovePendingAffliction(NetCrewMember crewMember, NetAffliction affliction) { foreach (NetCrewMember listMember in PendingHeals.ToList()) @@ -255,7 +263,7 @@ namespace Barotrauma newAfflictions.Add(pendingAffliction); } - pendingMember.Afflictions = newAfflictions.ToArray(); + pendingMember.Afflictions = newAfflictions.ToImmutableArray(); } if (!pendingMember.Afflictions.Any()) { continue; } @@ -280,9 +288,9 @@ namespace Barotrauma static float GetShowTreshold(Affliction affliction) => Math.Max(0, Math.Min(affliction.Prefab.ShowIconToOthersThreshold, affliction.Prefab.ShowInHealthScannerThreshold)); } - private NetAffliction[] GetAllAfflictions(CharacterHealth health) + private ImmutableArray GetAllAfflictions(CharacterHealth health) { - IEnumerable rawAfflictions = health.GetAllAfflictions().Where(a => IsHealable(a)); + IEnumerable rawAfflictions = health.GetAllAfflictions().Where(IsHealable); List afflictions = new List(); @@ -305,12 +313,12 @@ namespace Barotrauma afflictions.Add(newAffliction); } - return afflictions.ToArray(); + return afflictions.ToImmutableArray(); 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); + public int GetTotalCost() => PendingHeals.SelectMany(static h => h.Afflictions).Aggregate(0, static (current, affliction) => current + affliction.Price); private int GetAdjustedPrice(int price) => campaign?.Map?.CurrentLocation is { Type: { HasOutpost: true } } currentLocation ? currentLocation.GetAdjustedHealCost(price) : int.MaxValue; @@ -325,7 +333,7 @@ namespace Barotrauma } #endif - return Character.CharacterList.Where(c => c.Info != null && c.TeamID == CharacterTeamType.Team1).Select(c => c.Info).ToImmutableArray(); + return Character.CharacterList.Where(static c => c.Info != null && c.TeamID == CharacterTeamType.Team1).Select(static c => c.Info).ToImmutableArray(); } #if DEBUG && CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index a660bbbf3..6872f409c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -493,6 +493,10 @@ namespace Barotrauma base.PutItem(item, i, user, removeItem, createNetworkEvent); #if CLIENT CreateSlots(); + if (character == Character.Controlled) + { + HintManager.OnObtainedItem(character, item); + } #endif if (item.CampaignInteractionType == CampaignMode.InteractionType.Cargo) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index aa5dc7d28..9572b9953 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -843,10 +843,18 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { if (item.body == null || !item.body.Enabled) { return; } + + Character owner = picker ?? item.GetRootInventoryOwner() as Character; + + if (owner != null) + { + ApplyStatusEffects(ActionType.OnActive, deltaTime, owner); + } + if (picker == null || !picker.HasEquippedItem(item)) { if (Pusher != null) { Pusher.Enabled = false; } - if (attachTargetCell == null) { IsActive = false; } + if (attachTargetCell == null && owner == null) { IsActive = false; } return; } @@ -855,23 +863,7 @@ namespace Barotrauma.Items.Components Drawable = true; } - Vector2 swing = Vector2.Zero; - if (swingAmount != Vector2.Zero && !picker.IsUnconscious && picker.Stun <= 0.0f) - { - swingState += deltaTime; - swingState %= 1.0f; - if (SwingWhenHolding || - (SwingWhenAiming && picker.IsKeyDown(InputType.Aim)) || - (SwingWhenUsing && picker.IsKeyDown(InputType.Aim) && picker.IsKeyDown(InputType.Shoot))) - { - swing = swingAmount * new Vector2( - PerlinNoise.GetPerlin(swingState * SwingSpeed * 0.1f, swingState * SwingSpeed * 0.1f) - 0.5f, - PerlinNoise.GetPerlin(swingState * SwingSpeed * 0.1f + 0.5f, swingState * SwingSpeed * 0.1f + 0.5f) - 0.5f); - } - } - - ApplyStatusEffects(ActionType.OnActive, deltaTime, picker); - + UpdateSwingPos(deltaTime, out Vector2 swingPos); if (item.body.Dir != picker.AnimController.Dir) { item.FlipX(relativeToSub: false); @@ -884,7 +876,7 @@ namespace Barotrauma.Items.Components scaledHandlePos[0] = handlePos[0] * item.Scale; scaledHandlePos[1] = handlePos[1] * item.Scale; bool aim = picker.IsKeyDown(InputType.Aim) && aimPos != Vector2.Zero && picker.CanAim; - picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swing, aimPos + swing, aim, holdAngle); + picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swingPos, aimPos + swingPos, aim, holdAngle); if (!aim) { var rope = GetRope(); @@ -921,6 +913,24 @@ namespace Barotrauma.Items.Components } } + public void UpdateSwingPos(float deltaTime, out Vector2 swingPos) + { + swingPos = Vector2.Zero; + if (swingAmount != Vector2.Zero && !picker.IsUnconscious && picker.Stun <= 0.0f) + { + swingState += deltaTime; + swingState %= 1.0f; + if (SwingWhenHolding || + (SwingWhenAiming && picker.IsKeyDown(InputType.Aim)) || + (SwingWhenUsing && picker.IsKeyDown(InputType.Aim) && picker.IsKeyDown(InputType.Shoot))) + { + swingPos = swingAmount * new Vector2( + PerlinNoise.GetPerlin(swingState * SwingSpeed * 0.1f, swingState * SwingSpeed * 0.1f) - 0.5f, + PerlinNoise.GetPerlin(swingState * SwingSpeed * 0.1f + 0.5f, swingState * SwingSpeed * 0.1f + 0.5f) - 0.5f); + } + } + } + public override void ReceiveSignal(Signal signal, Connection connection) { //do nothing diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index f0b500c43..b1254de84 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -214,8 +214,9 @@ namespace Barotrauma.Items.Components bool aim = item.RequireAimToUse && picker.AllowInput && picker.IsKeyDown(InputType.Aim) && reloadTimer <= 0 && picker.CanAim; if (aim) { + UpdateSwingPos(deltaTime, out Vector2 swingPos); hitPos = MathUtils.WrapAnglePi(Math.Min(hitPos + deltaTime * 3f, MathHelper.PiOver4)); - ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos, aimMelee: true); + ac.HoldItem(deltaTime, item, handlePos, aimPos + swingPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos, aimMelee: true); if (ac.InWater) { ac.LockFlippingUntil = (float)Timing.TotalTime + Reload; @@ -392,34 +393,35 @@ namespace Barotrauma.Items.Components float damageMultiplier = 1 + User.GetStatValue(StatTypes.MeleeAttackMultiplier); damageMultiplier *= 1.0f + item.GetQualityModifier(Quality.StatType.StrikingPowerMultiplier); + Character user = User; Limb targetLimb = target.UserData as Limb; Character targetCharacter = targetLimb?.character ?? target.UserData as Character; if (Attack != null) { - Attack.SetUser(User); + Attack.SetUser(user); Attack.DamageMultiplier = damageMultiplier; if (targetLimb != null) { if (targetLimb.character.Removed) { return; } targetLimb.character.LastDamageSource = item; - Attack.DoDamageToLimb(User, targetLimb, item.WorldPosition, 1.0f); + Attack.DoDamageToLimb(user, targetLimb, item.WorldPosition, 1.0f); } else if (targetCharacter != null) { if (targetCharacter.Removed) { return; } targetCharacter.LastDamageSource = item; - Attack.DoDamage(User, targetCharacter, item.WorldPosition, 1.0f); + Attack.DoDamage(user, targetCharacter, item.WorldPosition, 1.0f); } else if ((target.UserData as Structure ?? targetFixture.UserData as Structure) is Structure targetStructure) { if (targetStructure.Removed) { return; } - Attack.DoDamage(User, targetStructure, item.WorldPosition, 1.0f); + Attack.DoDamage(user, targetStructure, item.WorldPosition, 1.0f); } else if (target.UserData is Item targetItem && targetItem.Prefab.DamagedByMeleeWeapons && targetItem.Condition > 0) { if (targetItem.Removed) { return; } - var attackResult = Attack.DoDamage(User, targetItem, item.WorldPosition, 1.0f); + var attackResult = Attack.DoDamage(user, targetItem, item.WorldPosition, 1.0f); #if CLIENT if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar) { @@ -435,7 +437,7 @@ namespace Barotrauma.Items.Components else if (target.UserData is Holdable holdable && holdable.CanPush) { if (holdable.Item.Removed) { return; } - Attack.DoDamage(User, holdable.Item, item.WorldPosition, 1.0f); + Attack.DoDamage(user, holdable.Item, item.WorldPosition, 1.0f); RestoreCollision(); hitting = false; User = null; @@ -448,29 +450,32 @@ namespace Barotrauma.Items.Components if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } - bool success = Rand.Range(0.0f, 0.5f) < DegreeOfSuccess(User); - -#if SERVER - if (GameMain.Server != null && targetCharacter != null) //TODO: Log structure hits + ActionType conditionalActionType = ActionType.OnSuccess; + if (user != null && Rand.Range(0.0f, 0.5f) > DegreeOfSuccess(user)) { - GameMain.Server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData( - success ? ActionType.OnUse : ActionType.OnFailure, - targetItemComponent: null, - targetCharacter, targetLimb)); - - string logStr = picker?.LogName + " used " + item.Name; - if (item.ContainedItems != null && item.ContainedItems.Any()) - { - logStr += " (" + string.Join(", ", item.ContainedItems.Select(i => i?.Name)) + ")"; - } - logStr += " on " + targetCharacter.LogName + "."; - Networking.GameServer.Log(logStr, Networking.ServerLog.MessageType.Attack); + conditionalActionType = ActionType.OnFailure; + } + if (GameMain.NetworkMember is { IsServer: true } server && targetCharacter != null) + { + server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, targetItemComponent: null, targetCharacter, targetLimb)); + server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnUse, targetItemComponent: null, targetCharacter, targetLimb)); + #if SERVER + if (GameMain.Server != null) //TODO: Log structure hits + { + string logStr = picker?.LogName + " used " + item.Name; + if (item.ContainedItems != null && item.ContainedItems.Any()) + { + logStr += " (" + string.Join(", ", item.ContainedItems.Select(i => i?.Name)) + ")"; + } + logStr += " on " + targetCharacter.LogName + "."; + Networking.GameServer.Log(logStr, Networking.ServerLog.MessageType.Attack); + } + #endif } -#endif - if (targetCharacter != null) //TODO: Allow OnUse to happen on structures too maybe?? { - ApplyStatusEffects(success ? ActionType.OnUse : ActionType.OnFailure, 1.0f, targetCharacter, targetLimb, user: User, afflictionMultiplier: damageMultiplier); + ApplyStatusEffects(conditionalActionType, 1.0f, targetCharacter, targetLimb, user: user, afflictionMultiplier: damageMultiplier); + ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, user: user, afflictionMultiplier: damageMultiplier); } if (DeleteOnUse) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs index cadea84be..fa2cf6dbd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs @@ -23,6 +23,8 @@ namespace Barotrauma.Items.Components [Serialize(0.0f, IsPropertySaveable.No, description: "The force to apply to the user's body."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)] public float Force { get; set; } + [Serialize(true, IsPropertySaveable.No, description: "If the item is held in RightHand or LeftHand, apply extra force there")] + public bool ApplyToHands { get; set; } #if CLIENT private string particles; [Serialize("", IsPropertySaveable.No, description: "The name of the particle prefab the item emits when used.")] @@ -70,13 +72,16 @@ namespace Barotrauma.Items.Components character.AnimController.Collider.ApplyForce(propulsion); - if (character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand)) - { - character.AnimController.GetLimb(LimbType.RightHand)?.body.ApplyForce(propulsion); - } - if (character.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand)) - { - character.AnimController.GetLimb(LimbType.LeftHand)?.body.ApplyForce(propulsion); + if (ApplyToHands) + { + if (character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand)) + { + character.AnimController.GetLimb(LimbType.RightHand)?.body.ApplyForce(propulsion); + } + if (character.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand)) + { + character.AnimController.GetLimb(LimbType.LeftHand)?.body.ApplyForce(propulsion); + } } #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index 998d7d7c1..054856773 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -32,6 +32,20 @@ namespace Barotrauma.Items.Components set { reload = Math.Max(value, 0.0f); } } + [Serialize(0f, IsPropertySaveable.No, description: "Weapons skill requirement to reload at normal speed.")] + public float ReloadSkillRequirement + { + get; + set; + } + + [Serialize(1.0f, IsPropertySaveable.No, description: "Reload time at 0 skill level. Reload time scales with skill level up to the Weapons skill requirement.")] + public float ReloadNoSkill + { + get; + set; + } + [Serialize(false, IsPropertySaveable.No, description: "Tells the AI to hold the trigger down when it uses this weapon")] public bool HoldTrigger { @@ -39,7 +53,7 @@ namespace Barotrauma.Items.Components set; } - [Serialize(1, IsPropertySaveable.No, description: "How projectiles the weapon launches when fired once.")] + [Serialize(1, IsPropertySaveable.No, description: "How many projectiles the weapon launches when fired once.")] public int ProjectileCount { get; @@ -60,6 +74,23 @@ namespace Barotrauma.Items.Components set; } + [Serialize(0.0f, IsPropertySaveable.No, description: "The impulse applied to the physics body of the projectile (the higher the impulse, the faster the projectiles are launched). Sum of weapon + projectile.")] + public float LaunchImpulse + { + get; + set; + } + + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Percentage of damage mitigation ignored when hitting armored body parts (deflecting limbs). Sum of weapon + projectile."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1f)] + public float Penetration { get; private set; } + + [Serialize(1f, IsPropertySaveable.Yes, description: "Weapon's damage modifier")] + public float WeaponDamageModifier + { + get; + private set; + } + [Serialize(0f, IsPropertySaveable.Yes, description: "The time required for a charge-type turret to charge up before able to fire.")] public float MaxChargeTime { @@ -99,6 +130,12 @@ namespace Barotrauma.Items.Components // TODO: should define this in xml if we have ranged weapons that don't require aim to use item.RequireAimToUse = true; characterUsable = true; + + if (ReloadSkillRequirement > 0 && ReloadNoSkill <= reload) + { + DebugConsole.AddWarning($"Invalid XML at {item.Name}: ReloadNoSkill is lower or equal than it's reload skill, despite having ReloadSkillRequirement."); + } + InitProjSpecific(element); } @@ -167,7 +204,15 @@ namespace Barotrauma.Items.Components if (currentChargeTime < MaxChargeTime) { return false; } IsActive = true; - ReloadTimer = reload / (1 + character?.GetStatValue(StatTypes.RangedAttackSpeed) ?? 0f); + float baseReloadTime = reload; + float weaponSkill = character.GetSkillLevel("weapons"); + if (ReloadSkillRequirement > 0 && ReloadNoSkill > reload && weaponSkill < ReloadSkillRequirement) + { + //Examples, assuming 40 weapon skill required: 1 - 40/40 = 0 ... 1 - 0/40 = 1 ... 1 - 20 / 40 = 0.5 + float reloadFailure = MathHelper.Clamp(1 - (weaponSkill / ReloadSkillRequirement), 0, 1); + baseReloadTime = MathHelper.Lerp(reload, ReloadNoSkill, reloadFailure); + } + ReloadTimer = baseReloadTime / (1 + character?.GetStatValue(StatTypes.RangedAttackSpeed) ?? 0f); currentChargeTime = 0f; if (character != null) @@ -218,9 +263,9 @@ namespace Barotrauma.Items.Components { lastProjectile?.Item.GetComponent()?.Snap(); } - float damageMultiplier = 1f + item.GetQualityModifier(Quality.StatType.FirepowerMultiplier); + float damageMultiplier = (1f + item.GetQualityModifier(Quality.StatType.FirepowerMultiplier)) * WeaponDamageModifier; projectile.Launcher = item; - projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: ignoredBodies.ToList(), createNetworkEvent: false, damageMultiplier); + projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: ignoredBodies.ToList(), createNetworkEvent: false, damageMultiplier, LaunchImpulse); projectile.Item.GetComponent()?.Attach(Item, projectile.Item); if (i == 0) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs index df595ca57..edf91b18e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs @@ -1,17 +1,26 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Items.Components { class Throwable : Holdable { - private float throwPos; - private bool throwing, throwDone; + enum ThrowState + { + None, + Initiated, + Throwing + } + + private const float ThrowAngleStart = -MathHelper.PiOver2, ThrowAngleEnd = MathHelper.PiOver2; + private float throwAngle = ThrowAngleStart; private bool midAir; + private ThrowState throwState; + + //continuous collision detection is used while the item is moving faster than this const float ContinuousCollisionThreshold = 5.0f; @@ -27,7 +36,6 @@ namespace Barotrauma.Items.Components public Throwable(Item item, ContentXElement element) : base(item, element) { - //throwForce = ToolBox.GetAttributeFloat(element, "throwforce", 1.0f); if (aimPos == Vector2.Zero) { aimPos = new Vector2(0.6f, 0.1f); @@ -36,22 +44,21 @@ namespace Barotrauma.Items.Components public override bool Use(float deltaTime, Character character = null) { - return characterUsable || character == null; //We do the actual throwing in Aim because Use might be used by chems + //actual throwing logic is handled in Update + return characterUsable || character == null; } public override bool SecondaryUse(float deltaTime, Character character = null) { - if (!throwDone) return false; //This should only be triggered in update - throwDone = false; - return true; + //actual throwing logic is handled in Update - SecondaryUse only triggers when the item is thrown + return false; } public override void Drop(Character dropper) { base.Drop(dropper); - - throwing = false; - throwPos = 0.0f; + throwState = ThrowState.None; + throwAngle = ThrowAngleStart; } public override void UpdateBroken(float deltaTime, Camera cam) @@ -100,13 +107,22 @@ namespace Barotrauma.Items.Components return; } - if (picker.IsKeyDown(InputType.Aim) && picker.IsKeyHit(InputType.Shoot)) { throwing = true; } - if (!picker.IsKeyDown(InputType.Aim) && !throwing) { throwPos = 0.0f; } - bool aim = picker.IsKeyDown(InputType.Aim) && picker.CanAim; + if (throwState != ThrowState.Throwing) + { + if (picker.IsKeyDown(InputType.Aim)) + { + if (picker.IsKeyDown(InputType.Shoot)) { throwState = ThrowState.Initiated; } + } + else if (throwState != ThrowState.Initiated) + { + throwAngle = ThrowAngleStart; + } + } + bool aim = picker.IsKeyDown(InputType.Aim) && picker.CanAim; if (picker.IsDead || !picker.AllowInput) { - throwing = false; + throwState = ThrowState.None; aim = false; } @@ -124,25 +140,29 @@ namespace Barotrauma.Items.Components item.Submarine = picker.Submarine; - if (!throwing) + if (throwState != ThrowState.Throwing) { - if (aim) + if (aim || throwState == ThrowState.Initiated) { - throwPos = MathUtils.WrapAnglePi(System.Math.Min(throwPos + deltaTime * 5.0f, MathHelper.PiOver2)); - ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwPos); + throwAngle = System.Math.Min(throwAngle + deltaTime * 8.0f, ThrowAngleEnd); + ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwAngle); + if (throwAngle >= ThrowAngleEnd && throwState == ThrowState.Initiated) + { + throwState = ThrowState.Throwing; + } } else { - throwPos = 0; + throwAngle = ThrowAngleStart; ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, aim: false, holdAngle); } } else { - throwPos = MathUtils.WrapAnglePi(throwPos - deltaTime * 15.0f); - ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwPos); + throwAngle = MathUtils.WrapAnglePi(throwAngle - deltaTime * 15.0f); + ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwAngle); - if (throwPos < 0) + if (throwAngle < 0) { Vector2 throwVector = Vector2.Normalize(picker.CursorWorldPosition - picker.WorldPosition); //throw upwards if cursor is at the position of the character @@ -180,8 +200,7 @@ namespace Barotrauma.Items.Components Limb rightHand = ac.GetLimb(LimbType.RightHand); item.body.AngularVelocity = rightHand.body.AngularVelocity; - throwPos = 0; - throwDone = true; + throwAngle = ThrowAngleStart; IsActive = true; if (GameMain.NetworkMember is { IsServer: true }) @@ -193,7 +212,7 @@ namespace Barotrauma.Items.Components //Stun grenades, flares, etc. all have their throw-related things handled in "onSecondaryUse" ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, CurrentThrower, user: CurrentThrower); } - throwing = false; + throwState = ThrowState.None; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index a1d69ce93..fac2fc0f0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -65,8 +65,16 @@ namespace Barotrauma.Items.Components public int Capacity { get { return capacity; } - set { capacity = Math.Max(value, 0); } + private set + { + capacity = Math.Max(value, 0); + MainContainerCapacity = value; + } } + /// + /// The capacity of the main container without taking the sub containers into account. Only differs when there's a sub container defined for the component. + /// + public int MainContainerCapacity { get; private set; } //how many items can be contained private int maxStackSize; @@ -229,6 +237,9 @@ namespace Barotrauma.Items.Components public ImmutableHashSet ContainableItemIdentifiers => containableItemIdentifiers; public List ContainableItems { get; } + public List AllSubContainableItems { get; } + + public readonly bool HasSubContainers; public ItemContainer(Item item, ContentXElement element) : base(item, element) @@ -251,6 +262,7 @@ namespace Barotrauma.Items.Components break; case "subcontainer": totalCapacity += subElement.GetAttributeInt("capacity", 1); + HasSubContainers = true; break; } } @@ -270,7 +282,7 @@ namespace Barotrauma.Items.Components int subCapacity = subElement.GetAttributeInt("capacity", 1); int subMaxStackSize = subElement.GetAttributeInt("maxstacksize", maxStackSize); - List subContainableItems = null; + var subContainableItems = new List(); foreach (var subSubElement in subElement.Elements()) { if (subSubElement.Name.ToString().ToLowerInvariant() != "containable") { continue; } @@ -281,8 +293,9 @@ namespace Barotrauma.Items.Components DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - containable with no identifiers."); continue; } - subContainableItems ??= new List(); subContainableItems.Add(containable); + AllSubContainableItems ??= new List(); + AllSubContainableItems.Add(containable); } for (int i = subContainerIndex; i < subContainerIndex + subCapacity; i++) @@ -357,6 +370,14 @@ namespace Barotrauma.Items.Components //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); + + if (IsActive && item.GetRootInventoryOwner() is Character owner && + owner.HasEquippedItem(item, predicate: slot => slot.HasFlag(InvSlotType.LeftHand) || slot.HasFlag(InvSlotType.RightHand))) + { + // Set the contained items active if there's an item inserted inside the container. Enables e.g. the rifle flashlight when it's attached to the rifle (put inside of it). + SetContainedActive(true); + } + OnContainedItemsChanged.Invoke(this); } @@ -409,6 +430,20 @@ namespace Barotrauma.Items.Components return false; } + public override void FlipX(bool relativeToSub) + { + base.FlipX(relativeToSub); + if (HideItems) { return; } + if (item.body == null) { return; } + foreach (Item containedItem in Inventory.AllItems) + { + if (containedItem.body != null && containedItem.body.Enabled && containedItem.body.Dir != item.body.Dir) + { + containedItem.FlipX(relativeToSub); + } + } + } + public override void Update(float deltaTime, Camera cam) { if (!string.IsNullOrEmpty(SpawnWithId) && !alwaysContainedItemsSpawned) @@ -477,7 +512,7 @@ namespace Barotrauma.Items.Components effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); - targets.AddRange(effect.GetNearbyTargets(item.WorldPosition, targets)); + effect.AddNearbyTargets(item.WorldPosition, targets); effect.Apply(ActionType.OnActive, deltaTime, item, targets); } } @@ -582,11 +617,53 @@ namespace Barotrauma.Items.Components public override void Drop(Character dropper) { IsActive = true; + SetContainedActive(false); } public override void Equip(Character character) { IsActive = true; + if (character != null && character.HasEquippedItem(item, predicate: slot => slot.HasFlag(InvSlotType.LeftHand) || slot.HasFlag(InvSlotType.RightHand))) + { + SetContainedActive(true); + } + } + + private void SetContainedActive(bool active) + { + foreach (Item containedItem in Inventory.AllItems) + { + RelatedItem containableItem = FindContainableItem(containedItem); + if (containableItem != null && containableItem.SetActive) + { + foreach (var ic in containedItem.Components) + { + ic.IsActive = active; + } + if (containedItem.body != null) + { + containedItem.body.Enabled = active; + if (active) + { + containedItem.body.PhysEnabled = false; + } + } + } + } + if (active) + { + FlipX(false); + } + } + + private RelatedItem FindContainableItem(Item item) + { + var relatedItem = ContainableItems?.FirstOrDefault(ci => ci.MatchesItem(item)); + if (relatedItem == null && AllSubContainableItems != null) + { + relatedItem = AllSubContainableItems.FirstOrDefault(ci => ci.MatchesItem(item)); + } + return relatedItem; } public override void ReceiveSignal(Signal signal, Connection connection) @@ -604,6 +681,7 @@ namespace Barotrauma.Items.Components } } +#warning There's some code duplication here and in DrawContainedItems() method, but it's not straightforward to get rid of it, because of slightly different logic and the usage of draw positions vs. positions etc. Should probably be splitted into smaller methods. public void SetContainedItemPositions() { Vector2 transformedItemPos = ItemPos * item.Scale; @@ -657,29 +735,70 @@ namespace Barotrauma.Items.Components transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform); transformedItemPos += item.Position; } - } - - float currentRotation = itemRotation; - if (item.body != null) - { - currentRotation *= item.body.Dir; - currentRotation += item.body.Rotation; - } - else - { - currentRotation += -item.RotationRad; } int i = 0; Vector2 currentItemPos = transformedItemPos; foreach (Item contained in Inventory.AllItems) { + Vector2 itemPos = currentItemPos; + var relatedItem = FindContainableItem(contained); + if (relatedItem != null) + { + if (relatedItem.Hide.HasValue && relatedItem.Hide.Value) { continue; } + if (relatedItem.ItemPos.HasValue) + { + Vector2 pos = relatedItem.ItemPos.Value; + if (item.body != null) + { + Matrix transform = Matrix.CreateRotationZ(item.body.Rotation); + pos.X *= item.body.Dir; + itemPos = Vector2.Transform(pos, transform) + item.body.Position; + } + else + { + itemPos = pos; + // This code is aped based on above. Not tested. + if (item.FlippedX) + { + itemPos.X = -itemPos.X; + itemPos.X += item.Rect.Width; + } + if (item.FlippedY) + { + itemPos.Y = -itemPos.Y; + itemPos.Y -= item.Rect.Height; + } + itemPos += new Vector2(item.Rect.X, item.Rect.Y); + if (Math.Abs(item.RotationRad) > 0.01f) + { + Matrix transform = Matrix.CreateRotationZ(item.RotationRad); + itemPos = Vector2.Transform(itemPos - item.Position, transform) + item.Position; + } + } + } + } + if (contained.body != null) { try { - Vector2 simPos = ConvertUnits.ToSimUnits(currentItemPos); - contained.body.FarseerBody.SetTransformIgnoreContacts(ref simPos, currentRotation); + Vector2 simPos = ConvertUnits.ToSimUnits(itemPos); + float rotation = itemRotation; + if (relatedItem != null && relatedItem.Rotation != 0) + { + rotation = MathHelper.ToRadians(relatedItem.Rotation); + } + if (item.body != null) + { + rotation *= item.body.Dir; + rotation += item.body.Rotation; + } + else + { + rotation += -item.RotationRad; + } + contained.body.FarseerBody.SetTransformIgnoreContacts(ref simPos, rotation); contained.body.SetPrevTransform(contained.body.SimPosition, contained.body.Rotation); contained.body.UpdateDrawPosition(); } @@ -695,8 +814,8 @@ namespace Barotrauma.Items.Components contained.Rect = new Rectangle( - (int)(currentItemPos.X - contained.Rect.Width / 2.0f), - (int)(currentItemPos.Y + contained.Rect.Height / 2.0f), + (int)(itemPos.X - contained.Rect.Width / 2.0f), + (int)(itemPos.Y + contained.Rect.Height / 2.0f), contained.Rect.Width, contained.Rect.Height); contained.Submarine = item.Submarine; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index 21179fc74..f8df97352 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -459,6 +459,12 @@ namespace Barotrauma.Items.Components progressTimer = 0.0f; progressState = 0.0f; } +#if CLIENT + else + { + HintManager.OnStartDeconstructing(user, this); + } +#endif inputContainer.Inventory.Locked = IsActive; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 537366213..e2c8cd0b0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -419,7 +419,7 @@ namespace Barotrauma.Items.Components } var fabricationIngredients = new AbilityFabricationItemIngredients(foundAvailableItems); - user.CheckTalents(AbilityEffectType.OnItemFabricatedIngredients, fabricationIngredients); + user?.CheckTalents(AbilityEffectType.OnItemFabricatedIngredients, fabricationIngredients); foreach (Item availableItem in fabricationIngredients.Items) { @@ -559,7 +559,7 @@ namespace Barotrauma.Items.Components if (fabricatedItem.TargetItem.ConfigElement.GetChildElement("Quality") == null) { return 0; } int quality = 0; float floatQuality = 0.0f; - floatQuality += user.GetStatValue(StatTypes.IncreaseFabricationQuality); + floatQuality += user.GetStatValue(StatTypes.IncreaseFabricationQuality, includeSaved: false); foreach (var tag in fabricatedItem.TargetItem.Tags) { floatQuality += user.Info.GetSavedStatValue(StatTypes.IncreaseFabricationQuality, tag); @@ -740,10 +740,27 @@ namespace Barotrauma.Items.Components //order by condition (prefer using worst-condition items) int index = 0; while (index < availableIngredients[itemIdentifier].Count && - availableIngredients[itemIdentifier][index].Condition < item.Condition) + compare(item, availableIngredients[itemIdentifier][index], inputContainer.Inventory) < 0) { index++; } + + static int compare(Item item1, Item item2, Inventory inputInventory) + { + bool item1InInputInventory = item1.ParentInventory == inputInventory; + bool item2InInputInventory = item2.ParentInventory == inputInventory; + //prefer items in the input inventory + if (item1InInputInventory != item2InInputInventory) + { + return item1InInputInventory ? 1 : -1; + } + else + { + //prefer items in worse condition + return Math.Sign(item2.Condition - item1.Condition); + } + } + availableIngredients[itemIdentifier].Insert(index, item); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs index a3588ac5c..8a3cd4d15 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs @@ -15,6 +15,8 @@ namespace Barotrauma.Items.Components public float? ReceivedOxygenAmount, ReceivedWaterAmount; + public double LastOxygenDataTime, LastWaterDataTime; + public readonly HashSet Cards = new HashSet(); public bool Distort; @@ -83,7 +85,7 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - //periodically reset all hull data + //reset data if we haven't received anything in a while //(so that outdated hull info won't be shown if detectors stop sending signals) if (DateTime.Now > resetDataTime) { @@ -91,8 +93,8 @@ namespace Barotrauma.Items.Components { if (!hullData.Distort) { - hullData.ReceivedOxygenAmount = null; - hullData.ReceivedWaterAmount = null; + if (Timing.TotalTime > hullData.LastOxygenDataTime + 1.0) { hullData.ReceivedOxygenAmount = null; } + if (Timing.TotalTime > hullData.LastWaterDataTime + 1.0) { hullData.ReceivedWaterAmount = null; } } } resetDataTime = DateTime.Now + new TimeSpan(0, 0, 1); @@ -159,6 +161,7 @@ namespace Barotrauma.Items.Components //cheating a bit because water detectors don't actually send the water level bool fromWaterDetector = source.GetComponent() != null; hullData.ReceivedWaterAmount = null; + hullData.LastWaterDataTime = Timing.TotalTime; if (fromWaterDetector) { hullData.ReceivedWaterAmount = WaterDetector.GetWaterPercentage(sourceHull); @@ -184,9 +187,10 @@ namespace Barotrauma.Items.Components oxy = Rand.Range(0.0f, 100.0f); } hullData.ReceivedOxygenAmount = oxy; + hullData.LastOxygenDataTime = Timing.TotalTime; foreach (var linked in sourceHull.linkedTo) { - if (!(linked is Hull linkedHull)) { continue; } + if (linked is not Hull linkedHull) { continue; } if (!hullDatas.TryGetValue(linkedHull, out HullData linkedHullData)) { linkedHullData = new HullData(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs index 6e727c30d..250c5b37a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs @@ -197,11 +197,6 @@ namespace Barotrauma.Items.Components if (currentPingIndex != -1) { var activePing = activePings[currentPingIndex]; - if (item.AiTarget != null) - { - float range = MathUtils.InverseLerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, Range * activePing.State / zoom); - item.AiTarget.SoundRange = MathHelper.Lerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, range); - } if (activePing.State > 1.0f) { aiPingCheckPending = true; @@ -235,6 +230,11 @@ namespace Barotrauma.Items.Components for (var pingIndex = 0; pingIndex < activePingsCount;) { + if (item.AiTarget != null) + { + float range = MathUtils.InverseLerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, Range * activePings[pingIndex].State / zoom); + item.AiTarget.SoundRange = Math.Max(item.AiTarget.SoundRange, MathHelper.Lerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, range)); + } if (activePings[pingIndex].State > 1.0f) { var lastIndex = --activePingsCount; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index 75776f814..90922ec50 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -144,6 +144,11 @@ namespace Barotrauma.Items.Components } } + public float TargetVelocityLengthSquared + { + get => TargetVelocity.LengthSquared(); + } + public Vector2 SteeringInput { get { return steeringInput; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index a56e5afde..741afba2e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -392,6 +392,7 @@ namespace Barotrauma.Items.Components } } User = character; + ApplyStatusEffects(ActionType.OnUse, 1.0f, User, user: User); return true; } @@ -916,23 +917,22 @@ namespace Barotrauma.Items.Components if (character != null) { character.LastDamageSource = item; } - ActionType actionType = ActionType.OnUse; - if (_user != null && Rand.Range(0.0f, 0.5f) > DegreeOfSuccess(_user)) + ActionType conditionalActionType = ActionType.OnSuccess; + if (User != null && Rand.Range(0.0f, 0.5f) > DegreeOfSuccess(User)) { - actionType = ActionType.OnFailure; + conditionalActionType = ActionType.OnFailure; } - #if CLIENT - PlaySound(actionType, user: _user); - PlaySound(ActionType.OnImpact, user: _user); + PlaySound(conditionalActionType, user: User); + PlaySound(ActionType.OnImpact, user: User); #endif if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { if (target.Body.UserData is Limb targetLimb) { - ApplyStatusEffects(actionType, 1.0f, character, targetLimb, user: _user); - ApplyStatusEffects(ActionType.OnImpact, 1.0f, character, targetLimb, user: _user); + ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, user: User); + ApplyStatusEffects(ActionType.OnImpact, 1.0f, character, targetLimb, user: User); var attack = targetLimb.attack; if (attack != null) { @@ -941,8 +941,6 @@ namespace Barotrauma.Items.Components { if (effect.type == ActionType.OnImpact) { - //effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb.character, targetLimb.WorldPosition); - if (effect.HasTargetType(StatusEffect.TargetType.This)) { effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb.character, targetLimb.WorldPosition); @@ -951,32 +949,27 @@ namespace Barotrauma.Items.Components effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); - targets.AddRange(effect.GetNearbyTargets(targetLimb.WorldPosition, targets)); + effect.AddNearbyTargets(targetLimb.WorldPosition, targets); effect.Apply(ActionType.OnActive, 1.0f, targetLimb.character, targets); } - } } } -#if SERVER - if (GameMain.NetworkMember.IsServer) + if (GameMain.NetworkMember is { IsServer: true } server) { - GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(actionType, this, targetLimb.character, targetLimb, null, item.WorldPosition)); - GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, targetLimb.character, targetLimb, null, item.WorldPosition)); + server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, this, targetLimb.character, targetLimb, null, item.WorldPosition)); + server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, targetLimb.character, targetLimb, null, item.WorldPosition)); } -#endif } else { - ApplyStatusEffects(actionType, 1.0f, useTarget: target.Body.UserData as Entity, user: _user); - ApplyStatusEffects(ActionType.OnImpact, 1.0f, useTarget: target.Body.UserData as Entity, user: _user); -#if SERVER - if (GameMain.NetworkMember.IsServer) + ApplyStatusEffects(conditionalActionType, 1.0f, useTarget: target.Body.UserData as Entity, user: User); + ApplyStatusEffects(ActionType.OnImpact, 1.0f, useTarget: target.Body.UserData as Entity, user: User); + if (GameMain.NetworkMember is { IsServer: true } server) { - GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(actionType, this, null, null, target.Body.UserData as Entity, item.WorldPosition)); - GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, null, null, target.Body.UserData as Entity, item.WorldPosition)); + server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, this, null, null, target.Body.UserData as Entity, item.WorldPosition)); + server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, null, null, target.Body.UserData as Entity, item.WorldPosition)); } -#endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index 32f151379..1ccd035f9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -289,7 +289,7 @@ namespace Barotrauma.Items.Components #if CLIENT Light.ParentSub = item.Submarine; #endif - if (item.Container != null) + if (item.Container != null && !(item.GetRootInventoryOwner() is Character)) { SetLightSourceState(false, 0.0f); return; @@ -301,7 +301,7 @@ namespace Barotrauma.Items.Components if (body != null && !body.Enabled) { SetLightSourceState(false, 0.0f); - return; + return; } //currPowerConsumption = powerConsumption; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 8902fd99e..a5020cff3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -138,7 +138,7 @@ namespace Barotrauma private ConcurrentQueue impactQueue; //a dictionary containing lists of the status effects in all the components of the item - private readonly bool[] hasStatusEffectsOfType; + private readonly bool[] hasStatusEffectsOfType = new bool[Enum.GetValues(typeof(ActionType)).Length]; private readonly Dictionary> statusEffectLists; public Dictionary SerializableProperties { get; protected set; } @@ -630,7 +630,7 @@ namespace Barotrauma { if (!spawnedInCurrentOutpost && value) { - OriginalOutpost = GameMain.GameSession?.StartLocation?.BaseName ?? ""; + OriginalOutpost = GameMain.GameSession?.LevelData?.Seed; } spawnedInCurrentOutpost = value; } @@ -651,7 +651,9 @@ namespace Barotrauma set { originalOutpost = value; - if (!string.IsNullOrEmpty(value) && GameMain.GameSession?.LevelData?.Type == LevelData.LevelType.Outpost && GameMain.GameSession?.StartLocation?.BaseName == value) + if (!string.IsNullOrEmpty(value) && + GameMain.GameSession?.LevelData?.Type == LevelData.LevelType.Outpost && + GameMain.GameSession?.LevelData?.Seed == value) { spawnedInCurrentOutpost = true; } @@ -1005,7 +1007,6 @@ namespace Barotrauma } } - hasStatusEffectsOfType = new bool[Enum.GetValues(typeof(ActionType)).Length]; foreach (ItemComponent ic in components) { if (ic is Pickable pickable) @@ -1654,7 +1655,7 @@ namespace Barotrauma if (effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters) || effect.HasTargetType(StatusEffect.TargetType.NearbyItems)) { - targets.AddRange(effect.GetNearbyTargets(WorldPosition, targets)); + effect.AddNearbyTargets(WorldPosition, targets); if (targets.Count > 0) { hasTargets = true; @@ -2764,18 +2765,20 @@ namespace Barotrauma if (!ic.HasRequiredContainedItems(user, addMessage: user == Character.Controlled)) { continue; } bool success = Rand.Range(0.0f, 0.5f) < ic.DegreeOfSuccess(user); - ActionType actionType = success ? ActionType.OnUse : ActionType.OnFailure; + ActionType conditionalActionType = success ? ActionType.OnSuccess : ActionType.OnFailure; #if CLIENT - ic.PlaySound(actionType, user); + ic.PlaySound(conditionalActionType, user); + ic.PlaySound(ActionType.OnUse, user); #endif ic.WasUsed = true; - ic.ApplyStatusEffects(actionType, 1.0f, character, targetLimb, user: user); + + ic.ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, user: user); + ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, character, targetLimb, user: user); if (GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData( - actionType, ic, character, targetLimb)); + GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(conditionalActionType, ic, character, targetLimb)); } if (ic.DeleteOnUse) { remove = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index 478b231fa..4dafd2d11 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -814,8 +814,6 @@ namespace Barotrauma ? category : MapEntityCategory.Misc; - var parentType = ConfigElement.Parent?.GetAttributeIdentifier("itemtype", ""); - //nameidentifier can be used to make multiple items use the same names and descriptions Identifier nameIdentifier = ConfigElement.GetAttributeIdentifier("nameidentifier", ""); @@ -831,7 +829,7 @@ namespace Barotrauma name = name.Fallback(OriginalName); } - if (parentType == "wrecked") + if (category == MapEntityCategory.Wrecked) { name = TextManager.GetWithVariable("wreckeditemformat", "[name]", name); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs index 26b7d6e00..5de6d463e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; +using Microsoft.Xna.Framework; using Barotrauma.Extensions; namespace Barotrauma @@ -57,6 +58,20 @@ namespace Barotrauma /// public int TargetSlot = -1; + /// + /// Overrides the position defined in ItemContainer. + /// + public Vector2? ItemPos; + + /// + /// Only affects when ItemContainer.hideItems is false. Doesn't override the value. + /// + public bool? Hide; + + public float Rotation; + + public bool SetActive; + public string JoinedIdentifiers { get { return string.Join(",", Identifiers); } @@ -202,7 +217,18 @@ namespace Barotrauma new XAttribute("requireempty", RequireEmpty), new XAttribute("excludefullcondition", ExcludeFullCondition), new XAttribute("targetslot", TargetSlot), - new XAttribute("allowvariants", AllowVariants)); + new XAttribute("allowvariants", AllowVariants), + new XAttribute("rotation", Rotation), + new XAttribute("setactive", SetActive)); + + if (Hide.HasValue) + { + element.Add(new XAttribute(nameof(Hide), Hide.Value)); + } + if (ItemPos.HasValue) + { + element.Add(new XAttribute(nameof(ItemPos), ItemPos.Value)); + } if (excludedIdentifiers.Count > 0) { @@ -267,8 +293,18 @@ namespace Barotrauma ExcludeBroken = element.GetAttributeBool("excludebroken", true), RequireEmpty = element.GetAttributeBool("requireempty", false), ExcludeFullCondition = element.GetAttributeBool("excludefullcondition", false), - AllowVariants = element.GetAttributeBool("allowvariants", true) + AllowVariants = element.GetAttributeBool("allowvariants", true), + Rotation = element.GetAttributeFloat("rotation", 0f), + SetActive = element.GetAttributeBool("setactive", false) }; + if (element.GetAttribute(nameof(Hide)) != null) + { + ri.Hide = element.GetAttributeBool(nameof(Hide), false); + } + if (element.GetAttribute(nameof(ItemPos)) != null) + { + ri.ItemPos = element.GetAttributeVector2(nameof(ItemPos), Vector2.Zero); + } string typeStr = element.GetAttributeString("type", ""); if (string.IsNullOrEmpty(typeStr)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs index ee915e10e..1144fc52b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs @@ -1,3 +1,5 @@ +using Barotrauma.Extensions; +using System.Collections.Generic; using System.Collections.Immutable; namespace Barotrauma @@ -18,6 +20,14 @@ namespace Barotrauma public readonly ImmutableHashSet AllowedZones; + private readonly SubmarineAvailability? submarineAvailability; + private readonly ImmutableHashSet submarineAvailabilityOverrides; + + public readonly record struct SubmarineAvailability( + Identifier LocationType, + SubmarineClass Class = SubmarineClass.Undefined, + int MaxTier = 0); + public Biome(ContentXElement element, LevelGenerationParametersFile file) : base(file, ParseIdentifier(element)) { OldIdentifier = element.GetAttributeIdentifier("oldidentifier", Identifier.Empty); @@ -34,6 +44,26 @@ namespace Barotrauma AllowedZones = element.GetAttributeIntArray("AllowedZones", new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }).ToImmutableHashSet(); MinDifficulty = element.GetAttributeFloat("MinDifficulty", 0); maxDifficulty = element.GetAttributeFloat("MaxDifficulty", 100); + + var submarineAvailabilityOverrides = new HashSet(); + if (element.GetChildElement("submarines") is ContentXElement availabilityElement) + { + submarineAvailability = GetAvailability(availabilityElement); + foreach (var overrideElement in availabilityElement.GetChildElements("override")) + { + var availabilityOverride = GetAvailability(overrideElement); + submarineAvailabilityOverrides.Add(availabilityOverride); + } + } + this.submarineAvailabilityOverrides = submarineAvailabilityOverrides.ToImmutableHashSet(); + + static SubmarineAvailability GetAvailability(ContentXElement element) + { + return new SubmarineAvailability( + LocationType: element.GetAttributeIdentifier("locationtype", Identifier.Empty), + Class: element.GetAttributeEnum("class", SubmarineClass.Undefined), + MaxTier: element.GetAttributeInt("maxtier", 0)); + } } public static Identifier ParseIdentifier(ContentXElement element) @@ -47,6 +77,31 @@ namespace Barotrauma return identifier; } + public int HighestSubmarineTierAvailable(SubmarineClass subClass, Identifier locationType) + { + if (!submarineAvailability.HasValue) + { + // If the availability is not explicitly defined, make all subs available + return SubmarineInfo.HighestTier; + } + int maxTier = submarineAvailability.Value.MaxTier; + if (submarineAvailabilityOverrides.FirstOrNull(a => a.LocationType == locationType && a.Class == subClass) is SubmarineAvailability locationAndClassOverride) + { + maxTier = locationAndClassOverride.MaxTier; + } + else if (submarineAvailabilityOverrides.FirstOrNull(a => a.LocationType == locationType && a.Class == SubmarineClass.Undefined) is SubmarineAvailability locationOverride) + { + maxTier = locationOverride.MaxTier; + } + else if (submarineAvailabilityOverrides.FirstOrNull(a => a.LocationType == Identifier.Empty && a.Class == subClass) is SubmarineAvailability classOverride) + { + maxTier = classOverride.MaxTier; + } + return maxTier; + } + + public bool IsSubmarineAvailable(SubmarineInfo info, Identifier locationType) => info.Tier <= HighestSubmarineTierAvailable(info.SubmarineClass, locationType); + public override void Dispose() { } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 433b711a2..46adf35e2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -447,7 +447,7 @@ namespace Barotrauma private Level(LevelData levelData) : base(null, 0) { - this.LevelData = levelData; + LevelData = levelData; borders = new Rectangle(Point.Zero, levelData.Size); } @@ -3939,7 +3939,7 @@ namespace Barotrauma } SubmarineInfo outpostInfo; - Submarine outpost; + Submarine outpost = null; if (i == 0 && preSelectedStartOutpost == null || i == 1 && preSelectedEndOutpost == null) { if (OutpostGenerationParams.OutpostParams.Any() || LevelData.ForceOutpostGenerationParams != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index 4120c334a..8e9ca73d1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -59,6 +59,7 @@ namespace Barotrauma public readonly List EventHistory = new List(); public readonly List NonRepeatableEvents = new List(); + public readonly HashSet UsedUniqueSets = new HashSet(); public bool EventsExhausted { get; set; } @@ -143,10 +144,11 @@ namespace Barotrauma string[] nonRepeatablePrefabNames = element.GetAttributeStringArray("nonrepeatableevents", new string[] { }); NonRepeatableEvents.AddRange(EventPrefab.Prefabs.Where(p => nonRepeatablePrefabNames.Any(n => p.Identifier == n))); + UsedUniqueSets = element.GetAttributeIdentifierArray(nameof(UsedUniqueSets), Array.Empty()).ToHashSet(); + EventsExhausted = element.GetAttributeBool(nameof(EventsExhausted).ToLower(), false); } - /// /// Instantiates level data using the properties of the connection (seed, size, difficulty) /// @@ -284,6 +286,12 @@ namespace Barotrauma newElement.Add(new XAttribute("nonrepeatableevents", string.Join(',', NonRepeatableEvents.Select(p => p.Identifier)))); } } + + if (UsedUniqueSets.Any()) + { + newElement.Add(new XAttribute(nameof(UsedUniqueSets), string.Join(',', UsedUniqueSets))); + } + parentElement.Add(newElement); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs index a2ad86140..213fad7bf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs @@ -287,6 +287,13 @@ namespace Barotrauma private set; } + [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes), Editable] + public Color SpriteColor + { + get; + private set; + } + public string Name => Identifier.Value; public List ChildObjects diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs index 480163ffa..a5c1b3f85 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs @@ -661,7 +661,7 @@ namespace Barotrauma if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); - targets.AddRange(effect.GetNearbyTargets(worldPosition, targets)); + effect.AddNearbyTargets(worldPosition, targets); effect.Apply(effect.type, deltaTime, triggerer, targets); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 80b11198b..a8426ed70 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -61,7 +61,7 @@ namespace Barotrauma private LocationType addInitialMissionsForType; - public bool Discovered { get; private set; } + public bool Discovered => GameMain.GameSession?.Map?.IsDiscovered(this) ?? false; public readonly Dictionary ProximityTimer = new Dictionary(); public (LocationTypeChange typeChange, int delay, MissionPrefab parentMission)? PendingLocationTypeChange; @@ -287,12 +287,12 @@ namespace Barotrauma var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); if (characters.Any()) { - if (Location.Reputation?.Faction is { } faction && faction.IsAffiliated()) + if (Location.Reputation?.Faction is { } faction && faction.GetPlayerAffiliationStatus() is FactionAffiliation.Affiliated) { price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplierAffiliated)); } - price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplier)); - price *= 1f + characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplier, tag))); + price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplier, includeSaved: false)); + price *= 1f - characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplier, tag))); } // Price should never go below 1 mk return Math.Max((int)price, 1); @@ -497,7 +497,6 @@ namespace Barotrauma baseName = element.GetAttributeString("basename", ""); Name = element.GetAttributeString("name", ""); MapPosition = element.GetAttributeVector2("position", Vector2.Zero); - Discovered = element.GetAttributeBool("discovered", false); PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 1.0f); IsGateBetweenBiomes = element.GetAttributeBool("isgatebetweenbiomes", false); MechanicalPriceMultiplier = element.GetAttributeFloat("mechanicalpricemultipler", 1.0f); @@ -1292,14 +1291,17 @@ namespace Barotrauma return characters.Sum(c => (int)c.GetStatValue(StatTypes.ExtraSpecialSalesCount)); } - public void Discover(bool checkTalents = true) + public int HighestSubmarineTierAvailable(SubmarineClass submarineClass) { - if (Discovered) { return; } - Discovered = true; - if (checkTalents) - { - GameSession.GetSessionCrewCharacters(CharacterType.Both).ForEach(c => c.CheckTalents(AbilityEffectType.OnLocationDiscovered, new AbilityLocation(this))); - } + if (!HasOutpost()) { return 0; } + return Biome?.HighestSubmarineTierAvailable(submarineClass, Type.Identifier) ?? SubmarineInfo.HighestTier; + } + + public int HighestSubmarineTierAvailable() => HighestSubmarineTierAvailable(SubmarineClass.Undefined); + + public bool IsSubmarineAvailable(SubmarineInfo info) + { + return Biome?.IsSubmarineAvailable(info, Type.Identifier) ?? true; } public void Reset() @@ -1313,7 +1315,6 @@ namespace Barotrauma ClearMissions(); LevelData?.EventHistory?.Clear(); UnlockInitialMissions(); - Discovered = false; } public XElement Save(Map map, XElement parentElement) @@ -1324,7 +1325,6 @@ namespace Barotrauma new XAttribute("basename", BaseName), new XAttribute("name", Name), new XAttribute("biome", Biome?.Identifier.Value ?? string.Empty), - new XAttribute("discovered", Discovered), new XAttribute("position", XMLExtensions.Vector2ToString(MapPosition)), new XAttribute("pricemultiplier", PriceMultiplier), new XAttribute("isgatebetweenbiomes", IsGateBetweenBiomes), @@ -1453,7 +1453,7 @@ namespace Barotrauma HireManager?.Remove(); } - class AbilityLocation : AbilityObject, IAbilityLocation + public class AbilityLocation : AbilityObject, IAbilityLocation { public AbilityLocation(Location location) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs index 97651462a..fdb6aad08 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs @@ -24,6 +24,7 @@ namespace Barotrauma public readonly Dictionary MinCountPerZone = new Dictionary(); public readonly LocalizedString Name; + public readonly LocalizedString Description; public readonly float BeaconStationChance; @@ -70,6 +71,13 @@ namespace Barotrauma public Sprite Sprite { get; private set; } public Sprite RadiationSprite { get; } + private readonly Identifier forceOutpostGenerationParamsIdentifier; + + /// + /// If set to true, only event sets that explicitly define this location type in can be selected at this location. Defaults to false. + /// + public bool IgnoreGenericEvents { get; } + public Color SpriteColor { get; @@ -96,6 +104,7 @@ namespace Barotrauma public LocationType(ContentXElement element, LocationTypesFile file) : base(file, element.GetAttributeIdentifier("identifier", element.Name.LocalName)) { Name = TextManager.Get("LocationName." + Identifier, "unknown"); + Description = TextManager.Get("LocationDescription." + Identifier, ""); BeaconStationChance = element.GetAttributeFloat("beaconstationchance", 0.0f); @@ -110,6 +119,10 @@ namespace Barotrauma ReplaceInRadiation = element.GetAttributeIdentifier(nameof(ReplaceInRadiation), Identifier.Empty); + forceOutpostGenerationParamsIdentifier = element.GetAttributeIdentifier("forceoutpostgenerationparams", Identifier.Empty); + + IgnoreGenericEvents = element.GetAttributeBool(nameof(IgnoreGenericEvents), false); + string teamStr = element.GetAttributeString("outpostteam", "FriendlyNPC"); Enum.TryParse(teamStr, out OutpostTeam); @@ -261,6 +274,15 @@ namespace Barotrauma } } + public OutpostGenerationParams GetForcedOutpostGenerationParams() + { + if (OutpostGenerationParams.OutpostParams.TryGet(forceOutpostGenerationParamsIdentifier, out var parameters)) + { + return parameters; + } + return null; + } + public override void Dispose() { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index 3da878131..cdcfaedab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -68,10 +68,15 @@ namespace Barotrauma public List Locations { get; private set; } + private readonly List locationsDiscovered = new List(); + private readonly List outpostsVisited = new List(); + public List Connections { get; private set; } public Radiation Radiation; + private bool wasLocationDiscoveryOrderTracked = true; + public Map(CampaignSettings settings) { generationParams = MapGenerationParams.Instance; @@ -282,7 +287,12 @@ namespace Barotrauma } } - CurrentLocation.Discover(true); + if (campaign.IsSinglePlayer && campaign.Settings.TutorialEnabled && LocationType.Prefabs.TryGet("tutorialoutpost", out var tutorialOutpost)) + { + CurrentLocation.ChangeType(tutorialOutpost); + } + Discover(CurrentLocation); + Visit(CurrentLocation); CurrentLocation.CreateStores(); foreach (var location in Locations) @@ -820,7 +830,8 @@ namespace Barotrauma SelectedConnection.Passed = true; CurrentLocation = SelectedLocation; - CurrentLocation.Discover(); + Discover(CurrentLocation); + Visit(CurrentLocation); SelectedLocation = null; CurrentLocation.CreateStores(); @@ -851,7 +862,7 @@ namespace Barotrauma Location prevLocation = CurrentLocation; CurrentLocation = Locations[index]; - CurrentLocation.Discover(); + Discover(CurrentLocation); CurrentLocation.CreateStores(); if (prevLocation != CurrentLocation) @@ -1184,6 +1195,51 @@ namespace Barotrauma partial void ClearAnimQueue(); + public void Discover(Location location, bool checkTalents = true) + { + if (location is null) { return; } + if (locationsDiscovered.Contains(location)) { return; } + locationsDiscovered.Add(location); + if (checkTalents) + { + GameSession.GetSessionCrewCharacters(CharacterType.Both).ForEach(c => c.CheckTalents(AbilityEffectType.OnLocationDiscovered, new Location.AbilityLocation(location))); + } + } + + public void Visit(Location location) + { + if (location is null) { return; } + if (!location.HasOutpost()) { return; } + if (outpostsVisited.Contains(location)) { return; } + outpostsVisited.Add(location); + } + + public void ClearLocationHistory() + { + locationsDiscovered.Clear(); + outpostsVisited.Clear(); + } + + public int? GetDiscoveryIndex(Location location) + { + if (!wasLocationDiscoveryOrderTracked) { return null; } + if (location is null) { return -1; } + return locationsDiscovered.IndexOf(location); + } + + public int? GetVisitIndex(Location location) + { + if (!wasLocationDiscoveryOrderTracked) { return null; } + if (location is null) { return -1; } + return outpostsVisited.IndexOf(location); + } + + public bool IsDiscovered(Location location) + { + if (location is null) { return false; } + return locationsDiscovered.Contains(location); + } + /// /// Load a previously saved map from an xml element /// @@ -1211,6 +1267,7 @@ namespace Barotrauma return; } + ClearLocationHistory(); foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -1226,19 +1283,12 @@ namespace Barotrauma } } location.LoadLocationTypeChange(subElement); + + // Backwards compatibility if (subElement.GetAttributeBool("discovered", false)) { - location.Discover(checkTalents: false); - } - if (location.Discovered) - { -#if CLIENT - RemoveFogOfWar(location); -#endif - if (furthestDiscoveredLocation == null || location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X) - { - furthestDiscoveredLocation = location; - } + Discover(location); + wasLocationDiscoveryOrderTracked = false; } Identifier locationType = subElement.GetAttributeIdentifier("type", Identifier.Empty); @@ -1268,6 +1318,36 @@ namespace Barotrauma case "radiation": Radiation = new Radiation(this, generationParams.RadiationParams, subElement); break; + case "discovered": + foreach (var childElement in subElement.GetChildElements("location")) + { + int index = childElement.GetAttributeInt("i", -1); + if (index < 0) { continue; } + if (Locations[index] is not Location l) { continue; } + Discover(l); + } + break; + case "visited": + foreach (var childElement in subElement.GetChildElements("location")) + { + int index = childElement.GetAttributeInt("i", -1); + if (index < 0) { continue; } + if (Locations[index] is not Location l) { continue; } + Visit(l); + } + break; + } + } + + void Discover(Location location) + { + this.Discover(location, checkTalents: false); +#if CLIENT + RemoveFogOfWar(location); +#endif + if (furthestDiscoveredLocation == null || location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X) + { + furthestDiscoveredLocation = location; } } @@ -1343,6 +1423,30 @@ namespace Barotrauma mapElement.Add(Radiation.Save()); } + if (locationsDiscovered.Any()) + { + var discoveryElement = new XElement("discovered"); + foreach (Location location in locationsDiscovered) + { + int index = Locations.IndexOf(location); + var locationElement = new XElement("location", new XAttribute("i", index)); + discoveryElement.Add(locationElement); + } + mapElement.Add(discoveryElement); + } + + if (outpostsVisited.Any()) + { + var visitElement = new XElement("visited"); + foreach (Location location in outpostsVisited) + { + int index = Locations.IndexOf(location); + var locationElement = new XElement("location", new XAttribute("i", index)); + visitElement.Add(locationElement); + } + mapElement.Add(visitElement); + } + element.Add(mapElement); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs index 6e9b95be9..aa8082144 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs @@ -96,6 +96,8 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes), Editable] public string ReplaceInRadiation { get; set; } + public ContentPath OutpostFilePath { get; set; } + public class ModuleCount { public Identifier Identifier; @@ -182,6 +184,7 @@ namespace Barotrauma Name = element.GetAttributeString("name", Identifier.Value); allowedLocationTypes = element.GetAttributeIdentifierArray("allowedlocationtypes", Array.Empty()).ToHashSet(); SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + OutpostFilePath = element.GetAttributeContentPath(nameof(OutpostFilePath)); var humanPrefabCollections = new List>(); foreach (var subElement in element.Elements()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 30217e2a5..9c76e0bb2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -143,7 +143,7 @@ namespace Barotrauma //select which module types the outpost should consist of List pendingModuleFlags = onlyEntrance ? - generationParams.ModuleCounts.First().Identifier.ToEnumerable().ToList() : + (generationParams.ModuleCounts.FirstOrDefault()?.Identifier.ToEnumerable() ?? Enumerable.Empty()).ToList() : SelectModules(outpostModules, generationParams); foreach (Identifier flag in pendingModuleFlags) @@ -246,6 +246,7 @@ namespace Barotrauma var outpostFiles = ContentPackageManager.EnabledPackages.All .SelectMany(p => p.GetFiles()) + .Where(f => !TutorialPrefab.Prefabs.Any(tp => tp.OutpostPath == f.Path)) .OrderBy(f => f.UintIdentifier).ToArray(); if (!outpostFiles.Any()) { @@ -696,6 +697,14 @@ namespace Barotrauma rect.Location += (module.Offset + module.MoveOffset).ToPoint(); rect.Y += module.Bounds.Height; + Vector2? selfGapPos1 = null; + Vector2? selfGapPos2 = null; + if (module.PreviousModule != null) + { + selfGapPos1 = module.Offset + module.ThisGap.Position + module.MoveOffset; + selfGapPos2 = module.PreviousModule.Offset + module.PreviousGap.Position + module.PreviousModule.MoveOffset; + } + foreach (PlacedModule otherModule in modules) { if (otherModule == module || otherModule.PreviousModule == null || otherModule.PreviousModule == module) { continue; } @@ -710,7 +719,17 @@ namespace Barotrauma Vector2 gapPos1 = otherModule.Offset + otherModule.ThisGap.Position + gapEdgeOffset + otherModule.MoveOffset; Vector2 gapPos2 = otherModule.PreviousModule.Offset + otherModule.PreviousGap.Position + gapEdgeOffset + otherModule.PreviousModule.MoveOffset; - if (Submarine.RectContains(rect, gapPos1) || Submarine.RectContains(rect, gapPos2) || MathUtils.GetLineRectangleIntersection(gapPos1, gapPos2, rect, out _)) + if (Submarine.RectContains(rect, gapPos1) || + Submarine.RectContains(rect, gapPos2) || + MathUtils.GetLineRectangleIntersection(gapPos1, gapPos2, rect, out _)) + { + return true; + } + + //check if the connection overlaps with this module's connection + if (selfGapPos1.HasValue && selfGapPos2.HasValue && + !gapPos1.NearlyEquals(gapPos2) && !selfGapPos1.Value.NearlyEquals(selfGapPos2.Value) && + MathUtils.LinesIntersect(gapPos1, gapPos2, selfGapPos1.Value, selfGapPos2.Value)) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs index 38cf694b1..532ce3957 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs @@ -55,7 +55,7 @@ namespace Barotrauma public PriceInfo(int price, bool canBeBought, int minAmount = 0, int maxAmount = 0, bool canBeSpecial = true, int minLevelDifficulty = 0, float buyingPriceMultiplier = 1f, - bool displayNonEmpty = false, string storeIdentifier = null) + bool displayNonEmpty = false, bool requiresUnlock = false, string storeIdentifier = null) { Price = price; CanBeBought = canBeBought; @@ -67,6 +67,7 @@ namespace Barotrauma CanBeSpecial = canBeSpecial; DisplayNonEmpty = displayNonEmpty; StoreIdentifier = new Identifier(storeIdentifier); + RequiresUnlock = requiresUnlock; } public static List CreatePriceInfos(XElement element, out PriceInfo defaultPrice) @@ -81,6 +82,7 @@ namespace Barotrauma float buyingPriceMultiplier = element.GetAttributeFloat("buyingpricemultiplier", 1f); bool displayNonEmpty = element.GetAttributeBool("displaynonempty", false); bool soldByDefault = element.GetAttributeBool("sold", element.GetAttributeBool("soldbydefault", true)); + bool requiresUnlock = element.GetAttributeBool("requiresunlock", false); foreach (XElement childElement in element.GetChildElements("price")) { float priceMultiplier = childElement.GetAttributeFloat("multiplier", 1.0f); @@ -94,26 +96,28 @@ namespace Barotrauma } string storeIdentifier = childElement.GetAttributeString("storeidentifier", backwardsCompatibleIdentifier); // TODO: Add some error messages if we have defined the min or max amount while the item is not sold - var priceInfo = new PriceInfo((int)(priceMultiplier * basePrice), - sold, - sold ? GetMinAmount(childElement, minAmount) : 0, - sold ? GetMaxAmount(childElement, maxAmount) : 0, - canBeSpecial, - storeMinLevelDifficulty, - storeBuyingMultiplier, - displayNonEmpty, - storeIdentifier); + var priceInfo = new PriceInfo(price: (int)(priceMultiplier * basePrice), + canBeBought: sold, + minAmount: sold ? GetMinAmount(childElement, minAmount) : 0, + maxAmount: sold ? GetMaxAmount(childElement, maxAmount) : 0, + canBeSpecial: canBeSpecial, + minLevelDifficulty: storeMinLevelDifficulty, + buyingPriceMultiplier: storeBuyingMultiplier, + displayNonEmpty: displayNonEmpty, + requiresUnlock: requiresUnlock, + storeIdentifier: storeIdentifier); priceInfos.Add(priceInfo); } bool soldElsewhere = soldByDefault && element.GetAttributeBool("soldelsewhere", element.GetAttributeBool("soldeverywhere", false)); - defaultPrice = new PriceInfo(basePrice, - soldElsewhere, - soldElsewhere ? minAmount : 0, - soldElsewhere ? maxAmount : 0, - canBeSpecial, - minLevelDifficulty, - buyingPriceMultiplier, - displayNonEmpty); + defaultPrice = new PriceInfo(price: basePrice, + canBeBought: soldElsewhere, + minAmount: soldElsewhere ? minAmount : 0, + maxAmount: soldElsewhere ? maxAmount : 0, + canBeSpecial: canBeSpecial, + minLevelDifficulty: minLevelDifficulty, + buyingPriceMultiplier: buyingPriceMultiplier, + displayNonEmpty: displayNonEmpty, + requiresUnlock: requiresUnlock); return priceInfos; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index 5ae404712..e2958efdf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -1114,7 +1114,8 @@ namespace Barotrauma { if (item.Submarine != this) { continue; } var pump = item.GetComponent(); - if (pump == null || !item.HasTag("ballast") || item.CurrentHull == null) { continue; } + if (pump == null || item.CurrentHull == null) { continue; } + if (!item.HasTag("ballast") && !item.CurrentHull.RoomName.Contains("ballast", StringComparison.OrdinalIgnoreCase)) { continue; } pump.FlowPercentage = 0.0f; ballastHulls.Add(item.CurrentHull); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index 426941fe8..c40ed58c8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -314,6 +314,7 @@ namespace Barotrauma Tier = original.Tier; IsManuallyOutfitted = original.IsManuallyOutfitted; Tags = original.Tags; + OutpostGenerationParams = original.OutpostGenerationParams; if (original.OutpostModuleInfo != null) { OutpostModuleInfo = new OutpostModuleInfo(original.OutpostModuleInfo); @@ -747,6 +748,8 @@ namespace Barotrauma return doc; } - public static int GetDefaultTier(int price) => price > 20000 ? 3 : price > 10000 ? 2 : 1; + public static int GetDefaultTier(int price) => price > 20000 ? HighestTier : price > 10000 ? 2 : 1; + + public const int HighestTier = 3; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs index 6e5300c4c..8bde51456 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs @@ -27,7 +27,8 @@ namespace Barotrauma.Networking SellSubItems = 0x4000, ManageMap = 0x8000, ManageHires = 0x10000, - All = 0x1FFFF + ManageBotTalents = 0x20000, + All = 0x3FFFF } class PermissionPreset diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index cbf0474b8..e4cbc1bb7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -382,12 +382,17 @@ namespace Barotrauma.Networking private bool autoRestart; - public bool IsPublic; - private int maxPlayers; public List ClientPermissions { get; private set; } = new List(); + [Serialize(true, IsPropertySaveable.Yes)] + public bool IsPublic + { + get; + set; + } + private int tickRate = 20; [Serialize(20, IsPropertySaveable.Yes)] public int TickRate @@ -518,13 +523,20 @@ namespace Barotrauma.Networking } } - [Serialize(Barotrauma.LosMode.Opaque, IsPropertySaveable.Yes)] + [Serialize(LosMode.Opaque, IsPropertySaveable.Yes)] public LosMode LosMode { get; set; } + [Serialize(EnemyHealthBarMode.ShowAll, IsPropertySaveable.Yes)] + public EnemyHealthBarMode ShowEnemyHealthBars + { + get; + set; + } + [Serialize(800, IsPropertySaveable.Yes)] public int LinesPerLogFile { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index e1ec4e598..b327c3903 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -35,10 +35,19 @@ namespace Barotrauma Activity } + public enum EnemyHealthBarMode + { + ShowAll, + BossHealthBarsOnly, + HideAll + } + public static class GameSettings { public struct Config { + public const float DefaultAimAssist = 0.05f; + public static Config GetDefault() { Config config = new Config @@ -50,7 +59,8 @@ namespace Barotrauma SubEditorBackground = new Color(13, 37, 69, 255), EnableSplashScreen = true, PauseOnFocusLost = true, - AimAssistAmount = 0.5f, + AimAssistAmount = DefaultAimAssist, + ShowEnemyHealthBars = EnemyHealthBarMode.ShowAll, EnableMouseLook = true, ChatOpen = true, CrewMenuOpen = true, @@ -110,6 +120,7 @@ namespace Barotrauma public LanguageIdentifier Language; public bool VerboseLogging; public bool SaveDebugConsoleLogs; + public string SavePath; public int SubEditorUndoBuffer; public int MaxAutoSaves; public int AutoSaveIntervalSeconds; @@ -118,6 +129,7 @@ namespace Barotrauma public bool PauseOnFocusLost; public float AimAssistAmount; public bool EnableMouseLook; + public EnemyHealthBarMode ShowEnemyHealthBars; public bool ChatOpen; public bool CrewMenuOpen; public bool EditorDisclaimerShown; diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs index f86226e55..b831c7670 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs @@ -24,7 +24,8 @@ namespace Barotrauma HasSpecifierTag, Affliction, EntityType, - LimbType + LimbType, + SkillRequirement } public enum Comparison @@ -73,6 +74,7 @@ namespace Barotrauma case "targetcontainer": case "targetgrandparent": case "targetcontaineditem": + case "skillrequirement": return false; default: return true; @@ -110,6 +112,11 @@ namespace Barotrauma { Type = ConditionType.Uncertain; } + + if (attribute.Parent.GetAttributeBool("skillrequirement", false)) + { + Type = ConditionType.SkillRequirement; + } AttributeValue = valueString; SplitAttributeValue = valueString.Split(','); @@ -305,25 +312,20 @@ namespace Barotrauma if (health == null) { return false; } var affliction = health.GetAffliction(AttributeName.ToIdentifier()); float afflictionStrength = affliction == null ? 0.0f : affliction.Strength; - if (FloatValue.HasValue) - { - float value = FloatValue.Value; - switch (Operator) - { - case OperatorType.Equals: - return afflictionStrength == value; - case OperatorType.GreaterThan: - return afflictionStrength > value; - case OperatorType.GreaterThanEquals: - return afflictionStrength >= value; - case OperatorType.LessThan: - return afflictionStrength < value; - case OperatorType.LessThanEquals: - return afflictionStrength <= value; - case OperatorType.NotEquals: - return afflictionStrength != value; - } - } + + return ValueMatchesRequirement(afflictionStrength); + } + } + return false; + case ConditionType.SkillRequirement: + { + if (target == null) { return Operator == OperatorType.NotEquals; } + + if (target is Character targetChar) + { + float skillLevel = targetChar.GetSkillLevel(AttributeName.ToIdentifier()); + + return ValueMatchesRequirement(skillLevel); } } return false; @@ -332,6 +334,30 @@ namespace Barotrauma } } + private bool ValueMatchesRequirement(float testedValue) + { + if (FloatValue.HasValue) + { + float value = FloatValue.Value; + switch (Operator) + { + case OperatorType.Equals: + return testedValue == value; + case OperatorType.GreaterThan: + return testedValue > value; + case OperatorType.GreaterThanEquals: + return testedValue >= value; + case OperatorType.LessThan: + return testedValue < value; + case OperatorType.LessThanEquals: + return testedValue <= value; + case OperatorType.NotEquals: + return testedValue != value; + } + } + return false; + } + private bool MatchesTagCondition(ISerializableEntity target) { if (!(target is Item item)) { return Operator == OperatorType.NotEquals; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 3173558d7..581a5ccc5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -877,10 +877,9 @@ namespace Barotrauma return true; } - public IReadOnlyList GetNearbyTargets(Vector2 worldPosition, List targets = null) + public void AddNearbyTargets(Vector2 worldPosition, List targets) { - targets ??= new List(); - if (Range <= 0.0f) { return targets; } + if (Range <= 0.0f) { return; } if (HasTargetType(TargetType.NearbyCharacters)) { foreach (Character c in Character.CharacterList) @@ -917,7 +916,6 @@ namespace Barotrauma } } } - return targets; bool CheckDistance(ISpatialEntity e) { @@ -1362,7 +1360,7 @@ namespace Barotrauma { if (breakLimb) { - targetLimb.character.TrySeverLimbJoints(targetLimb, severLimbsProbability: 1, damage: -1, allowBeheading: true, attacker: user); + targetLimb.character.TrySeverLimbJoints(targetLimb, severLimbsProbability: 1, damage: -1, allowBeheading: true, ignoreSeveranceProbabilityModifier: true, attacker: user); } if (hideLimb) { @@ -2163,7 +2161,7 @@ namespace Barotrauma if (result.Afflictions != null && result.Afflictions.Any(a => a.Prefab == limbAffliction.Prefab) && (!affliction.Prefab.LimbSpecific || limb.character.CharacterHealth.GetAfflictionLimb(affliction) == limb)) { - if (type == ActionType.OnUse) + if (type == ActionType.OnUse || type == ActionType.OnSuccess) { limbAffliction.AppliedAsSuccessfulTreatmentTime = Timing.TotalTime; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs index b4aa8a883..d83ebf7af 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs @@ -310,7 +310,7 @@ namespace Barotrauma.Steam + "unexpected deletion of your hard work.\n" + "Instead, modify a copy of your mod in LocalMods.\n"; - string workshopModDirReadmeLocation = Path.Combine(SaveUtil.SaveFolder, "WorkshopMods", "README.txt"); + string workshopModDirReadmeLocation = Path.Combine(SaveUtil.DefaultSaveFolder, "WorkshopMods", "README.txt"); if (!File.Exists(workshopModDirReadmeLocation)) { Directory.CreateDirectory(Path.GetDirectoryName(workshopModDirReadmeLocation)!); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs index 1aa43ae28..84b97b6b0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs @@ -108,6 +108,8 @@ namespace Barotrauma public readonly bool IsWallUpgrade; public readonly LocalizedString Name; + private readonly object mutex = new object(); + public readonly IEnumerable ItemTags; public UpgradeCategory(ContentXElement element, UpgradeModulesFile file) : base(element, file) @@ -119,7 +121,6 @@ namespace Barotrauma ItemTags = selfItemTags.CollectionConcat(prefabsThatAllowUpgrades); Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", Identifier.Empty); - if (!nameIdentifier.IsEmpty) { Name = TextManager.Get($"{nameIdentifier}"); @@ -132,10 +133,13 @@ namespace Barotrauma public void DeterminePrefabsThatAllowUpgrades() { - prefabsThatAllowUpgrades.Clear(); - prefabsThatAllowUpgrades.UnionWith(ItemPrefab.Prefabs - .Where(it => it.GetAllowedUpgrades().Contains(Identifier)) - .Select(it => it.Identifier)); + lock (mutex) + { + prefabsThatAllowUpgrades.Clear(); + prefabsThatAllowUpgrades.UnionWith(ItemPrefab.Prefabs + .Where(it => it.GetAllowedUpgrades().Contains(Identifier)) + .Select(it => it.Identifier)); + } } public bool CanBeApplied(MapEntity item, UpgradePrefab? upgradePrefab) @@ -153,26 +157,11 @@ namespace Barotrauma if (upgradePrefab != null && upgradePrefab.IsDisallowed(item)) { return false; } - return item.Prefab.GetAllowedUpgrades().Contains(Identifier) || - ItemTags.Any(tag => item.Prefab.Tags.Contains(tag) || item.Prefab.Identifier == tag); - } - - public bool CanBeApplied(XElement element, UpgradePrefab prefab) - { - if ("Structure" == element.NameAsIdentifier()) { return IsWallUpgrade; } - - Identifier identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); - if (identifier.IsEmpty) { return false; } - - ItemPrefab? item = ItemPrefab.Find(null, identifier); - if (item == null) { return false; } - - Identifier[] disallowedUpgrades = element.GetAttributeIdentifierArray("disallowedupgrades", Array.Empty()); - - if (disallowedUpgrades.Any(s => s == Identifier || s == prefab.Identifier)) { return false; } - - return item.GetAllowedUpgrades().Contains(Identifier) || - ItemTags.Any(tag => item.Tags.Contains(tag) || item.Identifier == tag); + lock (mutex) + { + return item.Prefab.GetAllowedUpgrades().Contains(Identifier) || + ItemTags.Any(tag => item.Prefab.Tags.Contains(tag) || item.Prefab.Identifier == tag); + } } public static UpgradeCategory? Find(Identifier identifier) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs index 688c284c5..0d6974172 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs @@ -104,6 +104,8 @@ namespace Barotrauma (float)Math.Floor(value / div) * div; } + public static int RoundToInt(float v) => (int)MathF.Round(v); + public static float RoundTowardsClosest(float value, float div) { return (float)Math.Round(value / div) * div; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/NetCollection.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/NetCollection.cs new file mode 100644 index 000000000..7467600c5 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/NetCollection.cs @@ -0,0 +1,17 @@ +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Barotrauma +{ + [NetworkSerialize] + public readonly record struct NetCollection(ImmutableArray Array) : INetSerializableStruct, IEnumerable + { + public static readonly NetCollection Empty = new(ImmutableArray.Empty); + + public NetCollection(params T[] elements) : this(elements.ToImmutableArray()) { } + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)Array).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)Array).GetEnumerator(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs index 8df000e58..55120383d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs @@ -18,7 +18,7 @@ namespace Barotrauma #if OSX //"/*user*/Library/Application Support/Daedalic Entertainment GmbH/" on Mac - public static readonly string SaveFolder = Path.Combine( + public static readonly string DefaultSaveFolder = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Library", "Application Support", @@ -27,13 +27,13 @@ namespace Barotrauma #else //"C:/Users/*user*/AppData/Local/Daedalic Entertainment GmbH/" on Windows //"/home/*user*/.local/share/Daedalic Entertainment GmbH/" on Linux - public static readonly string SaveFolder = Path.Combine( + public static readonly string DefaultSaveFolder = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Daedalic Entertainment GmbH", "Barotrauma"); #endif - public static string MultiplayerSaveFolder = Path.Combine(SaveFolder, "Multiplayer"); + public static string DefaultMultiplayerSaveFolder = Path.Combine(DefaultSaveFolder, "Multiplayer"); public static readonly string SubmarineDownloadFolder = Path.Combine("Submarines", "Downloaded"); public static readonly string CampaignDownloadFolder = Path.Combine("Data", "Saves", "Multiplayer_Downloaded"); @@ -43,9 +43,9 @@ namespace Barotrauma public static string TempPath { #if SERVER - get { return Path.Combine(SaveFolder, "temp_server"); } + get { return Path.Combine(GetSaveFolder(SaveType.Singleplayer), "temp_server"); } #else - get { return Path.Combine(SaveFolder, "temp"); } + get { return Path.Combine(GetSaveFolder(SaveType.Singleplayer), "temp"); } #endif } @@ -198,7 +198,10 @@ namespace Barotrauma } //deleting a multiplayer save file -> also delete character data - if (Path.GetFullPath(Path.GetDirectoryName(filePath)).Equals(Path.GetFullPath(MultiplayerSaveFolder))) + var fullPath = Path.GetFullPath(Path.GetDirectoryName(filePath)); + + if (fullPath.Equals(Path.GetFullPath(DefaultMultiplayerSaveFolder)) || + fullPath == Path.GetFullPath(GetSaveFolder(SaveType.Multiplayer))) { string characterDataSavePath = MultiPlayerCampaign.GetCharacterDataSavePath(filePath); if (File.Exists(characterDataSavePath)) @@ -215,35 +218,70 @@ namespace Barotrauma } } - public static string GetSavePath(SaveType saveType, string saveName) + public static string GetSaveFolder(SaveType saveType) { - string folder = saveType == SaveType.Singleplayer ? SaveFolder : MultiplayerSaveFolder; - return Path.Combine(folder, saveName); + string folder = string.Empty; + + if (!string.IsNullOrEmpty(GameSettings.CurrentConfig.SavePath)) + { + folder = GameSettings.CurrentConfig.SavePath; + if (saveType == SaveType.Multiplayer) + { + folder = Path.Combine(folder, "Multiplayer"); + } + if (!Directory.Exists(folder)) + { + DebugConsole.AddWarning($"Could not find the custom save folder \"{folder}\", creating the folder..."); + try + { + Directory.CreateDirectory(folder); + } + catch (Exception e) + { + DebugConsole.ThrowError($"Could not find the custom save folder \"{folder}\". Using the default save path instead.", e); + folder = string.Empty; + } + } + } + if (string.IsNullOrEmpty(folder)) + { + folder = saveType == SaveType.Singleplayer ? DefaultSaveFolder : DefaultMultiplayerSaveFolder; + } + return folder; } public static IReadOnlyList GetSaveFiles(SaveType saveType, bool includeInCompatible = true) { - string folder = saveType == SaveType.Singleplayer ? SaveFolder : MultiplayerSaveFolder; - if (!Directory.Exists(folder)) + string defaultFolder = saveType == SaveType.Singleplayer ? DefaultSaveFolder : DefaultMultiplayerSaveFolder; + if (!Directory.Exists(defaultFolder)) { - DebugConsole.Log("Save folder \"" + folder + " not found! Attempting to create a new folder..."); + DebugConsole.Log("Save folder \"" + defaultFolder + " not found! Attempting to create a new folder..."); try { - Directory.CreateDirectory(folder); + Directory.CreateDirectory(defaultFolder); } catch (Exception e) { - DebugConsole.ThrowError("Failed to create the folder \"" + folder + "\"!", e); + DebugConsole.ThrowError("Failed to create the folder \"" + defaultFolder + "\"!", e); } } - List files = Directory.GetFiles(folder, "*.save", System.IO.SearchOption.TopDirectoryOnly).ToList(); + List files = Directory.GetFiles(defaultFolder, "*.save", System.IO.SearchOption.TopDirectoryOnly).ToList(); + + var folder = GetSaveFolder(saveType); + if (!string.IsNullOrEmpty(folder) && Directory.Exists(folder)) + { + files.AddRange(Directory.GetFiles(folder, "*.save", System.IO.SearchOption.TopDirectoryOnly)); + } + string legacyFolder = saveType == SaveType.Singleplayer ? LegacySaveFolder : LegacyMultiplayerSaveFolder; if (Directory.Exists(legacyFolder)) { files.AddRange(Directory.GetFiles(legacyFolder, "*.save", System.IO.SearchOption.TopDirectoryOnly)); } + files = files.Distinct().ToList(); + List saveInfos = new List(); foreach (string file in files) { @@ -305,7 +343,7 @@ namespace Barotrauma { fileName = ToolBox.RemoveInvalidFileNameChars(fileName); - string folder = saveType == SaveType.Singleplayer ? SaveFolder : MultiplayerSaveFolder; + string folder = GetSaveFolder(saveType); if (fileName == "Save_Default") { fileName = TextManager.Get("SaveFile.DefaultName").Value; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index 1f5b69a16..dba2f7ea7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -3,6 +3,7 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using Barotrauma.IO; using System.Linq; @@ -101,7 +102,7 @@ namespace Barotrauma string startPath = directory ?? ""; - string saveFolder = SaveUtil.SaveFolder.Replace('\\', '/'); + string saveFolder = SaveUtil.DefaultSaveFolder.Replace('\\', '/'); if (originalFilename.Replace('\\', '/').StartsWith(saveFolder)) { //paths that lead to the save folder might have incorrect case, @@ -721,5 +722,119 @@ namespace Barotrauma { return TextManager.GetWithVariable("percentageformat", "[value]", ((int)MathF.Round(v * 100)).ToString()).Value; } + + private static readonly ImmutableHashSet affectedCharacters = ImmutableHashSet.Create('%', '+', '%'); + + /// + /// Extends % and + characters to color tags in talent name tooltips to make them look nicer. + /// This obviously does not work in languages like French where a non breaking space is used + /// so it's just a a bit extra for the languages it works with. + /// + /// + /// + public static string ExtendColorToPercentageSigns(string original) + { + const string colorEnd = "‖color:end‖", + colorStart = "‖color:"; + + const char definitionIndicator = '‖'; + + char[] chars = original.ToCharArray(); + + for (int i = 0; i < chars.Length; i++) + { + if (!TryGetAt(i, chars, out char currentChar) || !affectedCharacters.Contains(currentChar)) { continue; } + + // look behind + if (TryGetAt(i - 1, chars, out char c) && c is definitionIndicator) + { + int offset = colorEnd.Length; + + if (MatchesSequence(i - offset, colorEnd, chars)) + { + // push the color end tag forwards until the character is within the tag + char prev = currentChar; + for (int k = i - offset; k <= i; k++) + { + if (!TryGetAt(k, chars, out c)) { continue; } + + chars[k] = prev; + prev = c; + } + continue; + } + } + + // look ahead + if (TryGetAt(i + 1, chars, out c) && c is definitionIndicator) + { + if (!MatchesSequence(i + 1, colorStart, chars)) { continue; } + + int offset = FindNextDefinitionOffset(i, colorStart.Length, chars); + + // we probably reached the end of the string + if (offset > chars.Length) { continue; } + + // push the color start tag back until the character is within the tag + char prev = currentChar; + for (int k = i + offset; k >= i; k--) + { + if (!TryGetAt(k, chars, out c)) { continue; } + + chars[k] = prev; + prev = c; + } + + // skip needlessly checking this section again since we already know what's ahead + i += offset; + } + } + + static int FindNextDefinitionOffset(int index, int initialOffset, char[] chars) + { + int offset = initialOffset; + while (TryGetAt(index + offset, chars, out char c) && c is not definitionIndicator) { offset++; } + return offset; + } + + static bool MatchesSequence(int index, string sequence, char[] chars) + { + for (int i = 0; i < sequence.Length; i++) + { + if (!TryGetAt(index + i, chars, out char c) || c != sequence[i]) { return false; } + } + + return true; + } + + static bool TryGetAt(int i, char[] chars, out char c) + { + if (i >= 0 && i < chars.Length) + { + c = chars[i]; + return true; + } + + c = default; + return false; + } + + return new string(chars); + } + + public static bool StatIdentifierMatches(Identifier original, Identifier match) + { + if (original == match) { return true; } + + for (int i = 0; i < match.Value.Length; i++) + { + if (i >= original.Value.Length) { return match[i] is '~'; } + if (!CharEquals(original[i], match[i])) { return false; } + } + + return false; + + static bool CharEquals(char a, char b) => char.ToLowerInvariant(a) == char.ToLowerInvariant(b); + } } } diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index d8999df6b..9bc167c06 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,128 @@ +--------------------------------------------------------------------------------------------------------- +v0.20.4.0 +--------------------------------------------------------------------------------------------------------- + +Changes and additions: +- Added an objective to give the order to "Operate Turret" alongside other orders in the campaign tutorial. +- Added popups when completing tutorial chapters that allow you to restart or continue and to return back to the menu. +- Rebalanced clothing and apparel resistances. Overall basic clothes will give less resistance. +- Rebalanced damage dealt by tools. Damage should be a bit higher overall. +- SMG can now be crafted. +- Items' damage modifiers are shown in store tooltips. +- Added an option to hide enemy health bars. +- Server visibility can be adjusted in the server lobby (instead of having to restart the server). +- When throwing an item (such as a grenade), the whole throw animation is played before the item is actually launched. Prevents being able to throw items at a ridiculous rate by spamming the hotkeys and LMB. +- Players can select talents for bots in multiplayer. +- Some changes to wrecked item sprites (replacing the old low-res pictures with modified versions of the normal items' sprites). + +Balance: +- Made some weapons available later in game, to increase feeling of progression. + +Unstable only: +- Fixed campaign tutorial progress resetting. +- Fixed tutorial mudraptors spawning in the wrong places. +- Fixed a Role tutorial cabinet staying highlighted after completing the required action. +- Fixed fabricator taking items from linked containers even if there's suitable ones in the input slots. +- Fixed typos in europabrew and sulphuric acid syringes that made them craftable by anyone. +- Fixed several talents that were not working as described. +- Various balance changes to talents. +- Fixed an exploit that allowed making sonar pings undetectable by turning the sonar off after the ping. +- Fixed swapping magazines no longer working due to the addition of the flashlight slots. +- Fixed bots being unable to swap Exosuit's oxygen tanks or use it as divining gear. +- Fixed rifle bullets appearing at the tip of the rifle. +- Fixed limbs sometimes getting severed off mudraptors or other monsters when the target is alive, leading to floating mudraptors. + +Fixed: +- Fixed candidate box not being visible when using the Chinese input method. +- Fixed switching characters interrupting outpost events (even if there's currently no dialog active). +- Fixed certain events preventing other events from triggering when half-finished (e.g. preventing you from unlocking missions when an event is still running and for example waiting for you to talk to some other NPC). +- Fixed other players not seeing the spray particles when someone uses a sprayer in multiplayer. +- Fixed ability to "fire" (just dropping the projectile) hardpoints that are connected to a periscope and loader. + +--------------------------------------------------------------------------------------------------------- +v0.20.3.0 +--------------------------------------------------------------------------------------------------------- + +Changes and additions: +- New weapons: Rifle, Heavy Machine Gun, Machine Pistol, Harpoon Coil-Rifle. +- Limit which submarines are available in each outpost: high-tier subs become available as you get further in the campaign, and the submarine class selection depends on the type of the outpost. +- Added in-game hints for the genetic system. +- Added a button for treating all characters in one go to the medical clinic. +- Affliction descriptions change depending on the strength of the affliction, and whether you're treating someone else or yourself. + +Unstable only: +- Various balance changes to talents. +- Fixed clear button not doing anythign in new talent menu. +- Fixed various XP-giving talents causing console errors at the end of the round. +- Rum can be drunk (not just applied in the health interface). +- Fixed defense bot not aiming properly client-side. +- Fixed some talent items being craftable without the talent. +- Fixed some talent items being sold in outpost stores. +- Replaced some placeholder talent item sprites. +- Fixed "Mailman" talent multiplying mission rewards by 151. +- Fixed container indicator showing how full the 1st slot is when it should show how full the container is. +- Fixed crashing if a fabricator finishes fabricating with no user. +- Fixed skedaddle talent only giving 1 second of the buff instead of 5. +- Fixed clown crate not fit in crate shelf. + +Balance: +- Slightly adjusted values of handheld weapons. + - Power levels match cost better. + - Damage to structures has been revised (f.e. knives shouldn't be so efficient at cutting through walls). + - Some tools are now slightly more damaging and viable as a last resort weapon (don't atually try to fight mudraptors with a wrench though). + - Improved ammo availability for basic weapons. +- Slightly adjusted values of apparel (armor, clothing, diving suits) to better highlight strengths and weaknesses. + - Combat Diving Suit is now actually better for combat than the regular diving suit, due to higher damage resistances. + - PUCS no longer gives a bonus to speed when using Underwater Scooter, as it has plenty of other strengths. + - Mechanic's apparel now has higher laceration protection than Engineer's apparel, as that's typically the damage they'd get from failing to repair. + - All starter clothing gives less protection now, while some shop/npc clothing now gives some benefit. +- Usage of a minimum difficulty level to have some weapons appear in stores only later in the game. Even some previously talent-only items can appear in stores now in very late biomes. +- Plasma cutter is now much better at cutting. + +Bugfixes: +- Fixed a rounding error that caused Health Scanner HUD to display every level of bleeding below 100% as "minor". + +Modding: +- Allow 'launchimpulse' on RangedWeapon to affect projectile's speed (sum of launch impulses). +- Allow 'penetration' on RangedWeapon to affect projectile's penetration (sum of penetration). +- Added 'DontApplyToHands' property to Propulsion, preventing extra force applying to hands when the item is held in hands (instead applying only to the character's whole body). +- Added a skill requirement conditional for StatusEffect, example: to make a status effect occur only if the target has less than 35 weapon skill. +- Added ReloadSkillRequirement and ReloadNoSkill to RangedWeapon. F.e. a weapon with reload=2s, ReloadSkillRequirement=40, ReloadNoSkill=5s will have a character with 20 weapons skill reload at 3.5 s. + +--------------------------------------------------------------------------------------------------------- +v0.20.2.0 +--------------------------------------------------------------------------------------------------------- + +Unstable only: +- Balance improvements to the new talents. +- Fixed mistakes and inconsistencies in talent descriptions. +- Fixed several talents reducing item stats instead of increasing them. +- Fixed console errors caused by the Bounty Hunter talent. +- Fixed Bloody Business not working if you have the Gene Harvester talent unlocked. +- Added a proper sprite for the exosuit. +- Fixed exosuit working after you take out the battery. +- Fixed exosuit working regardless of which slot you put the battery and oxygen tank in. +- Fixed arc emitter sprite when worn on back. +- Fixed console error when hitting a shield with a melee weapon. +- Reduced makeshift shelves to 2 slots to match sprite. +- Reduced amount of talents available through Endocrine Booster usage. +- Add missing recipe unlocks: Ceremonial Sword, Handcannon. +- Moved "Steady Tune" to tier 2, added a new "Trickle Down" talent to the 3rd tier. + +--------------------------------------------------------------------------------------------------------- +v0.20.1.0 +--------------------------------------------------------------------------------------------------------- + +Tutorial improvements: +- A new campaign-integrated tutorial that teaches the basics of the campaign mode in the first outpost. 1st version: feedback and issue reports are much appreciated! +- Various fixes and improvements to the Basic and Role tutorials. + +Unstable only: +- Made saline a bit more effective (the nerf in the previous unstable version was a bit too much). +- Fixed ranged creature attacks causing a crash when the creature is targeting a hull. +- Fixed monsters with no inventory (e.g. watcher, hammerhead spawns) causing a crash. +- Most of the talent fixes and improvements we've been doing didn't make it into this build, but they're coming soon! + --------------------------------------------------------------------------------------------------------- v0.20.0.0 --------------------------------------------------------------------------------------------------------- @@ -132,6 +257,30 @@ Fixes to new issues in the previous build: - Fixed clients sometimes failing to reconnect to the server if the connection is momentarily lost. - Fixed item conditions resetting to 100% between rounds. - Fixed mineral missions always causing a crash. +feature/combat-n-handhelds-balance branch changes +--------------------------------------------------------------------------------------------------------- + +Balance: +- Slightly adjusted values of handheld weapons +-- Power levels match cost better +-- Damage to structures has been revised (f.e. knives shouldn't be so efficient at cutting through walls) +-- Some tools are now slightly more damaging and viable as a last resort weapon (don't atually try to fight mudraptors with a wrench though) +-- Improved ammo availability for basic weapons +- Slightly adjusted values of apparel (armor, clothing, diving suits) to better highlight strengths and weaknesses +-- Combat Diving Suit is now actually better for combat than the regular diving suit, due to higher damage resistances +-- PUCS no longer gives a bonus to speed when using Underwater Scooter, as it has plenty of other strengths +-- Mechanic's apparel now has higher laceration protection than Engineer's apparel, as that's typically the damage they'd get from failing to repair +-- All starter clothing gives less protection now, while some shop/npc clothing now gives some benefit +- Usage of a minimum difficulty level to have some weapons appear in stores only later in the game. Even some previously talent-only items can appear in stores now in very late biomes. +- Plasma cutter is now much better at cutting + +Modding: +- Allow 'launchimpulse' on RangedWeapon to affect projectile's speed (sum of launch impulses) +- Allow 'penetration' on RangedWeapon to affect projectile's penetration (sum of penetration) +- Added 'ApplyToHands' property to Propulsion (default=true), preventing extra force applying to hands when the item is held in hands when set to false (instead applying only to the character's whole body) +- Added a skill requirement conditional for StatusEffect, example: to make a status effect occur only if the target has less than 35 weapon skill +- Added ReloadSkillRequirement and ReloadNoSkill to RangedWeapon. F.e. a weapon with reload=2s, ReloadSkillRequirement=40, ReloadNoSkill=5s will have a character with 20 weapons skill reload at 3.5s + --------------------------------------------------------------------------------------------------------- v0.19.8.0 @@ -461,7 +610,8 @@ Changes: - Handheld sonars can't detect minerals from inside the sub. - Changed the plus and minus button in the campaign settings into arrows. The button on the right increases difficulty, which in the case of the starting balance and supplies means reducing them, making the plus and minus buttons misleading. - Reduced costs of handheld weapon ammunition significantly. -- Slightly reduced effectiveness of harpoons and revolver round to compensate for the cheaper ammo. +- Up revolver & harpoon shop availability (especially at military outposts / armory merchants) +- Slightly reduced effectiveness of harpoons and revolver round to compensate for the cheaper / more available ammo. - Changed recipes for Handcannon, Assault Rifle and Auto-Shotgun. Weapon crafting is more expensive, to compensate for cheaper ammo. - Adjusted numerous other recipes and price costs of materials. Previously little used materials (like tin) are now used more. - Partially reintroduced the "toggle inventory" keybind, now called "toggle entity list". Even though toggling the in-game inventory is no longer possible, the keybind can be used to change the hotkey for toggling the sub editor's entity list. diff --git a/Barotrauma/BarotraumaShared/hintmanager.xml b/Barotrauma/BarotraumaShared/hintmanager.xml index ed6c346cc..3f843afaa 100644 --- a/Barotrauma/BarotraumaShared/hintmanager.xml +++ b/Barotrauma/BarotraumaShared/hintmanager.xml @@ -58,6 +58,11 @@ + + + + + diff --git a/Barotrauma/BarotraumaTest/GenericToolBoxTests.cs b/Barotrauma/BarotraumaTest/GenericToolBoxTests.cs new file mode 100644 index 000000000..305187f05 --- /dev/null +++ b/Barotrauma/BarotraumaTest/GenericToolBoxTests.cs @@ -0,0 +1,55 @@ +#nullable enable + +using System; +using Xunit; +using Barotrauma; +using FluentAssertions; +using FsCheck; + +namespace TestProject; + +public sealed class GenericToolBoxTests +{ + public class CustomGenerators + { + public static Arbitrary IdentifierPairGenerator() + { + return Arb.From(from Identifier first in Arb.Generate() + from Identifier second in Arb.Generate().Where(second => second != first) + select new DifferentIdentifierPair(first, second)); + } + } + + public readonly struct DifferentIdentifierPair + { + public readonly Identifier First, + Second; + + public DifferentIdentifierPair(Identifier first, Identifier second) + { + if (first == second) { throw new InvalidOperationException("Identifiers must be different"); } + + First = first; + Second = second; + } + } + + public GenericToolBoxTests() + { + Arb.Register(); + Arb.Register(); + } + + [Fact] + public void MatchesStatIdentifier() + { + Prop.ForAll(static pair => + { + ToolBox.StatIdentifierMatches(pair.First, $"{pair.First}~{pair.Second}".ToIdentifier()).Should().BeTrue(); + ToolBox.StatIdentifierMatches(pair.First, pair.First).Should().BeTrue(); + + ToolBox.StatIdentifierMatches(pair.First, $"{pair.Second}~{pair.First}".ToIdentifier()).Should().BeFalse(); + ToolBox.StatIdentifierMatches(pair.First, pair.Second).Should().BeFalse(); + }).VerboseCheckThrowOnFailure(); + } +} diff --git a/Barotrauma/BarotraumaTest/TestProject.cs b/Barotrauma/BarotraumaTest/TestProject.cs index 614b0b23b..6b36c44e1 100644 --- a/Barotrauma/BarotraumaTest/TestProject.cs +++ b/Barotrauma/BarotraumaTest/TestProject.cs @@ -15,6 +15,12 @@ namespace TestProject select new Vector2(x, y)); } + public static Arbitrary IdentifierGenerator() + { + return Arb.From(from string value in Arb.Generate().Where(static s => s != null) + select new Identifier(value)); + } + public static Arbitrary ColorGenerator() { return Arb.From(from int r in Gen.Choose(0, 255) diff --git a/Libraries/ImeSharp/IMEString.cs b/Libraries/ImeSharp/IMEString.cs new file mode 100644 index 000000000..6635c85ae --- /dev/null +++ b/Libraries/ImeSharp/IMEString.cs @@ -0,0 +1,201 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace ImeSharp +{ + public unsafe struct IMEString : IEnumerable + { + internal const int IMECharBufferSize = 64; + + public static readonly IMEString Empty = new IMEString((List)null); + + internal struct Enumerator : IEnumerator + { + private IMEString _imeString; + private char _currentCharacter; + private int _currentIndex; + + public Enumerator(IMEString imeString) + { + _imeString = imeString; + _currentCharacter = '\0'; + _currentIndex = -1; + } + + public bool MoveNext() + { + int size = _imeString.Count; + + _currentIndex++; + + if (_currentIndex == size) + return false; + + fixed (char* ptr = _imeString.buffer) + { + _currentCharacter = *(ptr + _currentIndex); + } + + return true; + } + + public void Reset() + { + _currentIndex = -1; + } + + public void Dispose() + { + } + + public char Current { get { return _currentCharacter; } } + object IEnumerator.Current { get { return Current; } } + } + + public int Count { get { return _size; } } + + public char this[int index] + { + get + { + if (index >= Count || index < 0) + throw new ArgumentOutOfRangeException("index"); + + fixed (char* ptr = buffer) + { + return *(ptr + index); + } + } + } + + private int _size; + + fixed char buffer[IMECharBufferSize]; + + public IMEString(string characters) + { + if (string.IsNullOrEmpty(characters)) + { + _size = 0; + return; + } + + _size = characters.Length; + if (_size > IMECharBufferSize) + _size = IMECharBufferSize - 1; + + fixed (char* _ptr = buffer) + { + char* ptr = _ptr; + for (var i = 0; i < _size; i++) + { + *ptr = characters[i]; + ptr++; + } + } + } + + public IMEString(List characters) + { + if (characters == null || characters.Count == 0) + { + _size = 0; + return; + } + + _size = characters.Count; + if (_size > IMECharBufferSize) + _size = IMECharBufferSize - 1; + + fixed (char* _ptr = buffer) + { + char* ptr = _ptr; + for (var i = 0; i < _size; i++) + { + *ptr = characters[i]; + ptr++; + } + } + } + + public IMEString(char[] characters, int count) + { + if (characters == null || count <= 0) + { + _size = 0; + return; + } + + _size = count; + if (_size > IMECharBufferSize) + _size = IMECharBufferSize - 1; + + if (_size > characters.Length) + _size = characters.Length; + + fixed (char* _ptr = buffer) + { + char* ptr = _ptr; + for (var i = 0; i < _size; i++) + { + *ptr = characters[i]; + ptr++; + } + } + } + + public IMEString(IntPtr bStrPtr) + { + if (bStrPtr == IntPtr.Zero) + { + _size = 0; + return; + } + + var ptrSrc = (char*)bStrPtr; + + int i = 0; + + fixed (char* _ptr = buffer) + { + char* ptr = _ptr; + + while (ptrSrc[i] != '\0') + { + *ptr = ptrSrc[i]; + i++; + ptr++; + } + } + + _size = i; + } + + public override string ToString() + { + fixed (char* ptr = buffer) + { + return new string(ptr, 0, _size); + } + } + + public IntPtr ToIntPtr() + { + fixed (char* ptr = buffer) + { + return (IntPtr)ptr; + } + } + + public IEnumerator GetEnumerator() + { + return new Enumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/Libraries/ImeSharp/IMETextCompositionEventArgs.cs b/Libraries/ImeSharp/IMETextCompositionEventArgs.cs new file mode 100644 index 000000000..93548b0be --- /dev/null +++ b/Libraries/ImeSharp/IMETextCompositionEventArgs.cs @@ -0,0 +1,61 @@ +using System; + +namespace ImeSharp +{ + /// + /// Arguments for the event. + /// + public struct IMETextCompositionEventArgs + { + /// + // Construct a TextCompositionEventArgs with composition infos. + /// + public IMETextCompositionEventArgs(IMEString compositionText, + int cursorPosition, + IMEString[] candidateList = null, + int candidatePageStart = 0, + int candidatePageSize = 0, + int candidateSelection = 0) + { + CompositionText = compositionText; + CursorPosition = cursorPosition; + + CandidateList = candidateList; + CandidatePageStart = candidatePageStart; + CandidatePageSize = candidatePageSize; + CandidateSelection = candidateSelection; + } + + /// + /// The full string as it's composed by the IMM. + /// + public readonly IMEString CompositionText; + + /// + /// The position of the cursor inside the composed string. + /// + public readonly int CursorPosition; + + /// + /// The candidate text list for the composition. + /// This property is only supported on WindowsDX and WindowsUniversal. + /// If the composition string does not generate candidates this array is empty. + /// + public readonly IMEString[] CandidateList; + + /// + /// First candidate index of current page. + /// + public readonly int CandidatePageStart; + + /// + /// How many candidates should display per page. + /// + public readonly int CandidatePageSize; + + /// + /// The selected candidate index. + /// + public readonly int CandidateSelection; + } +} diff --git a/Libraries/ImeSharp/IMETextInputEventArgs.cs b/Libraries/ImeSharp/IMETextInputEventArgs.cs new file mode 100644 index 000000000..2d415a21e --- /dev/null +++ b/Libraries/ImeSharp/IMETextInputEventArgs.cs @@ -0,0 +1,12 @@ +namespace ImeSharp +{ + public struct IMETextInputEventArgs + { + public IMETextInputEventArgs(char character) + { + Character = character; + } + + public readonly char Character; + } +} \ No newline at end of file diff --git a/Libraries/ImeSharp/ImeSharp.csproj b/Libraries/ImeSharp/ImeSharp.csproj new file mode 100644 index 000000000..8cd198465 --- /dev/null +++ b/Libraries/ImeSharp/ImeSharp.csproj @@ -0,0 +1,30 @@ + + + + net6.0 + true + + + + C# wrapper for Windows IME APIs. Its goal is to support both IMM32 and TSF. + IME;netcoreapp3.1;net5.0;net6.0;winforms;windows;tsf;imm32 + ImeSharp + https://github.com/ryancheung/ImeSharp + https://github.com/ryancheung/ImeSharp + ryancheung + MIT + + + + ..\Artifacts + ImeSharp + ImeSharp + 5 + + + + + + + + diff --git a/Libraries/ImeSharp/Imm32Manager.cs b/Libraries/ImeSharp/Imm32Manager.cs new file mode 100644 index 000000000..9cf0c2768 --- /dev/null +++ b/Libraries/ImeSharp/Imm32Manager.cs @@ -0,0 +1,349 @@ +using System; +using System.Buffers; +using System.Runtime.InteropServices; +using System.Globalization; +using System.Diagnostics; +using ImeSharp.Native; + +namespace ImeSharp +{ + internal class Imm32Manager + { + + // If the system is IMM enabled, this is true. + private static bool _immEnabled = SafeSystemMetrics.IsImmEnabled; + + public static bool ImmEnabled { get { return _immEnabled; } } + + public const int LANG_CHINESE = 0x04; + public const int LANG_KOREAN = 0x12; + public const int LANG_JAPANESE = 0x11; + + public static int PRIMARYLANGID(int lgid) + { + return ((ushort)(lgid) & 0x3ff); + } + + static Imm32Manager() + { + SetCurrentCulture(); + } + + /// + /// return true if the current keyboard layout is a real IMM32-IME. + /// + public static bool IsImm32ImeCurrent() + { + if (!_immEnabled) + return false; + + IntPtr hkl = NativeMethods.GetKeyboardLayout(0); + + return IsImm32Ime(hkl); + } + + /// + /// return true if the keyboard layout is a real IMM32-IME. + /// + public static bool IsImm32Ime(IntPtr hkl) + { + if (hkl == IntPtr.Zero) + return false; + + return ((NativeMethods.IntPtrToInt32(hkl) & 0xf0000000) == 0xe0000000); + } + + private static int _inputLanguageId; + + internal static void SetCurrentCulture() + { + var hkl = NativeMethods.GetKeyboardLayout(0); + _inputLanguageId = NativeMethods.IntPtrToInt32(hkl) & 0xFFFF; + } + + private IntPtr _windowHandle; + + private IntPtr _defaultImc; + private IntPtr DefaultImc + { + get + { + if (_defaultImc == IntPtr.Zero) + { + IntPtr himc = NativeMethods.ImmCreateContext(); + + // Store the default imc to _defaultImc. + _defaultImc = himc; + } + return _defaultImc; + } + } + + private static ImmCompositionStringHandler _compositionStringHandler; + private static ImmCompositionIntHandler _compositionCursorHandler; + + private bool _lastImmOpenStatus; + + public Imm32Manager(IntPtr windowHandle) + { + _windowHandle = windowHandle; + + _compositionStringHandler = new ImmCompositionStringHandler(DefaultImc, NativeMethods.GCS_COMPSTR); + _compositionCursorHandler = new ImmCompositionIntHandler(DefaultImc, NativeMethods.GCS_CURSORPOS); + } + + public static Imm32Manager Current + { + get + { + var defaultImm32Manager = InputMethod.DefaultImm32Manager; + + if (defaultImm32Manager == null) + { + defaultImm32Manager = new Imm32Manager(InputMethod.WindowHandle); + InputMethod.DefaultImm32Manager = defaultImm32Manager; + } + + return defaultImm32Manager; + } + } + + public void Enable() + { + if (DefaultImc != IntPtr.Zero) + { + // Create a temporary system caret + NativeMethods.CreateCaret(_windowHandle, IntPtr.Zero, 2, 10); + NativeMethods.ImmAssociateContext(_windowHandle, _defaultImc); + } + } + + public void Disable() + { + NativeMethods.ImmAssociateContext(_windowHandle, IntPtr.Zero); + NativeMethods.DestroyCaret(); + } + + const int kCaretMargin = 1; + + // Set candidate window position. + // Borrowed from https://github.com/chromium/chromium/blob/master/ui/base/ime/win/imm32_manager.cc + public void SetCandidateWindow(TsfSharp.Rect caretRect) + { + int x = caretRect.Left; + int y = caretRect.Top; + + if (PRIMARYLANGID(_inputLanguageId) == LANG_CHINESE) + { + // Chinese IMEs ignore function calls to ::ImmSetCandidateWindow() + // when a user disables TSF (Text Service Framework) and CUAS (Cicero + // Unaware Application Support). + // On the other hand, when a user enables TSF and CUAS, Chinese IMEs + // ignore the position of the current system caret and uses the + // parameters given to ::ImmSetCandidateWindow() with its 'dwStyle' + // parameter CFS_CANDIDATEPOS. + // Therefore, we do not only call ::ImmSetCandidateWindow() but also + // set the positions of the temporary system caret. + var candidateForm = new NativeMethods.CANDIDATEFORM(); + candidateForm.dwStyle = NativeMethods.CFS_CANDIDATEPOS; + candidateForm.ptCurrentPos.X = x; + candidateForm.ptCurrentPos.Y = y; + NativeMethods.ImmSetCandidateWindow(_defaultImc, ref candidateForm); + } + + if (PRIMARYLANGID(_inputLanguageId) == LANG_JAPANESE) + NativeMethods.SetCaretPos(x, caretRect.Bottom); + else + NativeMethods.SetCaretPos(x, y); + + // Set composition window position also to ensure move the candidate window position. + var compositionForm = new NativeMethods.COMPOSITIONFORM(); + compositionForm.dwStyle = NativeMethods.CFS_POINT; + compositionForm.ptCurrentPos.X = x; + compositionForm.ptCurrentPos.Y = y; + NativeMethods.ImmSetCompositionWindow(_defaultImc, ref compositionForm); + + if (PRIMARYLANGID(_inputLanguageId) == LANG_KOREAN) + { + // Chinese IMEs and Japanese IMEs require the upper-left corner of + // the caret to move the position of their candidate windows. + // On the other hand, Korean IMEs require the lower-left corner of the + // caret to move their candidate windows. + y += kCaretMargin; + } + + // Need to return here since some Chinese IMEs would stuck if set + // candidate window position with CFS_EXCLUDE style. + if (PRIMARYLANGID(_inputLanguageId) == LANG_CHINESE) return; + + // Japanese IMEs and Korean IMEs also use the rectangle given to + // ::ImmSetCandidateWindow() with its 'dwStyle' parameter CFS_EXCLUDE + // to move their candidate windows when a user disables TSF and CUAS. + // Therefore, we also set this parameter here. + var excludeRectangle = new NativeMethods.CANDIDATEFORM(); + compositionForm.dwStyle = NativeMethods.CFS_EXCLUDE; + compositionForm.ptCurrentPos.X = x; + compositionForm.ptCurrentPos.Y = y; + compositionForm.rcArea.Left = x; + compositionForm.rcArea.Top = y; + compositionForm.rcArea.Right = caretRect.Right; + compositionForm.rcArea.Bottom = caretRect.Bottom; + NativeMethods.ImmSetCandidateWindow(_defaultImc, ref excludeRectangle); + } + + internal bool ProcessMessage(IntPtr hWnd, uint msg, ref IntPtr wParam, ref IntPtr lParam) + { + switch (msg) + { + case NativeMethods.WM_INPUTLANGCHANGE: + SetCurrentCulture(); + break; + case NativeMethods.WM_IME_SETCONTEXT: + if (wParam.ToInt32() == 1 && InputMethod.Enabled) + { + // Must re-associate ime context or things won't work. + NativeMethods.ImmAssociateContext(_windowHandle, DefaultImc); + + if (_lastImmOpenStatus) + NativeMethods.ImmSetOpenStatus(DefaultImc, true); + + var lParam64 = lParam.ToInt64(); + if (!InputMethod.ShowOSImeWindow) + lParam64 &= ~NativeMethods.ISC_SHOWUICANDIDATEWINDOW; + else + lParam64 &= ~NativeMethods.ISC_SHOWUICOMPOSITIONWINDOW; + lParam = (IntPtr)(int)lParam64; + } + break; + case NativeMethods.WM_KILLFOCUS: + _lastImmOpenStatus = NativeMethods.ImmGetOpenStatus(DefaultImc); + break; + case NativeMethods.WM_IME_NOTIFY: + IMENotify(wParam.ToInt32()); + if (!InputMethod.ShowOSImeWindow) + return true; + break; + case NativeMethods.WM_IME_STARTCOMPOSITION: + //Debug.WriteLine("NativeMethods.WM_IME_STARTCOMPOSITION"); + IMEStartComposion(lParam.ToInt32()); + // Force to not show composition window, `lParam64 &= ~ISC_SHOWUICOMPOSITIONWINDOW` don't work sometime. + return true; + case NativeMethods.WM_IME_COMPOSITION: + //Debug.WriteLine("NativeMethods.WM_IME_COMPOSITION"); + IMEComposition(lParam.ToInt32()); + break; + case NativeMethods.WM_IME_ENDCOMPOSITION: + //Debug.WriteLine("NativeMethods.WM_IME_ENDCOMPOSITION"); + IMEEndComposition(lParam.ToInt32()); + if (!InputMethod.ShowOSImeWindow) + return true; + break; + } + + return false; + } + + private void IMENotify(int WParam) + { + switch (WParam) + { + case NativeMethods.IMN_OPENCANDIDATE: + case NativeMethods.IMN_CHANGECANDIDATE: + IMEChangeCandidate(); + break; + case NativeMethods.IMN_CLOSECANDIDATE: + InputMethod.ClearCandidates(); + break; + default: + break; + } + } + + private void IMEChangeCandidate() + { + if (TextServicesLoader.ServicesInstalled) // TSF is enabled + { + if (!TextStore.Current.SupportUIElement) // But active IME not support UIElement + UpdateCandidates(); // We have to fetch candidate list here. + + return; + } + + // Normal candidate list fetch in IMM32 + UpdateCandidates(); + // Send event on candidate updates + InputMethod.OnTextComposition(this, new IMEString(_compositionStringHandler.Values, _compositionStringHandler.Count), _compositionCursorHandler.Value); + + if (InputMethod.CandidateList != null) + ArrayPool.Shared.Return(InputMethod.CandidateList); + } + + private unsafe void UpdateCandidates() + { + uint length = NativeMethods.ImmGetCandidateList(DefaultImc, 0, IntPtr.Zero, 0); + if (length > 0) + { + IntPtr pointer = Marshal.AllocHGlobal((int)length); + length = NativeMethods.ImmGetCandidateList(DefaultImc, 0, pointer, length); + NativeMethods.CANDIDATELIST* cList = (NativeMethods.CANDIDATELIST*)pointer; + + var selection = (int)cList->dwSelection; + var pageStart = (int)cList->dwPageStart; + var pageSize = (int)cList->dwPageSize; + + selection -= pageStart; + + IMEString[] candidates = ArrayPool.Shared.Rent(pageSize); + + int i, j; + for (i = pageStart, j = 0; i < cList->dwCount && j < pageSize; i++, j++) + { + int sOffset = Marshal.ReadInt32(pointer, 24 + 4 * i); + candidates[j] = new IMEString(pointer + sOffset); + } + + //Debug.WriteLine("IMM========IMM"); + //Debug.WriteLine("pageStart: {0}, pageSize: {1}, selection: {2}, candidates:", pageStart, pageSize, selection); + //for (int k = 0; k < candidates.Length; k++) + // Debug.WriteLine(" {2}{0}.{1}", k + 1, candidates[k], k == selection ? "*" : ""); + //Debug.WriteLine("IMM++++++++IMM"); + + InputMethod.CandidatePageStart = pageStart; + InputMethod.CandidatePageSize = pageSize; + InputMethod.CandidateSelection = selection; + InputMethod.CandidateList = candidates; + + Marshal.FreeHGlobal(pointer); + } + } + + private void ClearComposition() + { + _compositionStringHandler.Clear(); + } + + private void IMEStartComposion(int lParam) + { + InputMethod.OnTextCompositionStarted(this); + ClearComposition(); + } + + private void IMEComposition(int lParam) + { + if (_compositionStringHandler.Update(lParam)) + { + _compositionCursorHandler.Update(); + + InputMethod.OnTextComposition(this, new IMEString(_compositionStringHandler.Values, _compositionStringHandler.Count), _compositionCursorHandler.Value); + } + } + + private void IMEEndComposition(int lParam) + { + InputMethod.ClearCandidates(); + ClearComposition(); + + InputMethod.OnTextCompositionEnded(this); + } + } +} diff --git a/Libraries/ImeSharp/ImmCompositionResultHandler.cs b/Libraries/ImeSharp/ImmCompositionResultHandler.cs new file mode 100644 index 000000000..69d3d651b --- /dev/null +++ b/Libraries/ImeSharp/ImmCompositionResultHandler.cs @@ -0,0 +1,119 @@ +using System; +using System.Text; +using System.Runtime.InteropServices; +using ImeSharp.Native; + +namespace ImeSharp +{ + internal abstract class ImmCompositionResultHandler + { + protected IntPtr _imeContext; + + public int Flag { get; private set; } + + internal ImmCompositionResultHandler(IntPtr imeContext, int flag) + { + this.Flag = flag; + _imeContext = imeContext; + } + + internal virtual void Update() { } + + internal bool Update(int lParam) + { + if ((lParam & Flag) == Flag) + { + Update(); + return true; + } + return false; + } + } + + internal class ImmCompositionStringHandler : ImmCompositionResultHandler + { + internal const int BufferSize = 1024; + private byte[] _byteBuffer = new byte[BufferSize]; + private int _byteCount; + + private char[] _charBuffer = new char[BufferSize / 2]; + private int _charCount; + + public char[] Values { get { return _charBuffer; } } + public int Count { get { return _charCount; } } + + public char this[int index] + { + get + { + if (index >= _charCount || index < 0) + throw new ArgumentOutOfRangeException("index"); + + return _charBuffer[index]; + } + } + + internal ImmCompositionStringHandler(IntPtr imeContext, int flag) : base(imeContext, flag) + { + } + + public override string ToString() + { + if (_charCount <= 0) + return string.Empty; + + return new string(_charBuffer, 0, _charCount); + } + + internal void Clear() + { + Array.Clear(_byteBuffer, 0, _byteCount); + _byteCount = 0; + + Array.Clear(_charBuffer, 0, _charCount); + _charCount = 0; + } + + internal override void Update() + { + _byteCount = NativeMethods.ImmGetCompositionString(_imeContext, Flag, IntPtr.Zero, 0); + IntPtr pointer = Marshal.AllocHGlobal(_byteCount); + + try + { + Array.Clear(_byteBuffer, 0, _byteCount); + + if (_byteCount > 0) + { + NativeMethods.ImmGetCompositionString(_imeContext, Flag, pointer, _byteCount); + + Marshal.Copy(pointer, _byteBuffer, 0, _byteCount); + + Array.Clear(_charBuffer, 0, _charCount); + _charCount = Encoding.Unicode.GetChars(_byteBuffer, 0, _byteCount, _charBuffer, 0); + } + } + finally + { + Marshal.FreeHGlobal(pointer); + } + } + } + + internal class ImmCompositionIntHandler : ImmCompositionResultHandler + { + public int Value { get; private set; } + + internal ImmCompositionIntHandler(IntPtr imeContext, int flag) : base(imeContext, flag) { } + + public override string ToString() + { + return Value.ToString(); + } + + internal override void Update() + { + Value = NativeMethods.ImmGetCompositionString(_imeContext, Flag, IntPtr.Zero, 0); + } + } +} diff --git a/Libraries/ImeSharp/InputMethod.cs b/Libraries/ImeSharp/InputMethod.cs new file mode 100644 index 000000000..fa178cbd7 --- /dev/null +++ b/Libraries/ImeSharp/InputMethod.cs @@ -0,0 +1,246 @@ +using System; +using System.Globalization; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using ImeSharp.Native; + +namespace ImeSharp +{ + public static class InputMethod + { + private static IntPtr _windowHandle; + public static IntPtr WindowHandle { get { return _windowHandle; } } + + private static IntPtr _prevWndProc; + private static NativeMethods.WndProcDelegate _wndProcDelegate; + + private static TextServicesContext _textServicesContext; + internal static TextServicesContext TextServicesContext + { + get { return _textServicesContext; } + set { _textServicesContext = value; } + } + + private static TextStore _defaultTextStore; + internal static TextStore DefaultTextStore + { + get { return _defaultTextStore; } + set { _defaultTextStore = value; } + } + + private static Imm32Manager _defaultImm32Manager; + internal static Imm32Manager DefaultImm32Manager + { + get { return _defaultImm32Manager; } + set { _defaultImm32Manager = value; } + } + + private static bool _enabled; + public static bool Enabled + { + get { return _enabled; } + set + { + if (_enabled == value) return; + + _enabled = value; + + EnableOrDisableInputMethod(_enabled); + } + } + + internal static TsfSharp.Rect TextInputRect; + + /// + /// Set the position of the candidate window rendered by the OS. + /// Let the OS render the candidate window by set param "showOSImeWindow" to true on . + /// + public static void SetTextInputRect(int x, int y, int width, int height) + { + if (!_showOSImeWindow) return; + + TextInputRect.Left = x; + TextInputRect.Top = y; + TextInputRect.Right = x + width; + TextInputRect.Bottom = y + height; + + if (Imm32Manager.ImmEnabled) + Imm32Manager.Current.SetCandidateWindow(TextInputRect); + } + + private static bool _showOSImeWindow = false; + + /// + /// Return if let OS render IME Candidate window or not. + /// + public static bool ShowOSImeWindow { get { return _showOSImeWindow; } } + + internal static int CandidatePageStart; + internal static int CandidatePageSize; + internal static int CandidateSelection; + internal static IMEString[] CandidateList; + + internal static void ClearCandidates() + { + CandidateList = null; + CandidatePageStart = 0; + CandidatePageSize = 0; + CandidateSelection = 0; + } + + public static event EventHandler TextComposition; + public static event EventHandler TextInput; + public static event EventHandler CommitTextComposition; + + public static TextInputCallback TextInputCallback { get; set; } + public static TextCompositionCallback TextCompositionCallback { get; set; } + public static CommitTextCompositionCallback CommitTextCompositionCallback { get; set; } + + /// + /// Initialize InputMethod with a Window Handle. + /// Let the OS render the candidate window by set to true. + /// + public static void Initialize(IntPtr windowHandle, bool showOSImeWindow = true) + { + if (_windowHandle != IntPtr.Zero) + throw new InvalidOperationException("InputMethod can only be initialized once!"); + + _windowHandle = windowHandle; + _showOSImeWindow = showOSImeWindow; + + _wndProcDelegate = new NativeMethods.WndProcDelegate(WndProc); + _prevWndProc = (IntPtr)NativeMethods.SetWindowLongPtr(_windowHandle, NativeMethods.GWL_WNDPROC, + Marshal.GetFunctionPointerForDelegate(_wndProcDelegate)); + } + + internal static void OnTextInput(object sender, char character) + { + if (TextInput != null) + TextInput.Invoke(sender, new IMETextInputEventArgs(character)); + + if (TextInputCallback != null) + TextInputCallback(character); + } + + // Some Chinese IME only send composition start event but no composition update event. + // We need this to ensure candidate window position can be set in time. + internal static void OnTextCompositionStarted(object sender) + { + if (TextComposition != null) + TextComposition.Invoke(sender, new IMETextCompositionEventArgs(IMEString.Empty, 0)); + + if (TextCompositionCallback != null) + TextCompositionCallback(IMEString.Empty, 0, null, 0, 0, 0); + } + + // On text composition update. + internal static void OnTextComposition(object sender, IMEString compositionText, int cursorPos) + { + if (compositionText.Count == 0) // Crash guard + cursorPos = 0; + + if (cursorPos > compositionText.Count) // Another crash guard + cursorPos = compositionText.Count; + + if (TextComposition != null) + { + TextComposition.Invoke(sender, + new IMETextCompositionEventArgs(compositionText, cursorPos, CandidateList, CandidatePageStart, CandidatePageSize, CandidateSelection)); + } + + if (TextCompositionCallback != null) + TextCompositionCallback(compositionText, cursorPos, CandidateList, CandidatePageStart, CandidatePageSize, CandidateSelection); + } + + internal static void OnTextCompositionResult(object sender, string compositionResult) + { + if (CommitTextComposition != null) + CommitTextComposition.Invoke(sender, compositionResult); + + if (CommitTextCompositionCallback != null) + CommitTextCompositionCallback(compositionResult); + } + + internal static void OnTextCompositionEnded(object sender) + { + if (TextComposition != null) + TextComposition.Invoke(sender, new IMETextCompositionEventArgs(IMEString.Empty, 0)); + + if (TextCompositionCallback != null) + TextCompositionCallback(IMEString.Empty, 0, null, 0, 0, 0); + } + + private static void EnableOrDisableInputMethod(bool bEnabled) + { + // InputMethod enable/disabled status was changed on the current focus Element. + if (TextServicesLoader.ServicesInstalled) + { + if (bEnabled) + TextServicesContext.Current.SetFocusOnDefaultTextStore(); + else + TextServicesContext.Current.SetFocusOnEmptyDim(); + } + + // Under IMM32 enabled system, we associate default hIMC or null hIMC. + if (Imm32Manager.ImmEnabled) + { + if (bEnabled) + Imm32Manager.Current.Enable(); + else + Imm32Manager.Current.Disable(); + } + } + + private static IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + if (Imm32Manager.ImmEnabled) + { + if (Imm32Manager.Current.ProcessMessage(hWnd, msg, ref wParam, ref lParam)) + return IntPtr.Zero; + } + + switch (msg) + { + case NativeMethods.WM_DESTROY: + TextServicesContext.Current.Uninitialize(true); + break; + case NativeMethods.WM_CHAR: + { + if (InputMethod.Enabled) + InputMethod.OnTextInput(null, (char)wParam.ToInt32()); + + break; + } + } + + return NativeMethods.CallWindowProc(_prevWndProc, hWnd, msg, wParam, lParam); + } + + /// + /// Custom windows message pumping to fix frame stuck issue. + /// Normally, you need call this method in handler. + /// + public static void PumpMessage() + { + if (!Enabled) return; + if (!TextServicesLoader.ServicesInstalled) return; + + bool result; + var msg = new NativeMethods.NativeMessage(); + + do + { + result = NativeMethods.PeekMessage(out msg, _windowHandle, 0, 0, NativeMethods.PM_REMOVE); + + if (result) + { + NativeMethods.TranslateMessage(ref msg); + NativeMethods.DispatchMessage(ref msg); + } + } while (result); + + NativeMethods.PostMessage(_windowHandle, NativeMethods.WM_NULL, IntPtr.Zero, IntPtr.Zero); + } + } +} diff --git a/Libraries/ImeSharp/Native/NativeMethods.cs b/Libraries/ImeSharp/Native/NativeMethods.cs new file mode 100644 index 000000000..1e3d6f901 --- /dev/null +++ b/Libraries/ImeSharp/Native/NativeMethods.cs @@ -0,0 +1,149 @@ +using System; +using System.Text; +using System.Runtime.InteropServices; + +namespace ImeSharp.Native +{ + public partial class NativeMethods + { + #region Constants + + public const int S_OK = 0x00000000; + public const int S_FALSE = 0x00000001; + public const int E_FAIL = unchecked((int)0x80004005); + public const int E_INVALIDARG = unchecked((int)0x80070057); + public const int E_NOTIMPL = unchecked((int)0x80004001); + + public const int WM_KEYFIRST = 0x0100; + public const int WM_KEYDOWN = 0x0100; + public const int WM_KEYUP = 0x0101; + public const int WM_CHAR = 0x0102; + public const int WM_DEADCHAR = 0x0103; + public const int WM_SYSKEYDOWN = 0x0104; + public const int WM_SYSKEYUP = 0x0105; + public const int WM_SYSCHAR = 0x0106; + public const int WM_SYSDEADCHAR = 0x0107; + public const int WM_UNICHAR = 0x0109; + public const int WM_KEYLAST = 0x0109; + public const int UNICODE_NOCHAR = 0xFFFF; + + public const int WM_NOTIFY = 0x004E; + public const int WM_INPUTLANGCHANGEREQUEST = 0x0050; + public const int WM_INPUTLANGCHANGE = 0x0051; + public const int WM_TCARD = 0x0052; + public const int WM_HELP = 0x0053; + public const int WM_USERCHANGED = 0x0054; + public const int WM_NOTIFYFORMAT = 0x0055; + + public const int GWL_WNDPROC = -4; + + public const int WM_ACTIVATE = 0x0006; + // WM_ACTIVATE state values + public const int WA_INACTIVE = 0; + public const int WA_ACTIVE = 1; + public const int WA_CLICKACTIVE = 2; + + public const int WM_SETFOCUS = 0x0007; + public const int WM_KILLFOCUS = 0x0008; + + public const int WM_DESTROY = 0x0002; + public const int WM_NULL = 0x0000; + public const int WM_QUIT = 0x0012; + + public const int CLSCTX_INPROC_SERVER = 0x1; + + public const int PM_NOREMOVE = 0x0000; + public const int PM_REMOVE = 0x0001; + public const int PM_NOYIELD = 0x0002; + + #endregion Constants + + #region Structs + + [StructLayout(LayoutKind.Sequential)] + public struct NativeMessage + { + public IntPtr handle; + public uint msg; + public IntPtr wParam; + public IntPtr lParam; + public uint time; + public int ptX; + public int ptY; + } + + #endregion + + [DllImport("user32.dll")] + public static extern int GetSystemMetrics(SM nIndex); + + // We have this wrapper because casting IntPtr to int may + // generate OverflowException when one of high 32 bits is set. + public static int IntPtrToInt32(IntPtr intPtr) + { + return unchecked((int)intPtr.ToInt64()); + } + + [DllImport("user32.dll", ExactSpelling = true, CharSet = CharSet.Unicode)] + public static extern IntPtr GetKeyboardLayout(int dwLayout); + + public delegate IntPtr WndProcDelegate(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); + + [DllImport("user32.dll")] + public static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); + + // This static method is required because legacy OSes do not support + public static IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong) + { + if (IntPtr.Size == 8) + return SetWindowLongPtr64(hWnd, nIndex, dwNewLong); + else + return new IntPtr(SetWindowLong32(hWnd, nIndex, dwNewLong.ToInt32())); + } + + [DllImport("user32.dll", EntryPoint = "SetWindowLong", CharSet = CharSet.Unicode)] + private static extern int SetWindowLong32(IntPtr hWnd, int nIndex, int dwNewLong); + + [DllImport("user32.dll", EntryPoint = "SetWindowLongPtr", CharSet = CharSet.Unicode)] + private static extern IntPtr SetWindowLongPtr64(IntPtr hWnd, int nIndex, IntPtr dwNewLong); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool GetWindowRect(IntPtr hwnd, out TsfSharp.Rect lpRect); + + [DllImport("user32", ExactSpelling = true, SetLastError = true)] + public static extern int MapWindowPoints(IntPtr hWndFrom, IntPtr hWndTo, ref TsfSharp.Rect rect, [MarshalAs(UnmanagedType.U4)] int cPoints); + + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, StringBuilder lParam); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, [MarshalAs(UnmanagedType.LPWStr)] string lParam); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, int wParam, [MarshalAs(UnmanagedType.LPWStr)] string lParam); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, int wParam, ref IntPtr lParam); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, int wParam, IntPtr lParam); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + public static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); + + [DllImport("user32.dll")] + public static extern bool TranslateMessage(ref NativeMessage lpMsg); + + [DllImport("user32.dll")] + public static extern IntPtr DispatchMessage(ref NativeMessage lpmsg); + + [DllImport("User32.dll", CharSet = CharSet.Unicode)] + public static extern bool PeekMessage(out NativeMessage msg, IntPtr hWnd, uint messageFilterMin, uint messageFilterMax, uint flags); + + + [DllImport("ole32.dll", ExactSpelling = true, EntryPoint = "CoCreateInstance", PreserveSig = true)] + public static extern int CoCreateInstance([In, MarshalAs(UnmanagedType.LPStruct)] Guid rclsid, IntPtr pUnkOuter, int dwClsContext, [In, MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IntPtr ppv); + + } +} diff --git a/Libraries/ImeSharp/Native/NativeMethodsIMM32.cs b/Libraries/ImeSharp/Native/NativeMethodsIMM32.cs new file mode 100644 index 000000000..701d6aff2 --- /dev/null +++ b/Libraries/ImeSharp/Native/NativeMethodsIMM32.cs @@ -0,0 +1,156 @@ +using System; +using System.Runtime.InteropServices; + +namespace ImeSharp.Native +{ + public partial class NativeMethods + { + #region Constants + + public const int WM_IME_SETCONTEXT = 0x0281; + public const int WM_IME_NOTIFY = 0x0282; + public const int WM_IME_CONTROL = 0x0283; + public const int WM_IME_COMPOSITIONFULL = 0x0284; + public const int WM_IME_SELECT = 0x0285; + public const int WM_IME_CHAR = 0x0286; + public const int WM_IME_REQUEST = 0x0288; + public const int WM_IME_KEYDOWN = 0x0290; + public const int WM_IME_KEYUP = 0x0291; + public const int WM_IME_STARTCOMPOSITION = 0x010D; + public const int WM_IME_ENDCOMPOSITION = 0x010E; + public const int WM_IME_COMPOSITION = 0x010F; + public const int WM_IME_KEYLAST = 0x010F; + + // wParam of report message WM_IME_NOTIFY + public const int IMN_CLOSESTATUSWINDOW = 0x0001; + public const int IMN_OPENSTATUSWINDOW = 0x0002; + public const int IMN_CHANGECANDIDATE = 0x0003; + public const int IMN_CLOSECANDIDATE = 0x0004; + public const int IMN_OPENCANDIDATE = 0x0005; + public const int IMN_SETCONVERSIONMODE = 0x0006; + public const int IMN_SETSENTENCEMODE = 0x0007; + public const int IMN_SETOPENSTATUS = 0x0008; + public const int IMN_SETCANDIDATEPOS = 0x0009; + public const int IMN_SETCOMPOSITIONFONT = 0x000A; + public const int IMN_SETCOMPOSITIONWINDOW = 0x000B; + public const int IMN_SETSTATUSWINDOWPOS = 0x000C; + public const int IMN_GUIDELINE = 0x000D; + public const int IMN_PRIVATE = 0x000E; + + // wParam of report message WM_IME_REQUEST + public const int IMR_COMPOSITIONWINDOW = 0x0001; + public const int IMR_CANDIDATEWINDOW = 0x0002; + public const int IMR_COMPOSITIONFONT = 0x0003; + public const int IMR_RECONVERTSTRING = 0x0004; + public const int IMR_CONFIRMRECONVERTSTRING = 0x0005; + public const int IMR_QUERYCHARPOSITION = 0x0006; + public const int IMR_DOCUMENTFEED = 0x0007; + + // parameter of ImmGetCompositionString + public const int GCS_COMPREADSTR = 0x0001; + public const int GCS_COMPREADATTR = 0x0002; + public const int GCS_COMPREADCLAUSE = 0x0004; + public const int GCS_COMPSTR = 0x0008; + public const int GCS_COMPATTR = 0x0010; + public const int GCS_COMPCLAUSE = 0x0020; + public const int GCS_CURSORPOS = 0x0080; + public const int GCS_DELTASTART = 0x0100; + public const int GCS_RESULTREADSTR = 0x0200; + public const int GCS_RESULTREADCLAUSE = 0x0400; + public const int GCS_RESULTSTR = 0x0800; + public const int GCS_RESULTCLAUSE = 0x1000; + + public const int GCS_COMP = (GCS_COMPSTR | GCS_COMPATTR | GCS_COMPCLAUSE); + public const int GCS_COMPREAD = (GCS_COMPREADSTR | GCS_COMPREADATTR | GCS_COMPREADCLAUSE); + public const int GCS_RESULT = (GCS_RESULTSTR | GCS_RESULTCLAUSE); + public const int GCS_RESULTREAD = (GCS_RESULTREADSTR | GCS_RESULTREADCLAUSE); + + public const int CFS_CANDIDATEPOS = 0x0040; + public const int CFS_POINT = 0x0002; + public const int CFS_EXCLUDE = 0x0080; + + // lParam for WM_IME_SETCONTEXT + public const long ISC_SHOWUICANDIDATEWINDOW = 0x00000001; + public const long ISC_SHOWUICOMPOSITIONWINDOW = 0x80000000; + public const long ISC_SHOWUIGUIDELINE = 0x40000000; + public const long ISC_SHOWUIALLCANDIDATEWINDOW = 0x0000000F; + public const long ISC_SHOWUIALL = 0xC000000F; + + #endregion Constants + + [StructLayout(LayoutKind.Sequential)] + public unsafe struct CANDIDATELIST + { + public uint dwSize; + public uint dwStyle; + public uint dwCount; + public uint dwSelection; + public uint dwPageStart; + public uint dwPageSize; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1, ArraySubType = UnmanagedType.U4)] + public fixed uint dwOffset[1]; + } + + // CANDIDATEFORM structures + [StructLayout(LayoutKind.Sequential)] + public struct CANDIDATEFORM + { + public int dwIndex; + public int dwStyle; + public TsfSharp.Point ptCurrentPos; + public TsfSharp.Rect rcArea; + } + + // COMPOSITIONFORM structures + [StructLayout(LayoutKind.Sequential)] + public struct COMPOSITIONFORM + { + public int dwStyle; + public TsfSharp.Point ptCurrentPos; + public TsfSharp.Rect rcArea; + } + + [DllImport("imm32.dll", SetLastError = true)] + public static extern IntPtr ImmCreateContext(); + + [DllImport("imm32.dll", SetLastError = true)] + public static extern bool ImmDestroyContext(IntPtr hIMC); + + [DllImport("imm32.dll", SetLastError = true)] + public static extern IntPtr ImmAssociateContext(IntPtr hWnd, IntPtr hIMC); + + [DllImport("imm32.dll", SetLastError = true)] + public static extern IntPtr ImmReleaseContext(IntPtr hWnd, IntPtr hIMC); + + [DllImport("imm32.dll", CharSet = CharSet.Unicode)] + public static extern uint ImmGetCandidateList(IntPtr hIMC, uint deIndex, IntPtr candidateList, uint dwBufLen); + + [DllImport("imm32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern int ImmGetCompositionString(IntPtr hIMC, int CompositionStringFlag, IntPtr buffer, int bufferLength); + + [DllImport("imm32.dll", SetLastError = true)] + public static extern IntPtr ImmGetContext(IntPtr hWnd); + + [DllImport("Imm32.dll", SetLastError = true)] + public static extern bool ImmGetOpenStatus(IntPtr hIMC); + + [DllImport("Imm32.dll", SetLastError = true)] + public static extern bool ImmSetOpenStatus(IntPtr hIMC, bool open); + + [DllImport("imm32.dll", SetLastError = true)] + public static extern bool ImmSetCandidateWindow(IntPtr hIMC, ref CANDIDATEFORM candidateForm); + + [DllImport("imm32.dll", SetLastError = true)] + public static extern int ImmSetCompositionWindow(IntPtr hIMC, ref COMPOSITIONFORM compForm); + + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool CreateCaret(IntPtr hWnd, IntPtr hBitmap, int nWidth, int nHeight); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool DestroyCaret(); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool SetCaretPos(int x, int y); + } +} diff --git a/Libraries/ImeSharp/Native/NativeValues.cs b/Libraries/ImeSharp/Native/NativeValues.cs new file mode 100644 index 000000000..1c0dd9a36 --- /dev/null +++ b/Libraries/ImeSharp/Native/NativeValues.cs @@ -0,0 +1,95 @@ +using System; + +namespace ImeSharp.Native +{ + /// + /// SystemMetrics. SM_* + /// + public enum SM + { + CXSCREEN = 0, + CYSCREEN = 1, + CXVSCROLL = 2, + CYHSCROLL = 3, + CYCAPTION = 4, + CXBORDER = 5, + CYBORDER = 6, + CXFIXEDFRAME = 7, + CYFIXEDFRAME = 8, + CYVTHUMB = 9, + CXHTHUMB = 10, + CXICON = 11, + CYICON = 12, + CXCURSOR = 13, + CYCURSOR = 14, + CYMENU = 15, + CXFULLSCREEN = 16, + CYFULLSCREEN = 17, + CYKANJIWINDOW = 18, + MOUSEPRESENT = 19, + CYVSCROLL = 20, + CXHSCROLL = 21, + DEBUG = 22, + SWAPBUTTON = 23, + CXMIN = 28, + CYMIN = 29, + CXSIZE = 30, + CYSIZE = 31, + CXFRAME = 32, + CXSIZEFRAME = CXFRAME, + CYFRAME = 33, + CYSIZEFRAME = CYFRAME, + CXMINTRACK = 34, + CYMINTRACK = 35, + CXDOUBLECLK = 36, + CYDOUBLECLK = 37, + CXICONSPACING = 38, + CYICONSPACING = 39, + MENUDROPALIGNMENT = 40, + PENWINDOWS = 41, + DBCSENABLED = 42, + CMOUSEBUTTONS = 43, + SECURE = 44, + CXEDGE = 45, + CYEDGE = 46, + CXMINSPACING = 47, + CYMINSPACING = 48, + CXSMICON = 49, + CYSMICON = 50, + CYSMCAPTION = 51, + CXSMSIZE = 52, + CYSMSIZE = 53, + CXMENUSIZE = 54, + CYMENUSIZE = 55, + ARRANGE = 56, + CXMINIMIZED = 57, + CYMINIMIZED = 58, + CXMAXTRACK = 59, + CYMAXTRACK = 60, + CXMAXIMIZED = 61, + CYMAXIMIZED = 62, + NETWORK = 63, + CLEANBOOT = 67, + CXDRAG = 68, + CYDRAG = 69, + SHOWSOUNDS = 70, + CXMENUCHECK = 71, + CYMENUCHECK = 72, + SLOWMACHINE = 73, + MIDEASTENABLED = 74, + MOUSEWHEELPRESENT = 75, + XVIRTUALSCREEN = 76, + YVIRTUALSCREEN = 77, + CXVIRTUALSCREEN = 78, + CYVIRTUALSCREEN = 79, + CMONITORS = 80, + SAMEDISPLAYFORMAT = 81, + IMMENABLED = 82, + CXFOCUSBORDER = 83, + CYFOCUSBORDER = 84, + TABLETPC = 86, + MEDIACENTER = 87, + REMOTESESSION = 0x1000, + REMOTECONTROL = 0x2001, + } +} diff --git a/Libraries/ImeSharp/SafeSystemMetrics.cs b/Libraries/ImeSharp/SafeSystemMetrics.cs new file mode 100644 index 000000000..a2e76f5b2 --- /dev/null +++ b/Libraries/ImeSharp/SafeSystemMetrics.cs @@ -0,0 +1,58 @@ +using System; +using ImeSharp.Native; + +namespace ImeSharp +{ + /// + /// Contains properties that are queries into the system's various settings. + /// + internal sealed class SafeSystemMetrics + { + + private SafeSystemMetrics() + { + } + + /// + /// Maps to SM_CXDOUBLECLK + /// + public static int DoubleClickDeltaX + { + get { return NativeMethods.GetSystemMetrics(SM.CXDOUBLECLK); } + } + + /// + /// Maps to SM_CYDOUBLECLK + /// + public static int DoubleClickDeltaY + { + get { return NativeMethods.GetSystemMetrics(SM.CYDOUBLECLK); } + } + + + /// + /// Maps to SM_CXDRAG + /// + public static int DragDeltaX + { + get { return NativeMethods.GetSystemMetrics(SM.CXDRAG); } + } + + /// + /// Maps to SM_CYDRAG + /// + public static int DragDeltaY + { + get { return NativeMethods.GetSystemMetrics(SM.CYDRAG); } + } + + /// + /// Is an IMM enabled ? Maps to SM_IMMENABLED + /// + public static bool IsImmEnabled + { + get { return (NativeMethods.GetSystemMetrics(SM.IMMENABLED) != 0); } + } + + } +} diff --git a/Libraries/ImeSharp/TextInputCallbacks.cs b/Libraries/ImeSharp/TextInputCallbacks.cs new file mode 100644 index 000000000..c37837927 --- /dev/null +++ b/Libraries/ImeSharp/TextInputCallbacks.cs @@ -0,0 +1,6 @@ +namespace ImeSharp +{ + public delegate void TextInputCallback(char character); + public delegate void TextCompositionCallback(IMEString compositionText, int cursorPosition, IMEString[] candidateList, int candidatePageStart, int candidatePageSize, int candidateSelection); + public delegate void CommitTextCompositionCallback(string text); +} diff --git a/Libraries/ImeSharp/TextServicesContext.cs b/Libraries/ImeSharp/TextServicesContext.cs new file mode 100644 index 000000000..6ce846528 --- /dev/null +++ b/Libraries/ImeSharp/TextServicesContext.cs @@ -0,0 +1,374 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; +using System.Diagnostics; +using ImeSharp.Native; +using TsfSharp; + +namespace ImeSharp +{ + //------------------------------------------------------ + // + // TextServicesContext class + // + //------------------------------------------------------ + + /// + /// This class manages the ITfThreadMgr, EmptyDim and the reference to + /// the default TextStore. + /// + /// + /// + internal class TextServicesContext + { + public const int TF_POPF_ALL = 0x0001; + public const int TF_INVALID_COOKIE = -1; + public static readonly Guid IID_ITfUIElementSink = new Guid(0xea1ea136, 0x19df, 0x11d7, 0xa6, 0xd2, 0x00, 0x06, 0x5b, 0x84, 0x43, 0x5c); + public static readonly Guid IID_ITfTextEditSink = new Guid(0x8127d409, 0xccd3, 0x4683, 0x96, 0x7a, 0xb4, 0x3d, 0x5b, 0x48, 0x2b, 0xf7); + + + public static TextServicesContext Current + { + get + { + if (InputMethod.TextServicesContext == null) + InputMethod.TextServicesContext = new TextServicesContext(); + + return InputMethod.TextServicesContext; + } + } + + //------------------------------------------------------ + // + // Constructors + // + //------------------------------------------------------ + + #region Constructors + + /// + /// Instantiates a TextServicesContext. + /// + private TextServicesContext() + { + if (Thread.CurrentThread.GetApartmentState() != ApartmentState.STA) + Debug.WriteLine("CRASH: ImeSharp won't work on MTA thread!!!"); + } + + #endregion Constructors + + //------------------------------------------------------ + // + // public Methods + // + //------------------------------------------------------ + + #region public Methods + + /// + /// Releases all unmanaged resources allocated by the + /// TextServicesContext. + /// + /// + /// if appDomainShutdown == false, this method must be called on the + /// Dispatcher thread. Otherwise, the caller is an AppDomain.Shutdown + /// listener, and is calling from a worker thread. + /// + public void Uninitialize(bool appDomainShutdown) + { + // Unregister DefaultTextStore. + if (_defaultTextStore != null) + { + UnadviseSinks(); + if (_defaultTextStore.DocumentManager != null) + { + _defaultTextStore.DocumentManager.Pop(TF_POPF_ALL); + _defaultTextStore.DocumentManager.Dispose(); + _defaultTextStore.DocumentManager = null; + } + + _defaultTextStore = null; + } + + // Free up any remaining textstores. + if (_istimactivated == true) + { + // Shut down the thread manager when the last TextStore goes away. + // On XP, if we're called on a worker thread (during AppDomain shutdown) + // we can't call call any methods on _threadManager. The problem is + // that there's no proxy registered for ITfThreadMgr on OS versions + // previous to Vista. Not calling Deactivate will leak the IMEs, but + // in practice (1) they're singletons, so it's not unbounded; and (2) + // most applications will share the thread with other AppDomains that + // have a UI, in which case the IME won't be released until the process + // shuts down in any case. In theory we could also work around this + // problem by creating our own XP proxy/stub implementation, which would + // be added to WPF setup.... + if (!appDomainShutdown || System.Environment.OSVersion.Version.Major >= 6) + { + _threadManager.Deactivate(); + } + _istimactivated = false; + } + + // Release the empty dim. + if (_dimEmpty != null) + { + if (_dimEmpty != null) + { + _dimEmpty.Dispose(); + } + _dimEmpty = null; + } + + // Release the ThreadManager. + // We don't do this in UnregisterTextStore because someone may have + // called get_ThreadManager after the last TextStore was unregistered. + if (_threadManager != null) + { + if (_threadManager != null) + { + _threadManager.Dispose(); + } + _threadManager = null; + } + } + + // Called by framework's TextStore class. This method registers a + // document with TSF. The TextServicesContext must maintain this list + // to ensure all native resources are released after gc or uninitialization. + public void RegisterTextStore(TextStore defaultTextStore) + { + _defaultTextStore = defaultTextStore; + + ITfThreadMgrEx threadManager = ThreadManager; + + if (threadManager != null) + { + ITfDocumentMgr doc; + int editCookie = TF_INVALID_COOKIE; + + // Activate TSF on this thread if this is the first TextStore. + if (_istimactivated == false) + { + //temp variable created to retrieve the value + // which is then stored in the critical data. + if (InputMethod.ShowOSImeWindow) + _clientId = threadManager.Activate(); + else + _clientId = threadManager.ActivateEx(TfTmaeFlags.Uielementenabledonly); + + _istimactivated = true; + } + + // Create a TSF document. + doc = threadManager.CreateDocumentMgr(); + _defaultTextStore.DocumentManager = doc; + + doc.CreateContext(_clientId, 0 /* flags */, _defaultTextStore, out _editContext, out editCookie); + _defaultTextStore.EditCookie = editCookie; + _contextOwnerServices = _editContext.QueryInterface(); + + doc.Push(_editContext); + + AdviseSinks(); + } + } + + + public void SetFocusOnDefaultTextStore() + { + SetFocusOnDim(TextStore.Current.DocumentManager); + } + + public void SetFocusOnEmptyDim() + { + SetFocusOnDim(EmptyDocumentManager); + } + + + #endregion public Methods + + //------------------------------------------------------ + // + // public Properties + // + //------------------------------------------------------ + + /// + /// The default ITfThreadMgrEx object. + /// + public ITfThreadMgrEx ThreadManager + { + // The ITfThreadMgr for this thread. + get + { + if (_threadManager == null) + { + ITfThreadMgr threadMgr = null; + try + { + // This might fail in CoreRT + threadMgr = Tsf.GetThreadMgr(); + } + catch (SharpGen.Runtime.SharpGenException) + { + threadMgr = null; + } + + // Dispose previous ITfThreadMgr in case something weird happens + if (threadMgr != null) + { + if (threadMgr.IsThreadFocus) + threadMgr.Deactivate(); + threadMgr.Dispose(); + } + + _threadManager = TextServicesLoader.Load(); + + _uiElementMgr = _threadManager.QueryInterface(); + } + + return _threadManager; + } + } + + /// + /// Return the created ITfContext object. + /// + public ITfContext EditContext + { + get { return _editContext; } + } + + /// + /// Return the created ITfUIElementMgr object. + /// + public ITfUIElementMgr UIElementMgr + { + get { return _uiElementMgr; } + } + + /// + /// Return the created ITfContextOwnerServices object. + /// + public ITfContextOwnerServices ContextOwnerServices + { + get { return _contextOwnerServices; } + } + + //------------------------------------------------------ + // + // public Events + // + //------------------------------------------------------ + + //------------------------------------------------------ + // + // Private Methods + // + //------------------------------------------------------ + + private void SetFocusOnDim(ITfDocumentMgr dim) + { + ITfThreadMgrEx threadmgr = ThreadManager; + + if (threadmgr != null) + { + ITfDocumentMgr prevDocMgr = threadmgr.AssociateFocus(InputMethod.WindowHandle, dim); + } + } + + private void AdviseSinks() + { + var source = _uiElementMgr.QueryInterface(); + var guid = IID_ITfUIElementSink; + int sinkCookie = source.AdviseSink(guid, _defaultTextStore); + _defaultTextStore.UIElementSinkCookie = sinkCookie; + source.Dispose(); + + source = _editContext.QueryInterface(); + guid = IID_ITfTextEditSink; + sinkCookie = source.AdviseSink(guid, _defaultTextStore); + _defaultTextStore.TextEditSinkCookie = sinkCookie; + source.Dispose(); + } + + private void UnadviseSinks() + { + var source = _uiElementMgr.QueryInterface(); + + if (_defaultTextStore.UIElementSinkCookie != TF_INVALID_COOKIE) + { + source.UnadviseSink(_defaultTextStore.UIElementSinkCookie); + _defaultTextStore.UIElementSinkCookie = TF_INVALID_COOKIE; + } + source.Dispose(); + + source = _editContext.QueryInterface(); + if (_defaultTextStore.TextEditSinkCookie != TF_INVALID_COOKIE) + { + source.UnadviseSink(_defaultTextStore.TextEditSinkCookie); + _defaultTextStore.TextEditSinkCookie = TF_INVALID_COOKIE; + } + source.Dispose(); + } + + //------------------------------------------------------ + // + // Private Properties + // + //------------------------------------------------------ + + // Create an empty dim on demand. + private ITfDocumentMgr EmptyDocumentManager + { + get + { + if (_dimEmpty == null) + { + ITfThreadMgrEx threadManager = ThreadManager; + if (threadManager == null) + { + return null; + } + + ITfDocumentMgr dimEmptyTemp; + // Create a TSF document. + dimEmptyTemp = threadManager.CreateDocumentMgr(); + _dimEmpty = dimEmptyTemp; + } + return _dimEmpty; + } + } + + + //------------------------------------------------------ + // + // Private Fields + // + //------------------------------------------------------ + + #region Private Fields + + private TextStore _defaultTextStore; + + private ITfContext _editContext; + private ITfUIElementMgr _uiElementMgr; + private ITfContextOwnerServices _contextOwnerServices; + + // This is true if thread manager is activated. + private bool _istimactivated; + + // The root TSF object, created on demand. + private ITfThreadMgrEx _threadManager; + + // TSF ClientId from Activate call. + private int _clientId; + + // The empty dim for this thread. Created on demand. + private ITfDocumentMgr _dimEmpty; + + #endregion Private Fields + } +} diff --git a/Libraries/ImeSharp/TextServicesLoader.cs b/Libraries/ImeSharp/TextServicesLoader.cs new file mode 100644 index 000000000..ff2037b57 --- /dev/null +++ b/Libraries/ImeSharp/TextServicesLoader.cs @@ -0,0 +1,338 @@ +using System; +using System.Runtime.InteropServices; +using System.Diagnostics; +using System.Threading; +using Microsoft.Win32; +using ImeSharp.Native; +using TsfSharp; + +namespace ImeSharp +{ + // Creates ITfThreadMgr instances, the root object of the Text Services + // Framework. + internal class TextServicesLoader + { + public static readonly Guid CLSID_TF_ThreadMgr = new Guid("529a9e6b-6587-4f23-ab9e-9c7d683e3c50"); + public static readonly Guid IID_ITfThreadMgr = new Guid("aa80e801-2021-11d2-93e0-0060b067b86e"); + public static readonly Guid IID_ITfThreadMgrEx = new Guid("3e90ade3-7594-4cb0-bb58-69628f5f458c"); + public static readonly Guid IID_ITfThreadMgr2 = new Guid("0AB198EF-6477-4EE8-8812-6780EDB82D5E"); + + //------------------------------------------------------ + // + // Constructors + // + //------------------------------------------------------ + + #region Constructors + + // Private ctor to prevent anyone from instantiating this static class. + private TextServicesLoader() { } + + #endregion Constructors + + //------------------------------------------------------ + // + // public Properties + // + //------------------------------------------------------ + + #region public Properties + + /// + /// Loads an instance of the Text Services Framework. + /// + /// + /// May return null if no text services are available. + /// + public static ITfThreadMgrEx Load() + { + if (Thread.CurrentThread.GetApartmentState() != ApartmentState.STA) + Debug.WriteLine("CRASH: ImeSharp won't work on MTA thread!!!"); + + if (ServicesInstalled) + { + // NB: a COMException here means something went wrong initialzing Cicero. + // Cicero will throw an exception if it doesn't think it should have been + // loaded (no TIPs to run), you can check that in msctf.dll's NoTipsInstalled + // which lives in nt\windows\advcore\ctf\lib\immxutil.cpp. If that's the + // problem, ServicesInstalled is out of sync with Cicero's thinking. + IntPtr ret; + var hr = NativeMethods.CoCreateInstance(CLSID_TF_ThreadMgr, + IntPtr.Zero, + NativeMethods.CLSCTX_INPROC_SERVER, + IID_ITfThreadMgrEx, out ret); + + if (hr == NativeMethods.S_OK) + return new ITfThreadMgrEx(ret); + } + + return null; + } + + /// + /// return true if current OS version is Windows 7 or below. + /// + public static bool IsWindows7OrBelow() + { + if (Environment.OSVersion.Version.Major <= 5) + return true; + + if (Environment.OSVersion.Version.Major == 6 && Environment.OSVersion.Version.Minor <= 1) + return true; + + return false; + } + + /// + /// Informs the caller if text services are installed for the current user. + /// + /// + /// true if one or more text services are installed for the current user, otherwise false. + /// + /// + /// If this method returns false, TextServicesLoader.Load is guarenteed to return null. + /// Callers can use this information to avoid overhead that would otherwise be + /// required to support text services. + /// + public static bool ServicesInstalled + { + get + { + lock (s_servicesInstalledLock) + { + if (s_servicesInstalled == InstallState.Unknown) + { + s_servicesInstalled = TIPsWantToRun() ? InstallState.Installed : InstallState.NotInstalled; + } + } + + return (s_servicesInstalled == InstallState.Installed); + } + } + + #endregion public Properties + + //------------------------------------------------------ + // + // public Events + // + //------------------------------------------------------ + + //------------------------------------------------------ + // + // Private Methods + // + //------------------------------------------------------ + + #region Private Methods + + // + // This method tries to stop Avalon from loading Cicero when there are no TIPs to run. + // The perf tradeoff is a typically small number of registry checks versus loading and + // initializing cicero. + // + // The Algorithm: + // + // Do a quick check vs. the global disable flag, return false if it is set. + // For each key under HKLM\SOFTWARE\Microsoft\CTF\TIP (a TIP or category clsid) + // If the the key has a LanguageProfile subkey (it's a TIP clsid) + // Iterate under the matching TIP entry in HKCU. + // For each key under the LanguageProfile (a particular LANGID) + // For each key under the LANGID (an assembly GUID) + // Try to read the Enable value. + // If the value is set non-zero, then stop all processing and return true. + // If the value is set zero, continue. + // If the value does not exist, continue (default is disabled). + // If any Enable values were found under HKCU for the TIP, then stop all processing and return false. + // Else, no Enable values have been found thus far and we keep going to investigate HKLM. + // Iterate under the TIP entry in HKLM. + // For each key under the LanguageProfile (a particular LANGID) + // For each key under the LANGID (an assembly GUID) + // Try to read the Enable value. + // If the value is set non-zero, then stop all processing and return true. + // If the value does not exist, then stop all processing and return true (default is enabled). + // If the value is set zero, continue. + // If we finish iterating all entries under HKLM without returning true, return false. + // + + private static bool TIPsWantToRun() + { + object obj; + RegistryKey key; + bool tipsWantToRun = false; + + key = Registry.CurrentUser.OpenSubKey("Software\\Microsoft\\CTF", false); + + // Is cicero disabled completely for the current user? + if (key != null) + { + obj = key.GetValue("Disable Thread Input Manager"); + + if (obj is int && (int)obj != 0) + return false; + } + + // Loop through all the TIP entries for machine and current user. + tipsWantToRun = IterateSubKeys(Registry.LocalMachine, "SOFTWARE\\Microsoft\\CTF\\TIP", new IterateHandler(SingleTIPWantsToRun), true) == EnableState.Enabled; + + return tipsWantToRun; + } + + // Returns EnableState.Enabled if one or more TIPs are installed and + // enabled for the current user. + private static EnableState SingleTIPWantsToRun(RegistryKey keyLocalMachine, string subKeyName, bool localMachine) + { + EnableState result; + + if (subKeyName.Length != CLSIDLength) + return EnableState.Disabled; + + // We want subkey\LanguageProfile key. + // Loop through all the langid entries for TIP. + + // First, check current user. + result = IterateSubKeys(Registry.CurrentUser, "SOFTWARE\\Microsoft\\CTF\\TIP\\" + subKeyName + "\\LanguageProfile", new IterateHandler(IsLangidEnabled), false); + + // Any explicit value short circuits the process. + // Otherwise check local machine. + if (result == EnableState.None || result == EnableState.Error) + { + result = IterateSubKeys(keyLocalMachine, subKeyName + "\\LanguageProfile", new IterateHandler(IsLangidEnabled), true); + + if (result == EnableState.None) + { + result = EnableState.Enabled; + } + } + + return result; + } + + // Returns EnableState.Enabled if the supplied subkey is a valid LANGID key with enabled + // cicero assembly. + private static EnableState IsLangidEnabled(RegistryKey key, string subKeyName, bool localMachine) + { + if (subKeyName.Length != LANGIDLength) + return EnableState.Error; + + // Loop through all the assembly entries for the langid + return IterateSubKeys(key, subKeyName, new IterateHandler(IsAssemblyEnabled), localMachine); + } + + // Returns EnableState.Enabled if the supplied assembly key is enabled. + private static EnableState IsAssemblyEnabled(RegistryKey key, string subKeyName, bool localMachine) + { + RegistryKey subKey; + object obj; + + if (subKeyName.Length != CLSIDLength) + return EnableState.Error; + + // Open the local machine assembly key. + subKey = key.OpenSubKey(subKeyName); + + if (subKey == null) + return EnableState.Error; + + // Try to read the "Enable" value. + obj = subKey.GetValue("Enable"); + + if (obj is int) + { + return ((int)obj == 0) ? EnableState.Disabled : EnableState.Enabled; + } + + return EnableState.None; + } + + // Calls the supplied delegate on each of the children of keyBase. + private static EnableState IterateSubKeys(RegistryKey keyBase, string subKey, IterateHandler handler, bool localMachine) + { + RegistryKey key; + string[] subKeyNames; + EnableState state; + + key = keyBase.OpenSubKey(subKey, false); + + if (key == null) + return EnableState.Error; + + subKeyNames = key.GetSubKeyNames(); + state = EnableState.Error; + + foreach (string name in subKeyNames) + { + switch (handler(key, name, localMachine)) + { + case EnableState.Error: + break; + case EnableState.None: + if (localMachine) // For lm, want to return here right away. + return EnableState.None; + + // For current user, remember that we found no Enable value. + if (state == EnableState.Error) + { + state = EnableState.None; + } + break; + case EnableState.Disabled: + state = EnableState.Disabled; + break; + case EnableState.Enabled: + return EnableState.Enabled; + } + } + + return state; + } + + #endregion Private Methods + + //------------------------------------------------------ + // + // Private Properties + // + //------------------------------------------------------ + + //------------------------------------------------------ + // + // Private Fields + // + //------------------------------------------------------ + + #region Private Fields + + // String consts used to validate registry entires. + private const int CLSIDLength = 38; // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} + private const int LANGIDLength = 10; // 0x12345678 + + // Status of a TIP assembly. + private enum EnableState + { + Error, // Invalid entry. + None, // No explicit Enable entry on the assembly. + Enabled, // Assembly is enabled. + Disabled // Assembly is disabled. + }; + + // Callback delegate for the IterateSubKeys method. + private delegate EnableState IterateHandler(RegistryKey key, string subKeyName, bool localMachine); + + // Install state. + private enum InstallState + { + Unknown, // Haven't checked to see if any TIPs are installed yet. + Installed, // Checked and installed. + NotInstalled // Checked and not installed. + } + + // Cached install state value. + // Writes are not thread safe, but we don't mind the neglible perf hit + // of potentially writing it twice. + private static InstallState s_servicesInstalled = InstallState.Unknown; + private static object s_servicesInstalledLock = new object(); + + #endregion Private Fields + } +} diff --git a/Libraries/ImeSharp/TextStore.cs b/Libraries/ImeSharp/TextStore.cs new file mode 100644 index 000000000..b7bf9cf87 --- /dev/null +++ b/Libraries/ImeSharp/TextStore.cs @@ -0,0 +1,963 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using ImeSharp.Native; +using SharpGen.Runtime; +using SharpGen.Runtime.Win32; +using TsfSharp; + +namespace ImeSharp +{ + internal class TextStore : CallbackBase, + ITextStoreACP, + ITfContextOwnerCompositionSink, + ITfTextEditSink, + ITfUIElementSink + { + public static readonly Guid IID_ITextStoreACPSink = new Guid(0x22d44c94, 0xa419, 0x4542, 0xa2, 0x72, 0xae, 0x26, 0x09, 0x3e, 0xce, 0xcf); + public static readonly Guid GUID_PROP_COMPOSING = new Guid("e12ac060-af15-11d2-afc5-00105a2799b5"); + + //------------------------------------------------------ + // + // Constructors + // + //------------------------------------------------------ + + #region Constructors + + // Creates a TextStore instance. + public TextStore(IntPtr windowHandle) + { + _windowHandle = windowHandle; + + + _viewCookie = Environment.TickCount; + + _editCookie = Tsf.TF_INVALID_COOKIE; + _uiElementSinkCookie = Tsf.TF_INVALID_COOKIE; + _textEditSinkCookie = Tsf.TF_INVALID_COOKIE; + + _IMEStringPool = ArrayPool.Shared; + } + + #endregion Constructors + + //------------------------------------------------------ + // + // Methods - ITextStoreACP + // + //------------------------------------------------------ + + #region ITextStoreACP + + public void AdviseSink(Guid riid, IUnknown obj, int flags) + { + ITextStoreACPSink sink; + + if (riid != IID_ITextStoreACPSink) + throw new COMException("TextStore_CONNECT_E_CANNOTCONNECT"); + + sink = (obj as ComObject).QueryInterface(); + + if (sink == null) + throw new COMException("TextStore_E_NOINTERFACE"); + + // It's legal to replace existing sink. + if (_sink != null) + _sink.Dispose(); + + (obj as ComObject).Dispose(); + + _sink = sink; + } + + public void UnadviseSink(IUnknown obj) + { + var sink = (obj as ComObject).QueryInterface(); + if (sink.NativePointer != _sink.NativePointer) + throw new COMException("TextStore_CONNECT_E_NOCONNECTION"); + + _sink.Release(); + _sink = null; + } + + private bool _LockDocument(TsfSharp.TsLfFlags dwLockFlags) + { + if (_locked) + return false; + + _locked = true; + _lockFlags = dwLockFlags; + + return true; + } + + private void ResetIfRequired() + { + if (!_commited) + return; + + _commited = false; + + TsTextchange textChange; + textChange.AcpStart = 0; + textChange.AcpOldEnd = _inputBuffer.Count; + textChange.AcpNewEnd = 0; + _inputBuffer.Clear(); + + _sink.OnTextChange(0, textChange); + + _acpStart = _acpEnd = 0; + _sink.OnSelectionChange(); + _commitStart = _commitLength = 0; + + //Debug.WriteLine("TextStore reset!!!"); + } + + private void _UnlockDocument() + { + Result hr; + _locked = false; + _lockFlags = 0; + + ResetIfRequired(); + + //if there is a queued lock, grant it + if (_lockRequestQueue.Count > 0) + { + hr = RequestLock(_lockRequestQueue.Dequeue()); + } + + //if any layout changes occurred during the lock, notify the manager + if (_layoutChanged) + { + _layoutChanged = false; + _sink.OnLayoutChange(TsLayoutCode.TsLcChange, _viewCookie); + } + } + + private bool _IsLocked(TsfSharp.TsLfFlags dwLockType) + { + return _locked && (_lockFlags & dwLockType) != 0; + } + + public Result RequestLock(TsfSharp.TsLfFlags dwLockFlags) + { + Result hrSession; + + if (_sink == null) + throw new COMException("TextStore_NoSink"); + + if (dwLockFlags == 0) + throw new COMException("TextStore_BadLockFlags"); + + hrSession = Result.Fail; + + if (_locked) + { + //the document is locked + + if ((dwLockFlags & TsfSharp.TsLfFlags.Sync) == TsfSharp.TsLfFlags.Sync) + { + /* + The caller wants an immediate lock, but this cannot be granted because + the document is already locked. + */ + hrSession = (int)TsErrors.TsESynchronous; + } + else + { + //the request is asynchronous + + //Queue the lock request + _lockRequestQueue.Enqueue(dwLockFlags); + hrSession = (int)TsErrors.TsSAsync; + } + + return hrSession; + } + + //lock the document + _LockDocument(dwLockFlags); + + //call OnLockGranted + hrSession = _sink.OnLockGranted(dwLockFlags); + + //unlock the document + _UnlockDocument(); + + return hrSession; + } + + public TsStatus GetStatus() + { + TsStatus status = new TsStatus(); + status.DynamicFlags = 0; + status.StaticFlags = 0; + + return status; + } + + public void QueryInsert(int acpTestStart, int acpTestEnd, uint cch, out int acpResultStart, out int acpResultEnd) + { + acpResultStart = acpResultEnd = 0; + + // Fix possible crash + if (_inputBuffer.Count == 0) + return; + + //Queryins + if (acpTestStart > _inputBuffer.Count || acpTestEnd > _inputBuffer.Count) + throw new COMException("", Result.InvalidArg.Code); + + //Microsoft Pinyin seems does not init the result value, so we set the test value here, in case crash + acpResultStart = acpTestStart; + acpResultEnd = acpTestEnd; + } + + public uint GetSelection(uint index, ref TsSelectionAcp selection) + { + //does the caller have a lock + if (!_IsLocked(TsLfFlags.Read)) + { + //the caller doesn't have a lock + //return NativeMethods.TS_E_NOLOCK; + throw new COMException("", (int)TsErrors.TsENolock); + } + + //check the requested index + if (-1 == (int)index) + { + index = 0; + } + else if (index > 1) + { + /* + The index is too high. This app only supports one selection. + */ + throw new COMException("", Result.InvalidArg.Code); + } + + selection.AcpStart = _acpStart; + selection.AcpEnd = _acpEnd; + selection.Style.InterimCharFlag = _interimChar; + + if (_interimChar) + { + /* + fInterimChar will be set when an intermediate character has been + set. One example of when this will happen is when an IME is being + used to enter characters and a character has been set, but the IME + is still active. + */ + selection.Style.Ase = TsActiveSelEnd.TsAeNone; + } + else + { + selection.Style.Ase = _activeSelectionEnd; + } + + return 1; + } + + public void SetSelection(uint count, ref TsSelectionAcp selections) + { + //this implementaiton only supports a single selection + if (count != 1) + throw new COMException("", Result.InvalidArg.Code); + + //does the caller have a lock + if (!_IsLocked(TsLfFlags.Readwrite)) + { + //the caller doesn't have a lock + //return NativeMethods.TS_E_NOLOCK; + throw new COMException("", (int)TsErrors.TsENolock); + } + + _acpStart = selections.AcpStart; + _acpEnd = selections.AcpEnd; + _interimChar = selections.Style.InterimCharFlag; + + if (_interimChar) + { + /* + fInterimChar will be set when an intermediate character has been + set. One example of when this will happen is when an IME is being + used to enter characters and a character has been set, but the IME + is still active. + */ + _activeSelectionEnd = TsActiveSelEnd.TsAeNone; + } + else + { + _activeSelectionEnd = selections.Style.Ase; + } + + //if the selection end is at the start of the selection, reverse the parameters + int lStart = _acpStart; + int lEnd = _acpEnd; + + if (TsActiveSelEnd.TsAeStart == _activeSelectionEnd) + { + lStart = _acpEnd; + lEnd = _acpStart; + } + } + + + public void GetText(int acpStart, int acpEnd, System.IntPtr pchPlain, uint cchPlainReq, out uint cchPlainRet, + ref TsfSharp.TsRuninfo rgRunInfo, uint cRunInfoReq, out uint cRunInfoRet, out int acpNext) + { + cchPlainRet = 0; + cRunInfoRet = 0; + acpNext = 0; + + //does the caller have a lock + if (!_IsLocked(TsLfFlags.Read)) + { + //the caller doesn't have a lock + throw new COMException("", (int)TsErrors.TsENolock); + } + + bool fDoText = cchPlainReq > 0; + bool fDoRunInfo = cRunInfoReq > 0; + int cchTotal; + + cchPlainRet = 0; + acpNext = acpStart; + + cchTotal = _inputBuffer.Count; + + //validate the start pos + if ((acpStart < 0) || (acpStart > cchTotal)) + { + throw new COMException("", Result.InvalidArg.Code); + } + else + { + //are we at the end of the document + if (acpStart == cchTotal) + { + return; + } + else + { + int cchReq; + + /* + acpEnd will be -1 if all of the text up to the end is being requested. + */ + + if (acpEnd >= acpStart) + { + cchReq = acpEnd - acpStart; + } + else + { + cchReq = cchTotal - acpStart; + } + + if (fDoText) + { + if (cchReq > cchPlainReq) + { + cchReq = (int)cchPlainReq; + } + + //extract the specified text range + if (pchPlain != IntPtr.Zero && cchPlainReq > 0) + { + //_inputBuffer.CopyTo(acpStart, pchPlain, 0, cchReq); + + unsafe + { + var ptr = (char*)pchPlain; + + for (int i = acpStart; i < cchReq; i++) + { + *ptr = _inputBuffer[i]; + ptr++; + } + } + } + } + + //it is possible that only the length of the text is being requested + cchPlainRet = (uint)cchReq; + + if (fDoRunInfo) + { + /* + Runs are used to separate text characters from formatting characters. + + In this example, sequences inside and including the <> are treated as + control sequences and are not displayed. + + Plain text = "Text formatting." + Actual text = "Text formatting." + + If all of this text were requested, the run sequence would look like this: + + prgRunInfo[0].type = TS_RT_PLAIN; //"Text " + prgRunInfo[0].uCount = 5; + + prgRunInfo[1].type = TS_RT_HIDDEN; // + prgRunInfo[1].uCount = 6; + + prgRunInfo[2].type = TS_RT_PLAIN; //"formatting" + prgRunInfo[2].uCount = 10; + + prgRunInfo[3].type = TS_RT_HIDDEN; // + prgRunInfo[3].uCount = 8; + + prgRunInfo[4].type = TS_RT_PLAIN; //"." + prgRunInfo[4].uCount = 1; + + TS_RT_OPAQUE is used to indicate characters or character sequences + that are in the document, but are used privately by the application + and do not map to text. Runs of text tagged with TS_RT_OPAQUE should + NOT be included in the pchPlain or cchPlainOut [out] parameters. + */ + + /* + This implementation is plain text, so the text only consists of one run. + If there were multiple runs, it would be an error to have consecuative runs + of the same type. + */ + rgRunInfo.Type = TsRunType.TsRtPlain; + rgRunInfo.Count = (uint)cchReq; + } + + acpNext = acpStart + cchReq; + } + } + } + + public TsTextchange SetText(int dwFlags, int acpStart, int acpEnd, string pchText, uint cch) + { + /* + dwFlags can be: + TS_ST_CORRECTION + */ + TsTextchange change = new TsTextchange(); + + //set the selection to the specified range + TsSelectionAcp tsa = new TsSelectionAcp(); + tsa.AcpStart = acpStart; + tsa.AcpEnd = acpEnd; + tsa.Style.Ase = TsActiveSelEnd.TsAeStart; + tsa.Style.InterimCharFlag = false; + + SetSelection(1, ref tsa); + + int start, end; + InsertTextAtSelection(TsIasFlags.Noquery, pchText, cch, out start, out end, out change); + + return change; + } + + public IDataObject GetFormattedText(int startIndex, int endIndex) + { + throw new COMException("", Result.NotImplemented.Code); + } + + public IUnknown GetEmbedded(int index, Guid guidService, Guid riid) + { + throw new COMException("", Result.NotImplemented.Code); + } + + public RawBool QueryInsertEmbedded(Guid guidService, ref Formatetc formatEtc) + { + throw new COMException("", Result.NotImplemented.Code); + } + + public TsTextchange InsertEmbedded(int flags, int startIndex, int endIndex, TsfSharp.IDataObject dataObjectRef) + { + throw new COMException("", Result.NotImplemented.Code); + } + + public void InsertTextAtSelection(TsfSharp.TsIasFlags dwFlags, string pchText, uint cch, out int pacpStart, out int pacpEnd, out TsfSharp.TsTextchange pChange) + { + pacpStart = pacpEnd = 0; + pChange = new TsTextchange(); + + //does the caller have a lock + if (!_IsLocked(TsLfFlags.Readwrite)) + { + //the caller doesn't have a lock + throw new COMException("", (int)TsErrors.TsENolock); + } + + int acpStart; + int acpOldEnd; + int acpNewEnd; + + acpOldEnd = _acpEnd; + + //set the start point after the insertion + acpStart = _acpStart; + + //set the end point after the insertion + acpNewEnd = _acpStart + (int)cch; + + if ((dwFlags & TsIasFlags.Queryonly) == TsIasFlags.Queryonly) + { + pacpStart = acpStart; + pacpEnd = acpOldEnd; + return; + } + + //insert the text + _inputBuffer.RemoveRange(acpStart, acpOldEnd - acpStart); + _inputBuffer.InsertRange(acpStart, pchText); + + //set the selection + _acpStart = acpStart; + _acpEnd = acpNewEnd; + + if ((dwFlags & TsIasFlags.Noquery) != TsIasFlags.Noquery) + { + pacpStart = acpStart; + pacpEnd = acpNewEnd; + } + + //set the TS_TEXTCHANGE members + pChange.AcpStart = acpStart; + pChange.AcpOldEnd = acpOldEnd; + pChange.AcpNewEnd = acpNewEnd; + + //defer the layout change notification until the document is unlocked + _layoutChanged = true; + } + + public void InsertEmbeddedAtSelection(int flags, IDataObject obj, out int startIndex, out int endIndex, out TsTextchange change) + { + startIndex = endIndex = 0; + change = new TsTextchange(); + throw new COMException("", Result.NotImplemented.Code); + } + + public void RequestSupportedAttrs(int flags, uint cFilterAttrs, ref Guid filterAttributes) + { + } + + public void RequestAttrsAtPosition(int index, uint cFilterAttrs, ref Guid filterAttributes, int flags) + { + throw new COMException("", Result.NotImplemented.Code); + } + + + public void RequestAttrsTransitioningAtPosition(int position, uint cFilterAttrs, ref Guid filterAttributes, int flags) + { + throw new COMException("", Result.NotImplemented.Code); + } + + public void FindNextAttrTransition(int startIndex, int haltIndex, uint cFilterAttrs, ref Guid filterAttributes, int flags, out int acpNext, out RawBool found, out int foundOffset) + { + acpNext = 0; + found = false; + foundOffset = 0; + } + + public uint RetrieveRequestedAttrs(uint ulCount, ref TsfSharp.TsAttrval aAttrValsRef) + { + return 0; + } + + public int GetEndACP() + { + int acp = 0; + //does the caller have a lock + if (!_IsLocked(TsLfFlags.Read)) + { + //the caller doesn't have a lock + throw new COMException("", (int)TsErrors.TsENolock); + } + + acp = _inputBuffer.Count; + + return acp; + } + + public int GetActiveView() + { + return _viewCookie; + } + + public int GetACPFromPoint(int viewCookie, TsfSharp.Point tsfPoint, int dwFlags) + { + throw new COMException("", Result.NotImplemented.Code); + } + + public void GetTextExt(int viewCookie, int acpStart, int acpEnd, out Rect rect, out RawBool clipped) + { + clipped = false; + rect = InputMethod.TextInputRect; + + if (_viewCookie != viewCookie) + throw new COMException("", Result.InvalidArg.Code); + + //does the caller have a lock + if (!_IsLocked(TsLfFlags.Read)) + { + //the caller doesn't have a lock + throw new COMException("", (int)TsErrors.TsENolock); + } + + //According to Microsoft's doc, an ime should not make empty request, + //but some ime draw comp text themseleves, when empty req will be make + //Check empty request + //if (acpStart == acpEnd) { + // return E_INVALIDARG; + //} + + NativeMethods.MapWindowPoints(_windowHandle, IntPtr.Zero, ref rect, 2); + } + + public Rect GetScreenExt(int viewCookie) + { + Rect rect = new Rect(); + + if (_viewCookie != viewCookie) + throw new COMException("", Result.InvalidArg.Code); + + NativeMethods.GetWindowRect(_windowHandle, out rect); + + return rect; + } + + public IntPtr GetWnd(int viewCookie) + { + if (viewCookie != _viewCookie) + { + throw new COMException("", Result.False.Code); + } + + return _windowHandle; + } + + #endregion ITextStoreACP2 + + + //------------------------------------------------------ + // + // Public Methods - ITfContextOwnerCompositionSink + // + //------------------------------------------------------ + + #region ITfContextOwnerCompositionSink + + public RawBool OnStartComposition(ITfCompositionView view) + { + // Return true in ok to start the composition. + RawBool ok = true; + _compositionStart = _compositionLength = 0; + _currentComposition.Clear(); + + InputMethod.OnTextCompositionStarted(this); + _compViews.Add(view); + + return ok; + } + + public void OnUpdateComposition(ITfCompositionView view, ITfRange rangeNew) + { + var range = view.Range; + var rangeacp = range.QueryInterface(); + + rangeacp.GetExtent(out _compositionStart, out _compositionLength); + rangeacp.Dispose(); + range.Dispose(); + _compViews.Add(view); + } + + public void OnEndComposition(ITfCompositionView view) + { + var range = view.Range; + var rangeacp = range.QueryInterface(); + + rangeacp.GetExtent(out _commitStart, out _commitLength); + rangeacp.Dispose(); + range.Dispose(); + + // Ensure composition string reset + _compositionStart = _compositionLength = 0; + _currentComposition.Clear(); + + InputMethod.ClearCandidates(); + InputMethod.OnTextCompositionEnded(this); + view.Dispose(); + foreach(var item in _compViews) + item.Dispose(); + _compViews.Clear(); + } + + #endregion ITfContextOwnerCompositionSink + + #region ITfTextEditSink + + public void OnEndEdit(ITfContext context, int ecReadOnly, ITfEditRecord editRecord) + { + ITfProperty property = context.GetProperty(GUID_PROP_COMPOSING); + + ITfRangeACP rangeACP = TextServicesContext.Current.ContextOwnerServices.CreateRange(_compositionStart, _compositionStart + _compositionLength); + Variant val = property.GetValue(ecReadOnly, rangeACP); + property.Dispose(); + rangeACP.Dispose(); + if (val.Value == null || (int)val.Value == 0) + { + if (_commitLength == 0 || _inputBuffer.Count == 0) + return; + + //Debug.WriteLine("Composition result: {0}", new object[] { new string(_inputBuffer.GetRange(_commitStart, _commitLength).ToArray()) }); + + _commited = true; + for (int i = 0; i < _commitLength; i++) + InputMethod.OnTextCompositionResult(this, new string(_inputBuffer.GetRange(_commitStart, _commitLength).ToArray())); + } + + if (_commited) + return; + + if (_inputBuffer.Count == 0 && _compositionLength > 0) // Composition just ended + return; + + _currentComposition.Clear(); + for (int i = 0; i < _compositionLength; i++) + _currentComposition.Add(_inputBuffer[_compositionStart + i]); + + InputMethod.OnTextComposition(this, new IMEString(_currentComposition), _acpEnd); + + //var compStr = new string(_currentComposition.ToArray()); + //compStr = compStr.Insert(_acpEnd, "|"); + //Debug.WriteLine("Composition string: {0}, cursor pos: {1}", compStr, _acpEnd); + } + + #endregion ITfTextEditSink + + //------------------------------------------------------ + // + // Public Methods - ITfUIElementSink + // + //------------------------------------------------------ + + #region ITfUIElementSink + + public RawBool BeginUIElement(int dwUIElementId) + { + // Hide OS rendered Candidate list Window + RawBool pbShow = InputMethod.ShowOSImeWindow; + + OnUIElement(dwUIElementId, true); + + return pbShow; + } + + public void UpdateUIElement(int dwUIElementId) + { + OnUIElement(dwUIElementId, false); + } + + public void EndUIElement(int dwUIElementId) + { + } + + private void OnUIElement(int uiElementId, bool onStart) + { + if (InputMethod.ShowOSImeWindow || !_supportUIElement) return; + + ITfUIElement uiElement = TextServicesContext.Current.UIElementMgr.GetUIElement(uiElementId); + + ITfCandidateListUIElementBehavior candList; + + try + { + candList = uiElement.QueryInterface(); + } + catch (SharpGenException) + { + _supportUIElement = false; + return; + } + finally + { + uiElement.Dispose(); + } + + uint selection = 0; + uint currentPage = 0; + uint count = 0; + uint pageCount = 0; + uint pageStart = 0; + uint pageSize = 0; + uint i, j; + + selection = candList.GetSelection(); + currentPage = candList.GetCurrentPage(); + + count = candList.GetCount(); + + pageCount = candList.GetPageIndex(null, 0); + + if (pageCount > 0) + { + uint[] pageStartIndexes = ArrayPool.Shared.Rent((int)pageCount); + pageCount = candList.GetPageIndex(pageStartIndexes, pageCount); + pageStart = pageStartIndexes[currentPage]; + + if (pageStart >= count - 1) + { + candList.Abort(); + ArrayPool.Shared.Return(pageStartIndexes); + return; + } + + if (currentPage < pageCount - 1) + pageSize = Math.Min(count, pageStartIndexes[currentPage + 1]) - pageStart; + else + pageSize = count - pageStart; + + ArrayPool.Shared.Return(pageStartIndexes); + } + + selection -= pageStart; + + IMEString[] candidates = _IMEStringPool.Rent((int)pageSize); + + IntPtr bStrPtr; + for (i = pageStart, j = 0; i < count && j < pageSize; i++, j++) + { + bStrPtr = candList.GetString(i); + candidates[j] = new IMEString(bStrPtr); + } + + //Debug.WriteLine("TSF========TSF"); + //Debug.WriteLine("pageStart: {0}, pageSize: {1}, selection: {2}, currentPage: {3} candidates:", pageStart, pageSize, selection, currentPage); + //for (int k = 0; k < candidates.Length; k++) + // Debug.WriteLine(" {2}{0}.{1}", k + 1, candidates[k], k == selection ? "*" : ""); + //Debug.WriteLine("TSF++++++++TSF"); + + InputMethod.CandidatePageStart = (int)pageStart; + InputMethod.CandidatePageSize = (int)pageSize; + InputMethod.CandidateSelection = (int)selection; + InputMethod.CandidateList = candidates; + + if (_currentComposition != null) + { + InputMethod.OnTextComposition(this, new IMEString(_currentComposition), _acpEnd); + _IMEStringPool.Return(candidates); + } + + candList.Dispose(); + } + + #endregion ITfUIElementSink + + //------------------------------------------------------ + // + // Public Properties + // + //------------------------------------------------------ + + public static TextStore Current + { + get + { + TextStore defaultTextStore = InputMethod.DefaultTextStore; + if (defaultTextStore == null) + { + defaultTextStore = InputMethod.DefaultTextStore = new TextStore(InputMethod.WindowHandle); + + defaultTextStore.Register(); + } + + return defaultTextStore; + } + } + + public ITfDocumentMgr DocumentManager + { + get { return _documentMgr; } + set { _documentMgr = value; } + } + + // EditCookie for ITfContext. + public int EditCookie + { + // get { return _editCookie; } + set { _editCookie = value; } + } + + public int UIElementSinkCookie + { + get { return _uiElementSinkCookie; } + set { _uiElementSinkCookie = value; } + } + + public int TextEditSinkCookie + { + get { return _textEditSinkCookie; } + set { _textEditSinkCookie = value; } + } + + public bool SupportUIElement { get { return _supportUIElement; } } + + + //------------------------------------------------------ + // + // Private Methods + // + //------------------------------------------------------ + + // This function calls TextServicesContext to create TSF document and start transitory extension. + private void Register() + { + // Create TSF document and advise the sink to it. + TextServicesContext.Current.RegisterTextStore(this); + } + + //------------------------------------------------------ + // + // Private Fields + // + //------------------------------------------------------ + + // The TSF document object. This is a native resource. + private ITfDocumentMgr _documentMgr; + + private int _viewCookie; + + // The edit cookie TSF returns from CreateContext. + private int _editCookie; + private int _uiElementSinkCookie; + private int _textEditSinkCookie; + + private ITextStoreACPSink _sink; + private IntPtr _windowHandle; + private int _acpStart; + private int _acpEnd; + private bool _interimChar; + private TsActiveSelEnd _activeSelectionEnd; + private List _inputBuffer = new List(); + + private bool _locked; + private TsLfFlags _lockFlags; + private Queue _lockRequestQueue = new Queue(); + private bool _layoutChanged; + + private List _currentComposition = new List(); + private int _compositionStart; + private int _compositionLength; + private int _commitStart; + private int _commitLength; + private bool _commited; + + private bool _supportUIElement = true; + private List _compViews = new List(); + + private ArrayPool _IMEStringPool; + + } +} diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/GameWindow.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/GameWindow.cs index ec4d56407..8b166be16 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/GameWindow.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/GameWindow.cs @@ -106,6 +106,11 @@ namespace Microsoft.Xna.Framework { /// This event is only supported on the Windows DirectX, Windows OpenGL and Linux platforms. /// public event EventHandler TextInput; + + /// + /// Used for displaying uncommitted IME text. + /// + public event EventHandler TextEditing; #endif #endregion Events @@ -152,6 +157,11 @@ namespace Microsoft.Xna.Framework { { EventHelpers.Raise(this, TextInput, e); } + + protected void OnTextEditing(object sender, TextEditingEventArgs e) + { + EventHelpers.Raise(this, TextEditing, e); + } #endif protected internal abstract void SetSupportedOrientations (DisplayOrientation orientations); diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Linux.NetStandard.csproj b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Linux.NetStandard.csproj index 5f9dbc68a..41c2f71d8 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Linux.NetStandard.csproj +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Linux.NetStandard.csproj @@ -71,6 +71,8 @@ + + Angle,Linux,MacOS,Windows,WindowsGL,WindowsUniversal diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.MacOS.NetStandard.csproj b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.MacOS.NetStandard.csproj index dd5a32d33..a8b412890 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.MacOS.NetStandard.csproj +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.MacOS.NetStandard.csproj @@ -71,6 +71,8 @@ + + Angle,Linux,MacOS,Windows,WindowsGL,WindowsUniversal diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Windows.NetStandard.csproj b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Windows.NetStandard.csproj index 3ff6019b1..e6bc855bf 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Windows.NetStandard.csproj +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Windows.NetStandard.csproj @@ -84,6 +84,8 @@ + + Angle,Linux,MacOS,Windows,WindowsGL,WindowsUniversal diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDL2.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDL2.cs index 5ee24f7f0..8b4672ef6 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDL2.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDL2.cs @@ -240,6 +240,22 @@ internal static class Sdl return pointer; } + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate bool d_sdl_istextinputactive(); + public static d_sdl_istextinputactive SDL_IsTextInputActive = FuncLoader.LoadFunction(NativeLibrary, "SDL_IsTextInputActive"); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void d_sdl_starttextinput(); + public static d_sdl_starttextinput SDL_StartTextInput = FuncLoader.LoadFunction(NativeLibrary, "SDL_StartTextInput"); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void d_sdl_stoptextinput(); + public static d_sdl_stoptextinput SDL_StopTextInput = FuncLoader.LoadFunction(NativeLibrary, "SDL_StopTextInput"); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void d_sdl_settextinputrect(ref Rectangle rect); + public static d_sdl_settextinputrect SDL_SetTextInputRect = FuncLoader.LoadFunction(NativeLibrary, "SDL_SetTextInputRect"); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void d_sdl_clearerror(); public static d_sdl_clearerror ClearError = FuncLoader.LoadFunction(NativeLibrary, "SDL_ClearError"); diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGamePlatform.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGamePlatform.cs index e18d8a4d4..fb2f05833 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGamePlatform.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGamePlatform.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Threading; using System.Runtime.InteropServices; +using System.Text; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; @@ -136,8 +137,7 @@ namespace Microsoft.Xna.Framework { var key = KeyboardUtil.ToXna(ev.Key.Keysym.Sym); - if (!_keys.Contains(key)) - _keys.Add(key); + if (!_keys.Contains(key)) _keys.Add(key); //TODO: rethink all of this char character = (char)KeyboardUtil.ApplyModifiers(ev.Key.Keysym.Sym, ev.Key.Keysym.Mod); @@ -161,24 +161,23 @@ namespace Microsoft.Xna.Framework var key = KeyboardUtil.ToXna(ev.Key.Keysym.Sym); _keys.Remove(key); } + else if (ev.Type == Sdl.EventType.TextEditing) + { + string text; + unsafe { text = ReadString(ev.Text.Text); } + + _view.CallTextEditing(text, ev.Edit.Start, ev.Edit.Length); + } else if (ev.Type == Sdl.EventType.TextInput) { - int len = 0; - string text = String.Empty; - unsafe + string text; + unsafe { text = ReadString(ev.Text.Text); } + + if (text.Length is 0) { continue; } + + foreach (char c in text) { - while (Marshal.ReadByte ((IntPtr)ev.Text.Text, len) != 0) { - len++; - } - var buffer = new byte [len]; - Marshal.Copy ((IntPtr)ev.Text.Text, buffer, 0, len); - text = System.Text.Encoding.UTF8.GetString (buffer); - } - if (text.Length == 0) - continue; - foreach (var c in text) - { - var key = KeyboardUtil.ToXna((int)c); + var key = KeyboardUtil.ToXna(c); _view.CallTextInput(c, key); } } @@ -194,11 +193,22 @@ namespace Microsoft.Xna.Framework IsActive = false; else if (ev.Window.EventID == Sdl.Window.EventId.Moved) _view.Moved(); - else if (ev.Window.EventID == Sdl.Window.EventId.Close) - _isExiting++; + else if (ev.Window.EventID == Sdl.Window.EventId.Close) _isExiting++; } } } + + static unsafe string ReadString(byte* ptr) + { + int len = 0; + while (Marshal.ReadByte((IntPtr)ptr, len) != 0) + { + len++; + } + var buffer = new byte [len]; + Marshal.Copy((IntPtr)ptr, buffer, 0, len); + return Encoding.UTF8.GetString(buffer); + } } public override void StartRunLoop() diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGameWindow.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGameWindow.cs index 7d5a071ca..91eab7e6a 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGameWindow.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGameWindow.cs @@ -113,6 +113,12 @@ namespace Microsoft.Xna.Framework Sdl.SetHint("SDL_VIDEO_MINIMIZE_ON_FOCUS_LOSS", "0"); Sdl.SetHint("SDL_JOYSTICK_ALLOW_BACKGROUND_EVENTS", "1"); + /* + * By default SDL2 will hide IME popups since it probably assumes the game will implement their own suggestions box. + * We don't want that, so this hint will allow the system native IME popups to show up when typing in the game. + */ + Sdl.SetHint("SDL_HINT_IME_SHOW_UI", "1"); + // when running NUnit tests entry assembly can be null if (Assembly.GetEntryAssembly() != null) { @@ -333,6 +339,11 @@ namespace Microsoft.Xna.Framework OnTextInput(this, new TextInputEventArgs(c, key)); } + public void CallTextEditing(string text, int start, int length) + { + OnTextEditing(this, new TextEditingEventArgs(text, start, length)); + } + public void DropFile(string filePath) { OnFileDropped(new FileDropEventArgs(filePath)); diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/TextEditingEventArgs.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/TextEditingEventArgs.cs new file mode 100644 index 000000000..cce9ccc66 --- /dev/null +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/TextEditingEventArgs.cs @@ -0,0 +1,17 @@ +using System; +namespace Microsoft.Xna.Framework +{ + public sealed class TextEditingEventArgs : EventArgs + { + public readonly string Text; + public readonly int Start; + public readonly int Length; + + public TextEditingEventArgs(string text, int start, int length) + { + Text = text; + Start = start; + Length = length; + } + } +} diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/TextInput.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/TextInput.cs new file mode 100644 index 000000000..df18c7bba --- /dev/null +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/TextInput.cs @@ -0,0 +1,29 @@ +#nullable enable + +namespace Microsoft.Xna.Framework +{ + public static class TextInput + { + public static void StartTextInput() + { + Sdl.SDL_StartTextInput(); + } + + public static void StopTextInput() + { + Sdl.SDL_StopTextInput(); + } + + public static void SetTextInputRect(Rectangle rectangle) + { + Sdl.Rectangle r = new Sdl.Rectangle + { + X = rectangle.X, + Y = rectangle.Y, + Width = rectangle.Width, + Height = rectangle.Height + }; + Sdl.SDL_SetTextInputRect(ref r); + } + } +} diff --git a/Libraries/MonoGame.Framework/Src/ThirdParty/Dependencies/SDL/Linux/x64/libSDL2-2.0.so.0 b/Libraries/MonoGame.Framework/Src/ThirdParty/Dependencies/SDL/Linux/x64/libSDL2-2.0.so.0 old mode 100644 new mode 100755 index bdd69273a..846fdc9a0 Binary files a/Libraries/MonoGame.Framework/Src/ThirdParty/Dependencies/SDL/Linux/x64/libSDL2-2.0.so.0 and b/Libraries/MonoGame.Framework/Src/ThirdParty/Dependencies/SDL/Linux/x64/libSDL2-2.0.so.0 differ diff --git a/Libraries/MonoGame.Framework/Src/ThirdParty/Dependencies/SDL/Linux/x86/libSDL2-2.0.so.0 b/Libraries/MonoGame.Framework/Src/ThirdParty/Dependencies/SDL/Linux/x86/libSDL2-2.0.so.0 old mode 100644 new mode 100755 index 1f9bf7bb4..33ae87596 Binary files a/Libraries/MonoGame.Framework/Src/ThirdParty/Dependencies/SDL/Linux/x86/libSDL2-2.0.so.0 and b/Libraries/MonoGame.Framework/Src/ThirdParty/Dependencies/SDL/Linux/x86/libSDL2-2.0.so.0 differ diff --git a/LinuxSolution.sln b/LinuxSolution.sln index 6d0f82d4f..8c10904cd 100644 --- a/LinuxSolution.sln +++ b/LinuxSolution.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29201.188 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.32014.148 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D32A29D8-AC7B-4189-B734-8ED9EB4120D0}" ProjectSection(SolutionItems) = preProject @@ -39,11 +39,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LinuxServer", "Barotrauma\B EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonoGame.Framework.Linux.NetStandard", "Libraries\MonoGame.Framework\Src\MonoGame.Framework\MonoGame.Framework.Linux.NetStandard.csproj", "{33E95A21-E071-4432-819F-AA64CF3EF3F1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LinuxTest", "Barotrauma\BarotraumaTest\LinuxTest.csproj", "{F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LinuxTest", "Barotrauma\BarotraumaTest\LinuxTest.csproj", "{F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeployAll", "Deploy\DeployAll\DeployAll.csproj", "{60B82E13-2CDD-4C74-8373-FD7264D6C80B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DeployAll", "Deploy\DeployAll\DeployAll.csproj", "{60B82E13-2CDD-4C74-8373-FD7264D6C80B}" EndProject Global + GlobalSection(SharedMSBuildProjectFiles) = preSolution + Libraries\GameAnalytics\GA-SDK-MONO-SHARED\GA-SDK-MONO-SHARED.projitems*{95c4d59d-9be4-4278-b4f8-46c0ba1a3916}*SharedItemsImports = 5 + EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64 diff --git a/MacSolution.sln b/MacSolution.sln index 77418fa58..ee5cddf3f 100644 --- a/MacSolution.sln +++ b/MacSolution.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29201.188 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.32014.148 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D32A29D8-AC7B-4189-B734-8ED9EB4120D0}" ProjectSection(SolutionItems) = preProject @@ -39,11 +39,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonoGame.Framework.MacOS.Ne EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Facepunch.Steamworks.Posix", "Libraries\Facepunch.Steamworks\Facepunch.Steamworks.Posix.csproj", "{F10CE3BB-26B8-446E-84D2-86D25E850F61}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MacTest", "Barotrauma\BarotraumaTest\MacTest.csproj", "{20BC9336-B439-4BF1-8B65-D587DBF421D1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MacTest", "Barotrauma\BarotraumaTest\MacTest.csproj", "{20BC9336-B439-4BF1-8B65-D587DBF421D1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeployAll", "Deploy\DeployAll\DeployAll.csproj", "{36B38D18-3574-4B67-A89C-FD3C2D39F1D6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DeployAll", "Deploy\DeployAll\DeployAll.csproj", "{36B38D18-3574-4B67-A89C-FD3C2D39F1D6}" EndProject Global + GlobalSection(SharedMSBuildProjectFiles) = preSolution + Libraries\GameAnalytics\GA-SDK-MONO-SHARED\GA-SDK-MONO-SHARED.projitems*{c54f0dfe-add3-4767-8cbc-101859218d66}*SharedItemsImports = 5 + EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64 diff --git a/WindowsSolution.sln b/WindowsSolution.sln index 54c1ad98e..d66f0eaf5 100644 --- a/WindowsSolution.sln +++ b/WindowsSolution.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29201.188 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.32014.148 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D32A29D8-AC7B-4189-B734-8ED9EB4120D0}" ProjectSection(SolutionItems) = preProject @@ -39,9 +39,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XNATypes", "Libraries\XNATy EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpFont.NetStandard", "Libraries\SharpFont\Source\SharpFont\SharpFont.NetStandard.csproj", "{6911872D-40EF-400C-B0A1-9985A19ED488}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowsTest", "Barotrauma\BarotraumaTest\WindowsTest.csproj", "{C7212AE2-A925-4225-A639-AE0653EF65B0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsTest", "Barotrauma\BarotraumaTest\WindowsTest.csproj", "{C7212AE2-A925-4225-A639-AE0653EF65B0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeployAll", "Deploy\DeployAll\DeployAll.csproj", "{C98FE0D0-BC7D-4806-B592-734B53016FD8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DeployAll", "Deploy\DeployAll\DeployAll.csproj", "{C98FE0D0-BC7D-4806-B592-734B53016FD8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImeSharp", "Libraries\ImeSharp\ImeSharp.csproj", "{D42D8E48-A687-445D-AAF1-9E7E6EBF1DE6}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution @@ -131,6 +133,12 @@ Global {C98FE0D0-BC7D-4806-B592-734B53016FD8}.Release|x64.Build.0 = Release|Any CPU {C98FE0D0-BC7D-4806-B592-734B53016FD8}.Unstable|x64.ActiveCfg = Debug|Any CPU {C98FE0D0-BC7D-4806-B592-734B53016FD8}.Unstable|x64.Build.0 = Debug|Any CPU + {D42D8E48-A687-445D-AAF1-9E7E6EBF1DE6}.Debug|x64.ActiveCfg = Debug|Any CPU + {D42D8E48-A687-445D-AAF1-9E7E6EBF1DE6}.Debug|x64.Build.0 = Debug|Any CPU + {D42D8E48-A687-445D-AAF1-9E7E6EBF1DE6}.Release|x64.ActiveCfg = Release|Any CPU + {D42D8E48-A687-445D-AAF1-9E7E6EBF1DE6}.Release|x64.Build.0 = Release|Any CPU + {D42D8E48-A687-445D-AAF1-9E7E6EBF1DE6}.Unstable|x64.ActiveCfg = Debug|Any CPU + {D42D8E48-A687-445D-AAF1-9E7E6EBF1DE6}.Unstable|x64.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -150,6 +158,7 @@ Global {6911872D-40EF-400C-B0A1-9985A19ED488} = {DE36F45F-F09E-4719-B953-00D148F7722A} {C7212AE2-A925-4225-A639-AE0653EF65B0} = {78A9F0AA-5519-407A-9B72-2A09F5DF7068} {C98FE0D0-BC7D-4806-B592-734B53016FD8} = {F35DF9BF-0BED-4FEF-A51C-DD83C531882F} + {D42D8E48-A687-445D-AAF1-9E7E6EBF1DE6} = {DE36F45F-F09E-4719-B953-00D148F7722A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {17032EAB-554B-4B44-A4F6-EFB177ACAB7A}