Merge remote-tracking branch 'upstream/dev' into develop

This commit is contained in:
EvilFactory
2022-12-09 17:33:44 -03:00
416 changed files with 12674 additions and 5862 deletions

View File

@@ -175,11 +175,11 @@ namespace Barotrauma
position += amount;
}
public void ClientWrite(IWriteMessage msg)
public void ClientWrite(in SegmentTableWriter<ClientNetSegment> segmentTableWriter, IWriteMessage msg)
{
if (Character.Controlled != null && !Character.Controlled.IsDead) { return; }
msg.WriteByte((byte)ClientNetObject.SPECTATING_POS);
segmentTableWriter.StartNewSegment(ClientNetSegment.SpectatingPos);
msg.WriteSingle(position.X);
msg.WriteSingle(position.Y);
}

View File

@@ -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);

View File

@@ -442,8 +442,7 @@ namespace Barotrauma
{
foreach (Limb limb in Limbs)
{
if (limb == null || limb.IsSevered || limb.ActiveSprite == null) { continue; }
if (limb == null || limb.IsSevered || limb.ActiveSprite == null || !limb.DoesFlip) { continue; }
Vector2 spriteOrigin = limb.ActiveSprite.Origin;
spriteOrigin.X = limb.ActiveSprite.SourceRect.Width - spriteOrigin.X;
limb.ActiveSprite.Origin = spriteOrigin;
@@ -468,7 +467,10 @@ namespace Barotrauma
{
var damageSound = character.GetSound(s => s.Type == CharacterSound.SoundType.Damage);
float range = damageSound != null ? damageSound.Range * 2 : ConvertUnits.ToDisplayUnits(character.AnimController.Collider.GetSize().Length() * 10);
SoundPlayer.PlayDamageSound(limbJoint.Params.BreakSound, 1.0f, limbJoint.LimbA.body.DrawPosition, range: range);
if (!limbJoint.Params.BreakSound.IsNullOrEmpty() && !limbJoint.Params.BreakSound.Equals("none", StringComparison.OrdinalIgnoreCase))
{
SoundPlayer.PlayDamageSound(limbJoint.Params.BreakSound, 1.0f, limbJoint.LimbA.body.DrawPosition, range: range);
}
}
}

View File

@@ -42,7 +42,7 @@ namespace Barotrauma
if (sound != null)
{
SoundPlayer.PlaySound(sound.Sound, worldPosition, sound.Volume, sound.Range, ignoreMuffling: sound.IgnoreMuffling);
SoundPlayer.PlaySound(sound.Sound, worldPosition, sound.Volume, sound.Range, ignoreMuffling: sound.IgnoreMuffling, freqMult: sound.GetRandomFrequencyMultiplier());
}
}
}

View File

@@ -109,6 +109,22 @@ namespace Barotrauma
set => grainStrength = Math.Max(0, value);
}
/// <summary>
/// Can be used to set camera shake from status effects
/// </summary>
public float CameraShake
{
get { return Screen.Selected?.Cam?.Shake ?? 0.0f; }
set
{
if (!MathUtils.IsValid(value)) { return; }
if (Screen.Selected?.Cam != null)
{
Screen.Selected.Cam.Shake = value;
}
}
}
private readonly List<ParticleEmitter> bloodEmitters = new List<ParticleEmitter>();
public IEnumerable<ParticleEmitter> BloodEmitters
{
@@ -637,18 +653,6 @@ namespace Barotrauma
partial void UpdateProjSpecific(float deltaTime, Camera cam)
{
if (InvisibleTimer > 0.0f)
{
if (Controlled == null || Controlled == this || (Controlled.CharacterHealth.GetAffliction("psychosis")?.Strength ?? 0.0f) <= 0.0f)
{
InvisibleTimer = 0.0f;
}
else
{
InvisibleTimer -= deltaTime;
}
}
foreach (GUIMessage message in guiMessages)
{
bool wasPending = message.Timer < 0.0f;
@@ -965,7 +969,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));

View File

@@ -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;
@@ -10,7 +9,7 @@ using System.Linq;
namespace Barotrauma
{
class CharacterHUD
partial class CharacterHUD
{
const float BossHealthBarDuration = 120.0f;
@@ -100,8 +99,9 @@ namespace Barotrauma
}
}
public static bool ShouldRecreateHudTexts { get; set; } = true;
private static bool heldDownShiftWhenGotHudTexts;
public static bool RecreateHudTexts { get; set; } = true;
private static bool lastHudTextsContextual;
private static float timeHealthWindowClosed;
public static bool IsCampaignInterfaceOpen =>
GameMain.GameSession?.Campaign != null &&
@@ -114,7 +114,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;
}
@@ -175,7 +175,8 @@ namespace Barotrauma
if (character.Info != null && !character.ShouldLockHud() && character.SelectedCharacter == null && Screen.Selected != GameMain.SubEditorScreen)
{
bool mouseOnPortrait = MouseOnCharacterPortrait() && GUI.MouseOn == null;
if (mouseOnPortrait && PlayerInput.PrimaryMouseButtonClicked() && Inventory.DraggingItems.None())
bool healthWindowOpen = CharacterHealth.OpenHealthWindow != null || timeHealthWindowClosed < 0.2f;
if (mouseOnPortrait && !healthWindowOpen && PlayerInput.PrimaryMouseButtonClicked() && Inventory.DraggingItems.None())
{
CharacterHealth.OpenHealthWindow = character.CharacterHealth;
}
@@ -217,7 +218,7 @@ namespace Barotrauma
if (focusedItemOverlayTimer <= 0.0f)
{
focusedItem = null;
ShouldRecreateHudTexts = true;
RecreateHudTexts = true;
}
}
}
@@ -243,6 +244,15 @@ namespace Barotrauma
}
}
}
if (CharacterHealth.OpenHealthWindow != null)
{
timeHealthWindowClosed = 0.0f;
}
else
{
timeHealthWindowClosed += deltaTime;
}
}
public static void Draw(SpriteBatch spriteBatch, Character character, Camera cam)
@@ -307,7 +317,7 @@ namespace Barotrauma
{
if (!brokenItem.IsInteractable(character)) { continue; }
float alpha = GetDistanceBasedIconAlpha(brokenItem);
if (alpha <= 0.0f) continue;
if (alpha <= 0.0f) { continue; }
GUI.DrawIndicator(spriteBatch, brokenItem.DrawPosition, cam, 100.0f, GUIStyle.BrokenIcon.Value.Sprite,
Color.Lerp(GUIStyle.Red, GUIStyle.Orange * 0.5f, brokenItem.Condition / brokenItem.MaxCondition) * alpha);
}
@@ -330,7 +340,7 @@ namespace Barotrauma
if (focusedItem != character.FocusedItem)
{
focusedItemOverlayTimer = Math.Min(1.0f, focusedItemOverlayTimer);
ShouldRecreateHudTexts = true;
RecreateHudTexts = true;
}
focusedItem = character.FocusedItem;
}
@@ -354,14 +364,14 @@ namespace Barotrauma
if (!GUI.DisableItemHighlights && !Inventory.DraggingItemToWorld)
{
bool shiftDown = PlayerInput.IsShiftDown();
if (ShouldRecreateHudTexts || heldDownShiftWhenGotHudTexts != shiftDown)
bool hudTextsContextual = PlayerInput.IsShiftDown();
if (RecreateHudTexts || lastHudTextsContextual != hudTextsContextual)
{
ShouldRecreateHudTexts = true;
heldDownShiftWhenGotHudTexts = shiftDown;
RecreateHudTexts = true;
lastHudTextsContextual = hudTextsContextual;
}
var hudTexts = focusedItem.GetHUDTexts(character, ShouldRecreateHudTexts);
ShouldRecreateHudTexts = false;
var hudTexts = focusedItem.GetHUDTexts(character, RecreateHudTexts);
RecreateHudTexts = false;
int dir = Math.Sign(focusedItem.WorldPosition.X - character.WorldPosition.X);
@@ -492,7 +502,17 @@ namespace Barotrauma
{
var item = character.Inventory.GetItemAt(i);
if (item == null || character.Inventory.SlotTypes[i] == InvSlotType.Any) { continue; }
//if the item is also equipped in another slot we already went through, don't draw the hud again
bool duplicateFound = false;
for (int j = 0; j < i; j++)
{
if (character.Inventory.SlotTypes[j] != InvSlotType.Any && character.Inventory.GetItemAt(j) == item)
{
duplicateFound = true;
break;
}
}
if (duplicateFound) { continue; }
foreach (ItemComponent ic in item.Components)
{
if (ic.DrawHudWhenEquipped) { ic.DrawHUD(spriteBatch, character); }
@@ -548,7 +568,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 +664,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 +695,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 +738,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);
@@ -762,5 +790,25 @@ namespace Barotrauma
Vector2 drawPos = objectiveEntity.Entity.WorldPosition;// + Vector2.UnitX * objectiveEntity.Sprite.size.X * 1.5f;
GUI.DrawIndicator(spriteBatch, drawPos, cam, 100.0f, objectiveEntity.Sprite, objectiveEntity.Color * iconAlpha);
}
static partial void RecreateHudTextsIfControllingProjSpecific(Character character)
{
if (character == Character.Controlled)
{
RecreateHudTexts = true;
}
}
static partial void RecreateHudTextsIfFocusedProjSpecific(params Item[] items)
{
foreach (var item in items)
{
if (item == Character.Controlled?.FocusedItem)
{
RecreateHudTexts = true;
break;
}
}
}
}
}

View File

@@ -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))
{

View File

@@ -113,9 +113,9 @@ namespace Barotrauma
}
}
public void ClientWriteInput(IWriteMessage msg)
public void ClientWriteInput(in SegmentTableWriter<ClientNetSegment> segmentTableWriter, IWriteMessage msg)
{
msg.WriteByte((byte)ClientNetObject.CHARACTER_INPUT);
segmentTableWriter.StartNewSegment(ClientNetSegment.CharacterInput);
if (memInput.Count > 60)
{
@@ -501,7 +501,7 @@ namespace Barotrauma
info?.ClearSavedStatValues(statType);
for (int i = 0; i < savedStatValueCount; i++)
{
string statIdentifier = msg.ReadString();
Identifier statIdentifier = msg.ReadIdentifier();
float statValue = msg.ReadSingle();
bool removeOnDeath = msg.ReadBoolean();
info?.ChangeSavedStatValue(statType, statValue, statIdentifier, removeOnDeath, setValue: true);

View File

@@ -32,12 +32,6 @@ namespace Barotrauma
public static Sprite DamageOverlay => DamageOverlayPrefab.Prefabs.ActivePrefab.DamageOverlay;
private readonly static LocalizedString[] strengthTexts = new LocalizedString[]
{
TextManager.Get("AfflictionStrengthLow"),
TextManager.Get("AfflictionStrengthMedium"),
TextManager.Get("AfflictionStrengthHigh")
};
private Point screenResolution;
@@ -89,11 +83,23 @@ namespace Barotrauma
private int selectedLimbIndex = -1;
private LimbHealth currentDisplayedLimb;
/// <summary>
/// Container for the icons above the health bar
/// </summary>
private GUIComponent afflictionIconContainer;
private GUIButton showHiddenAfflictionsButton;
/// <summary>
/// Container for passive afflictions that have been hidden from afflictionIconContainer
/// </summary>
private GUIComponent hiddenAfflictionIconContainer;
private GUIProgressBar healthWindowHealthBar;
private GUIProgressBar healthWindowHealthBarShadow;
private GUITextBlock characterName;
private GUIListBox afflictionIconContainer;
private GUIListBox afflictionIconList;
private GUILayoutGroup treatmentLayout;
private GUIListBox recommendedTreatmentContainer;
@@ -150,7 +156,7 @@ namespace Barotrauma
Character.Controlled.DeselectCharacter();
}
Character.Controlled.ResetInteract = true;
Character.Controlled.DisableInteract = true;
if (openHealthWindow != null)
{
if (value.Character.Info == null || value.Character == Character.Controlled || Character.Controlled.HasEquippedItem("healthscanner".ToIdentifier()))
@@ -331,7 +337,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 +385,26 @@ 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")
{
Visible = false,
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 +622,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 +726,8 @@ namespace Barotrauma
distortTimer = 0.0f;
}
UpdateStatusHUD(deltaTime);
if (PlayerInput.KeyHit(InputType.Health) && GUI.KeyboardDispatcher.Subscriber == null &&
Character.Controlled.AllowInput && !toggledThisFrame)
{
@@ -726,9 +754,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 +928,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 +988,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 +1022,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 +1054,134 @@ 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<Affliction> statusIcons = new List<Affliction>();
private readonly Dictionary<AfflictionPrefab, float> statusIconVisibleTime = new Dictionary<AfflictionPrefab, float>();
private const 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<GUIImage>();
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);
}
afflictionIconContainer.Visible = true;
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;
@@ -1126,6 +1199,7 @@ namespace Barotrauma
}
else
{
afflictionIconContainer.Visible = hiddenAfflictionIconContainer.Visible = false;
if (Vitality > 0.0f)
{
float currHealth = healthWindowHealthBar.BarSize;
@@ -1150,18 +1224,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 +1248,7 @@ namespace Barotrauma
{
if (selectedLimb == null)
{
afflictionIconContainer.Content.ClearChildren();
afflictionIconList.Content.ClearChildren();
return;
}
@@ -1207,7 +1283,7 @@ namespace Barotrauma
private void CreateAfflictionInfos(IEnumerable<Affliction> afflictions)
{
afflictionIconContainer.ClearChildren();
afflictionIconList.ClearChildren();
displayedAfflictions.Clear();
Affliction mostSevereAffliction = SortAfflictionsBySeverity(afflictions, excludeBuffs: false).FirstOrDefault();
@@ -1217,7 +1293,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 +1351,7 @@ namespace Barotrauma
}
buttonToSelect?.OnClicked(buttonToSelect, buttonToSelect.UserData);
afflictionIconContainer.RecalculateChildren();
afflictionIconList.RecalculateChildren();
}
private void CreateRecommendedTreatments()
@@ -1386,7 +1462,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 +1513,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
};
@@ -1449,8 +1528,7 @@ namespace Barotrauma
Point nameDims = new Point(afflictionName.Rect.Width, (int)(GUIStyle.LargeFont.Size * 1.5f));
afflictionStrength.Text = strengthTexts[
MathHelper.Clamp((int)Math.Floor((affliction.Strength / affliction.Prefab.MaxStrength) * strengthTexts.Length), 0, strengthTexts.Length - 1)];
afflictionStrength.Text = affliction.GetStrengthText();
Vector2 strengthDims = GUIStyle.SubHeadingFont.MeasureString(afflictionStrength.Text);
@@ -1482,10 +1560,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<GUIButton>();
if (btn != null)
if (child is GUIButton btn)
{
btn.Selected = btn == button && !selected;
}
@@ -1516,7 +1593,7 @@ namespace Barotrauma
afflictionEffectColor = GUIStyle.Green;
}
var child = afflictionIconContainer.Content.FindChild(affliction);
var child = afflictionIconList.Content.FindChild(affliction);
var afflictionStrengthPredictionBar = child.GetChild<GUILayoutGroup>().GetChildByUserData("afflictionstrengthprediction") as GUIProgressBar;
afflictionStrengthPredictionBar.BarSize = 0.0f;
@@ -1542,6 +1619,11 @@ namespace Barotrauma
}
}
if (!affliction.Prefab.ShowBarInHealthMenu)
{
afflictionStrengthBar.BarSize = 1f;
}
if (afflictionTooltip != null && afflictionTooltip.UserData == affliction)
{
UpdateAfflictionInfo(afflictionTooltip.Content, affliction);
@@ -1581,8 +1663,7 @@ namespace Barotrauma
var strengthText = labelContainer.GetChildByUserData("strength") as GUITextBlock;
strengthText.Text = strengthTexts[
MathHelper.Clamp((int)Math.Floor((affliction.Strength / affliction.Prefab.MaxStrength) * strengthTexts.Length), 0, strengthTexts.Length - 1)];
strengthText.Text = affliction.GetStrengthText();
strengthText.TextColor = Color.Lerp(GUIStyle.Orange, GUIStyle.Red,
affliction.Strength / affliction.Prefab.MaxStrength);
@@ -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);
}

View File

@@ -51,11 +51,10 @@ namespace Barotrauma
&& p.InstallTime.TryUnwrap(out var installTime)
&& item.LatestUpdateTime <= installTime))
.ToArray();
if (needInstalling.Any())
{
await Task.WhenAll(
needInstalling.Select(SteamManager.Workshop.DownloadModThenEnqueueInstall));
}
if (!needInstalling.Any()) { return Enumerable.Empty<Steamworks.Ugc.Item>(); }
await Task.WhenAll(
needInstalling.Select(SteamManager.Workshop.DownloadModThenEnqueueInstall));
return needInstalling;
}

View File

@@ -30,7 +30,7 @@ namespace Barotrauma
public void ClientExecute(string[] args)
{
bool allowCheats = GameMain.NetworkMember == null && (GameMain.GameSession?.GameMode is TestGameMode || Screen.Selected is EditorScreen);
bool allowCheats = GameMain.NetworkMember == null && (GameMain.GameSession?.GameMode is TestGameMode || Screen.Selected is { IsEditor: true });
if (!allowCheats && !CheatsEnabled && IsCheat)
{
NewMessage("You need to enable cheats using the command \"enablecheats\" before you can use the command \"" + names[0] + "\".", Color.Red);
@@ -128,21 +128,17 @@ namespace Barotrauma
public static void Update(float deltaTime)
{
lock (queuedMessages)
while (queuedMessages.TryDequeue(out var newMsg))
{
while (queuedMessages.Count > 0)
{
var newMsg = queuedMessages.Dequeue();
AddMessage(newMsg);
AddMessage(newMsg);
if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging)
if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging)
{
unsavedMessages.Add(newMsg);
if (unsavedMessages.Count >= messagesPerFile)
{
unsavedMessages.Add(newMsg);
if (unsavedMessages.Count >= messagesPerFile)
{
SaveLogs();
unsavedMessages.Clear();
}
SaveLogs();
unsavedMessages.Clear();
}
}
}
@@ -178,9 +174,9 @@ namespace Barotrauma
Character.DisableControls = true;
if (PlayerInput.KeyHit(Keys.Tab))
if (PlayerInput.KeyHit(Keys.Tab) && !textBox.IsIMEActive)
{
textBox.Text = AutoComplete(textBox.Text, increment: string.IsNullOrEmpty(currentAutoCompletedCommand) ? 0 : 1 );
textBox.Text = AutoComplete(textBox.Text, increment: string.IsNullOrEmpty(currentAutoCompletedCommand) ? 0 : 1 );
}
if (PlayerInput.KeyDown(Keys.LeftControl) || PlayerInput.KeyDown(Keys.RightControl))
@@ -260,25 +256,21 @@ namespace Barotrauma
public static void DequeueMessages()
{
lock (queuedMessages)
while (queuedMessages.TryDequeue(out var newMsg))
{
while (queuedMessages.Count > 0)
if (listBox == null)
{
var newMsg = queuedMessages.Dequeue();
if (listBox == null)
{
//don't attempt to add to the listbox if it hasn't been created yet
Messages.Add(newMsg);
}
else
{
AddMessage(newMsg);
}
//don't attempt to add to the listbox if it hasn't been created yet
Messages.Add(newMsg);
}
else
{
AddMessage(newMsg);
}
if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging)
{
unsavedMessages.Add(newMsg);
}
if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging)
{
unsavedMessages.Add(newMsg);
}
}
}
@@ -1138,6 +1130,17 @@ namespace Barotrauma
});
AssignRelayToServer("debugdraw", false);
AssignOnExecute("debugdrawlocalization", (string[] args) =>
{
if (args.None() || !bool.TryParse(args[0], out bool state))
{
state = !TextManager.DebugDraw;
}
TextManager.DebugDraw = state;
NewMessage("Localization debug draw mode " + (TextManager.DebugDraw ? "enabled" : "disabled"), Color.White);
});
AssignRelayToServer("debugdraw", false);
AssignOnExecute("togglevoicechatfilters", (string[] args) =>
{
if (args.None() || !bool.TryParse(args[0], out bool state))
@@ -1697,6 +1700,8 @@ namespace Barotrauma
config.Language = language;
GameSettings.SetCurrentConfig(config);
}
HashSet<string> missingTexts = new HashSet<string>();
//key = text tag, value = list of languages the tag is missing from
Dictionary<Identifier, HashSet<LanguageIdentifier>> missingTags = new Dictionary<Identifier, HashSet<LanguageIdentifier>>();
@@ -1757,20 +1762,38 @@ namespace Barotrauma
foreach (Type itemComponentType in typeof(ItemComponent).Assembly.GetTypes().Where(type => type.IsSubclassOf(typeof(ItemComponent))))
{
foreach (var property in itemComponentType.GetProperties())
checkSerializableEntityType(itemComponentType);
}
checkSerializableEntityType(typeof(Item));
checkSerializableEntityType(typeof(Hull));
checkSerializableEntityType(typeof(Structure));
void checkSerializableEntityType(Type t)
{
foreach (var property in t.GetProperties())
{
if (!property.IsDefined(typeof(InGameEditable), false)) { continue; }
if (!property.IsDefined(typeof(Editable), false)) { continue; }
string propertyTag = $"{property.DeclaringType.Name}.{property.Name}";
addIfMissingAll(language,
if (addIfMissingAll(language,
propertyTag.ToIdentifier(),
property.Name.ToIdentifier(),
$"sp.{propertyTag}.name".ToIdentifier());
$"sp.{property.Name}.name".ToIdentifier(),
$"sp.{propertyTag}.name".ToIdentifier()) && language == "English".ToLanguageIdentifier())
{
missingTexts.Add($"<sp.{propertyTag.ToLower()}.name>{property.Name.FormatCamelCaseWithSpaces()}</sp.{propertyTag.ToLower()}.name>");
}
addIfMissingAll(language,
var description = (property.GetCustomAttributes(true).First(a => a is Serialize) as Serialize).Description;
if (addIfMissingAll(language,
$"sp.{propertyTag}.description".ToIdentifier(),
$"{property.Name.ToIdentifier()}.description".ToIdentifier());
$"sp.{property.Name}.description".ToIdentifier(),
$"{property.Name.ToIdentifier()}.description".ToIdentifier()) && language == "English".ToLanguageIdentifier())
{
missingTexts.Add($"<sp.{propertyTag.ToLower()}.description>{description}</sp.{propertyTag.ToLower()}.description>");
}
}
}
@@ -1794,7 +1817,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)
@@ -1891,6 +1925,23 @@ namespace Barotrauma
ToolBox.OpenFileWithShell(Path.GetFullPath(filePath));
SwapLanguage(TextManager.DefaultLanguage);
if (missingTexts.Any())
{
ShowQuestionPrompt("Dump the property names and descriptions missing from English to a new xml file? Y/N",
(option) =>
{
if (option.ToLowerInvariant() == "y")
{
string path = "newtexts.txt";
Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true;
File.WriteAllLines(path, missingTexts);
Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false;
ToolBox.OpenFileWithShell(Path.GetFullPath(path));
SwapLanguage(TextManager.DefaultLanguage);
}
});
}
void addIfMissing(Identifier tag, LanguageIdentifier language)
{
if (!tags[language].Contains(tag))
@@ -1899,15 +1950,90 @@ namespace Barotrauma
missingTags[tag].Add(language);
}
}
void addIfMissingAll(LanguageIdentifier language, params Identifier[] potentialTags)
bool addIfMissingAll(LanguageIdentifier language, params Identifier[] potentialTags)
{
if (!potentialTags.Any(t => tags[language].Contains(t)))
{
var tag = potentialTags.First();
if (!missingTags.ContainsKey(tag)) { missingTags[tag] = new HashSet<LanguageIdentifier>(); }
missingTags[tag].Add(language);
return true;
}
return false;
}
}));
commands.Add(new Command("checkduplicateloca", "", (string[] args) =>
{
if (args.Length < 1)
{
ThrowError("Please specify a file path.");
return;
}
XDocument doc1 = XMLExtensions.TryLoadXml(args[0]);
if (doc1?.Root == null)
{
ThrowError($"Could not load the file \"{args[0]}\"");
return;
}
List<(string tag, string text)> texts = new List<(string tag, string text)>();
bool duplicatesFound = false;
foreach (XElement element in doc1.Root.Elements())
{
string tag = element.Name.ToString();
string text = element.ElementInnerText();
if (texts.Any(t => t.tag == tag))
{
ThrowError($"Duplicate tag \"{tag}\".");
duplicatesFound = true;
}
}
if (duplicatesFound)
{
ThrowError($"Aborting, please fix duplicate tags in the file and try again.");
return;
}
foreach (XElement element in doc1.Root.Elements())
{
string tag = element.Name.ToString();
string text = element.ElementInnerText();
if (texts.Any(t => t.text == text))
{
if (tag.StartsWith("sp."))
{
string[] split = tag.Split('.');
if (split.Length > 3)
{
texts.RemoveAll(t => t.text == text);
string newTag = $"sp.{split[2]}.{split[3]}";
texts.Add((newTag, text));
NewMessage($"Duplicate text \"{tag}\", merging to \"{newTag}\".");
}
else
{
NewMessage($"Duplicate text \"{tag}\", using existing one \"{texts.Find(t => t.text == text).tag}\".");
}
}
else
{
texts.Add((tag, text));
ThrowError($"Duplicate text \"{tag}\". Could not determine if the text can be merged with an existing one, please check it manually.");
}
}
else
{
texts.Add((tag, text));
}
}
string filePath = "uniquetexts.xml";
Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true;
File.WriteAllLines(filePath, texts.Select(t => $"<{t.tag}>{t.text}</{t.tag}>"));
Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false;
ToolBox.OpenFileWithShell(Path.GetFullPath(filePath));
}));
commands.Add(new Command("comparelocafiles", "comparelocafiles [file1] [file2]", (string[] args) =>
@@ -2087,6 +2213,15 @@ namespace Barotrauma
}
}));
commands.Add(new Command("spawnallitems", "", (string[] args) =>
{
var cursorPos = Screen.Selected.Cam?.ScreenToWorld(PlayerInput.MousePosition) ?? Vector2.Zero;
foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs)
{
Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, cursorPos);
}
}));
commands.Add(new Command("camerasettings", "camerasettings [defaultzoom] [zoomsmoothness] [movesmoothness] [minzoom] [maxzoom]: debug command for testing camera settings. The values default to 1.1, 8.0, 8.0, 0.1 and 2.0.", (string[] args) =>
{
float defaultZoom = Screen.Selected.Cam.DefaultZoom;
@@ -2587,99 +2722,6 @@ namespace Barotrauma
}));
#endif
commands.Add(new Command("cleanbuild", "", (string[] args) =>
{
/*GameSettings.CurrentConfig.MusicVolume = 0.5f;
GameSettings.CurrentConfig.SoundVolume = 0.5f;
GameSettings.CurrentConfig.DynamicRangeCompressionEnabled = true;
GameSettings.CurrentConfig.VoipAttenuationEnabled = true;
NewMessage("Music and sound volume set to 0.5", Color.Green);
GameSettings.CurrentConfig.GraphicsWidth = 0;
GameSettings.CurrentConfig.GraphicsHeight = 0;
GameSettings.CurrentConfig.WindowMode = WindowMode.BorderlessWindowed;
NewMessage("Resolution set to 0 x 0 (screen resolution will be used)", Color.Green);
NewMessage("Fullscreen enabled", Color.Green);
GameSettings.CurrentConfig.VerboseLogging = false;
if (GameSettings.CurrentConfig.MasterServerUrl != "http://www.undertowgames.com/baromaster")
{
ThrowError("MasterServerUrl \"" + GameSettings.CurrentConfig.MasterServerUrl + "\"!");
}
GameSettings.SaveCurrentConfig();*/
throw new NotImplementedException();
#warning TODO: reimplement
var saveFiles = Barotrauma.IO.Directory.GetFiles(SaveUtil.SaveFolder);
foreach (string saveFile in saveFiles)
{
Barotrauma.IO.File.Delete(saveFile);
NewMessage("Deleted " + saveFile, Color.Green);
}
if (Barotrauma.IO.Directory.Exists(Barotrauma.IO.Path.Combine(SaveUtil.SaveFolder, "temp")))
{
Barotrauma.IO.Directory.Delete(Barotrauma.IO.Path.Combine(SaveUtil.SaveFolder, "temp"), true);
NewMessage("Deleted temp save folder", Color.Green);
}
if (Barotrauma.IO.Directory.Exists(ServerLog.SavePath))
{
var logFiles = Barotrauma.IO.Directory.GetFiles(ServerLog.SavePath);
foreach (string logFile in logFiles)
{
Barotrauma.IO.File.Delete(logFile);
NewMessage("Deleted " + logFile, Color.Green);
}
}
if (Barotrauma.IO.File.Exists("filelist.xml"))
{
Barotrauma.IO.File.Delete("filelist.xml");
NewMessage("Deleted filelist", Color.Green);
}
if (Barotrauma.IO.File.Exists("Data/bannedplayers.txt"))
{
Barotrauma.IO.File.Delete("Data/bannedplayers.txt");
NewMessage("Deleted bannedplayers.txt", Color.Green);
}
if (Barotrauma.IO.File.Exists("Submarines/TutorialSub.sub"))
{
Barotrauma.IO.File.Delete("Submarines/TutorialSub.sub");
NewMessage("Deleted TutorialSub from the submarine folder", Color.Green);
}
/*if (Barotrauma.IO.File.Exists(GameServer.SettingsFile))
{
Barotrauma.IO.File.Delete(GameServer.SettingsFile);
NewMessage("Deleted server settings", Color.Green);
}
if (Barotrauma.IO.File.Exists(GameServer.ClientPermissionsFile))
{
Barotrauma.IO.File.Delete(GameServer.ClientPermissionsFile);
NewMessage("Deleted client permission file", Color.Green);
}*/
if (Barotrauma.IO.File.Exists("crashreport.log"))
{
Barotrauma.IO.File.Delete("crashreport.log");
NewMessage("Deleted crashreport.log", Color.Green);
}
if (!Barotrauma.IO.File.Exists("Content/Map/TutorialSub.sub"))
{
ThrowError("TutorialSub.sub not found!");
}
}));
commands.Add(new Command("reloadcorepackage", "", (string[] args) =>
{
if (args.Length < 1)

View File

@@ -1,75 +1,22 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using System;
using System.Runtime.InteropServices;
using System.Text;
namespace EventInput
{
public class CharacterEventArgs : EventArgs
public readonly record struct CharacterEventArgs(char Character, long Param)
{
private readonly char character;
private readonly long lParam;
public CharacterEventArgs(char character, long lParam)
{
this.character = character;
this.lParam = lParam;
}
public char Character
{
get { return character; }
}
public long Param
{
get { return lParam; }
}
public long RepeatCount
{
get { return lParam & 0xffff; }
}
public bool ExtendedKey
{
get { return (lParam & (1 << 24)) > 0; }
}
public bool AltPressed
{
get { return (lParam & (1 << 29)) > 0; }
}
public bool PreviousState
{
get { return (lParam & (1 << 30)) > 0; }
}
public bool TransitionState
{
get { return (lParam & (1 << 31)) > 0; }
}
public long RepeatCount => Param & 0xffff;
public bool ExtendedKey => (Param & (1 << 24)) > 0;
public bool AltPressed => (Param & (1 << 29)) > 0;
public bool PreviousState => (Param & (1 << 30)) > 0;
public bool TransitionState => (Param & (1 << 31)) > 0;
}
public class KeyEventArgs : EventArgs
{
private Keys keyCode;
public KeyEventArgs(Keys keyCode)
{
this.keyCode = keyCode;
}
public Keys KeyCode
{
get { return keyCode; }
}
}
public readonly record struct KeyEventArgs(Keys KeyCode, char Character);
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 +35,12 @@ namespace EventInput
/// </summary>
public static event KeyEventHandler KeyUp;
/// <summary>
/// Raised when the user is editing text and IME is in progress.
/// </summary>
public static event EditingTextHandler EditingText;
static bool initialized;
/// <summary>
@@ -100,8 +53,10 @@ namespace EventInput
{
return;
}
window.TextInput += ReceiveInput;
window.KeyDown += ReceiveKeyDown;
window.TextEditing += ReceiveTextEditing;
initialized = true;
}
@@ -109,7 +64,16 @@ namespace EventInput
private static void ReceiveInput(object sender, TextInputEventArgs e)
{
OnCharEntered(e.Character);
KeyDown?.Invoke(sender, new KeyEventArgs(e.Key));
}
private static void ReceiveKeyDown(object sender, TextInputEventArgs e)
{
KeyDown?.Invoke(sender, new KeyEventArgs(e.Key, e.Character));
}
private static void ReceiveTextEditing(object sender, TextEditingEventArgs e)
{
EditingText?.Invoke(sender, e);
}
public static void OnCharEntered(char character)

View File

@@ -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;
@@ -14,6 +10,7 @@ namespace EventInput
void ReceiveTextInput(string text);
void ReceiveCommandInput(char command);
void ReceiveSpecialInput(Keys key);
void ReceiveEditingInput(string text, int start, int length);
bool Selected { get; set; } //or Focused
}
@@ -25,47 +22,27 @@ namespace EventInput
EventInput.Initialize(window);
EventInput.CharEntered += EventInput_CharEntered;
EventInput.KeyDown += EventInput_KeyDown;
EventInput.EditingText += EventInput_TextEditing;
GameMain.ResetIMEWorkaround();
}
public void EventInput_TextEditing(object sender, TextEditingEventArgs e)
{
_subscriber?.ReceiveEditingInput(e.Text, e.Start, e.Length);
}
public void EventInput_KeyDown(object sender, KeyEventArgs e)
{
if (_subscriber == null)
return;
_subscriber.ReceiveSpecialInput(e.KeyCode);
_subscriber?.ReceiveSpecialInput(e.KeyCode);
if (char.IsControl(e.Character))
{
_subscriber?.ReceiveCommandInput(e.Character);
}
}
void EventInput_CharEntered(object sender, CharacterEventArgs e)
{
if (_subscriber == null)
return;
if (char.IsControl(e.Character))
{
_subscriber.ReceiveCommandInput(e.Character);
// Doesn't work as expected. Not sure why this should be run in a separate thread.
//#if WINDOWS
// //ctrl-v
// if (e.Character == 0x16) // 22
// {
// //XNA runs in Multiple Thread Apartment state, which cannot recieve clipboard
// Thread thread = new Thread(PasteThread);
// thread.SetApartmentState(ApartmentState.STA);
// thread.Start();
// thread.Join();
// _subscriber.ReceiveTextInput(_pasteResult);
// }
// else
// {
// _subscriber.ReceiveCommandInput(e.Character);
// }
//#else
// _subscriber.ReceiveCommandInput(e.Character);
//#endif
}
else
{
_subscriber.ReceiveTextInput(e.Character);
}
_subscriber?.ReceiveTextInput(e.Character);
}
IKeyboardSubscriber _subscriber;
@@ -74,12 +51,26 @@ 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.MouseRect);
TextInput.StartTextInput();
TextInput.SetTextInputRect(box.MouseRect);
}
_subscriber = value;
if (value != null)
{
value.Selected = true;
}
}
}
}

View File

@@ -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
};
}
}
}

View File

@@ -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)
{

View File

@@ -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)

View File

@@ -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;
}
}
}

View File

@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework;
using System;
using System.Linq;
namespace Barotrauma;
@@ -10,37 +11,66 @@ partial class UIHighlightAction : EventAction
partial void UpdateProjSpecific()
{
bool useCircularFlash = false;
GUIComponent component = null;
if (Id != ElementId.None)
{
component = GUI.GetAdditions().FirstOrDefault(c => Equals(Id, c.UserData));
FindAndFlashComponents(c => Equals(Id, c.UserData));
}
else if (!EntityIdentifier.IsEmpty)
{
component = GUI.GetAdditions().FirstOrDefault(c =>
FindAndFlashComponents(c =>
c.UserData is MapEntityPrefab mep && mep.Identifier == EntityIdentifier || c.UserData is MapEntity me && me.Prefab.Identifier == EntityIdentifier);
}
else if (!OrderIdentifier.IsEmpty)
{
useCircularFlash = true;
bool foundMinimapNode = false;
if (!OrderTargetTag.IsEmpty)
{
component =
GUI.GetAdditions().FirstOrDefault(c =>
c.UserData is CrewManager.MinimapNodeData nodeData && nodeData.Order is Order order &&
order.Identifier == OrderIdentifier && order.Option == OrderOption && order.TargetEntity is Item item && item.HasTag(OrderTargetTag));
foundMinimapNode = FindAndFlashComponents(c =>
c.UserData is CrewManager.MinimapNodeData nodeData && nodeData.Order is Order order &&
order.Identifier == OrderIdentifier && order.Option == OrderOption && order.TargetEntity is Item item && item.HasTag(OrderTargetTag));
}
if (!foundMinimapNode)
{
FindAndFlashComponents(c => c.UserData is Order order && order.Identifier == OrderIdentifier && order.Option == OrderOption,
c => c.UserData is Order order && order.Identifier == OrderIdentifier,
c => Equals(OrderCategory, c.UserData));
}
component ??=
GUI.GetAdditions().FirstOrDefault(c => c.UserData is Order order && order.Identifier == OrderIdentifier && order.Option == OrderOption) ??
GUI.GetAdditions().FirstOrDefault(c => c.UserData is Order order && order.Identifier == OrderIdentifier) ??
GUI.GetAdditions().FirstOrDefault(c => Equals(OrderCategory, c.UserData));
}
if (component != null && component.FlashTimer <= 0.0f)
bool FindAndFlashComponents(params Func<GUIComponent, bool>[] predicates)
{
component.Flash(highlightColor, useCircularFlash: useCircularFlash);
component.Bounce |= Bounce;
foreach (var predicate in predicates)
{
if (HighlightMultiple)
{
bool found = false;
foreach (var component in GUI.GetAdditions())
{
if (predicate(component))
{
Flash(component);
found = true;
}
};
return found;
}
else if (GUI.GetAdditions().FirstOrDefault(predicate) is GUIComponent component)
{
Flash(component);
return true;
}
}
return false;
}
void Flash(GUIComponent component)
{
if (component.FlashTimer <= 0.0f)
{
component.Flash(highlightColor, useCircularFlash: useCircularFlash);
component.Bounce |= Bounce;
}
}
}
}

View File

@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Xml.Linq;
using Barotrauma.Threading;
namespace Barotrauma
@@ -37,7 +38,7 @@ namespace Barotrauma
private set;
}
public bool IsCJK
public TextManager.SpeciallyHandledCharCategory SpeciallyHandledCharCategory
{
get;
private set;
@@ -84,17 +85,35 @@ namespace Barotrauma
}
}
public static TextManager.SpeciallyHandledCharCategory ExtractShccFromXElement(XElement element)
=> TextManager.SpeciallyHandledCharCategories
.Where(category => element.GetAttributeBool($"is{category}", category switch {
// CJK isn't supported by default
TextManager.SpeciallyHandledCharCategory.CJK => false,
// For backwards compatibility, we assume that Cyrillic is supported by default
TextManager.SpeciallyHandledCharCategory.Cyrillic => true,
_ => throw new Exception("unreachable")
}))
.Aggregate(TextManager.SpeciallyHandledCharCategory.None, (current, category) => current | category);
public ScalableFont(ContentXElement element, GraphicsDevice gd = null)
: this(
element.GetAttributeContentPath("file")?.Value,
(uint)element.GetAttributeInt("size", 14),
gd,
element.GetAttributeBool("dynamicloading", false),
element.GetAttributeBool("iscjk", false))
ExtractShccFromXElement(element))
{
}
public ScalableFont(string filename, uint size, GraphicsDevice gd = null, bool dynamicLoading = false, bool isCJK = false)
public ScalableFont(
string filename,
uint size,
GraphicsDevice gd = null,
bool dynamicLoading = false,
TextManager.SpeciallyHandledCharCategory speciallyHandledCharCategory = TextManager.SpeciallyHandledCharCategory.None)
{
lock (globalMutex)
{
@@ -120,7 +139,7 @@ namespace Barotrauma
this.textures = new List<Texture2D>();
this.texCoords = new Dictionary<uint, GlyphData>();
this.DynamicLoading = dynamicLoading;
this.IsCJK = isCJK;
this.SpeciallyHandledCharCategory = speciallyHandledCharCategory;
this.graphicsDevice = gd;
if (gd != null && !dynamicLoading)

View File

@@ -775,7 +775,7 @@ namespace Barotrauma
if (Character.Controlled != null && ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent radio))
{
radio.Channel = channel;
GameMain.Client?.CreateEntityEvent(radio.Item, new Item.ChangePropertyEventData(radio.SerializableProperties["channel".ToIdentifier()]));
GameMain.Client?.CreateEntityEvent(radio.Item, new Item.ChangePropertyEventData(radio.SerializableProperties["channel".ToIdentifier()], radio));
if (setText)
{

View File

@@ -357,6 +357,17 @@ namespace Barotrauma
string txt = directory;
if (txt.StartsWith(currentDirectory)) { txt = txt.Substring(currentDirectory.Length); }
if (!txt.EndsWith("/")) { txt += "/"; }
//get directory info
DirectoryInfo dirInfo = new DirectoryInfo(directory);
try
{
//this will throw an exception if the directory can't be opened
Directory.GetDirectories(directory);
}
catch (UnauthorizedAccessException)
{
continue;
}
var itemFrame = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), fileList.Content.RectTransform), txt)
{
UserData = ItemIsDirectory.Yes

View File

@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Diagnostics;
using Barotrauma.IO;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.CharacterEditor;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
@@ -50,6 +49,14 @@ namespace Barotrauma
static class GUI
{
// Controls where a line is drawn for given coords.
public enum OutlinePosition
{
Default = 0, // Thickness is inside of top left and outside of bottom right coord
Inside = 1, // Thickness is subtracted from the inside
Centered = 2, // Thickness is centered on given coords
Outside = 3, // Tickness is added to the outside
}
public static GUICanvas Canvas => GUICanvas.Instance;
public static CursorState MouseCursor = CursorState.Default;
@@ -248,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)
{
@@ -310,7 +327,7 @@ namespace Barotrauma
DrawString(spriteBatch, new Vector2(10, y),
"FPS: " + Math.Round(GameMain.PerformanceCounter.AverageFramesPerSecond),
Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
if (GameMain.GameSession != null && Timing.TotalTime > GameMain.GameSession.RoundStartTime + 1.0)
if (GameMain.GameSession != null && GameMain.GameSession.RoundDuration > 1.0)
{
y += yStep;
DrawString(spriteBatch, new Vector2(10, y),
@@ -1350,7 +1367,7 @@ namespace Barotrauma
}
}
#region Element drawing
#region Element drawing
private static readonly List<float> usedIndicatorAngles = new List<float>();
@@ -1605,6 +1622,54 @@ namespace Barotrauma
}
}
public static void DrawRectangle(SpriteBatch sb, Vector2 position, Vector2 size, Vector2 origin, float rotation, Color clr, float depth = 0.0f, float thickness = 1, OutlinePosition outlinePos = OutlinePosition.Centered)
{
Vector2 topLeft = new Vector2(-origin.X, -origin.Y);
Vector2 topRight = new Vector2(-origin.X + size.X, -origin.Y);
Vector2 bottomLeft = new Vector2(-origin.X, -origin.Y + size.Y);
Vector2 actualSize = size;
switch(outlinePos)
{
case OutlinePosition.Default:
actualSize += new Vector2(thickness);
break;
case OutlinePosition.Centered:
topLeft -= new Vector2(thickness * 0.5f);
topRight -= new Vector2(thickness * 0.5f);
bottomLeft -= new Vector2(thickness * 0.5f);
actualSize += new Vector2(thickness);
break;
case OutlinePosition.Inside:
topRight -= new Vector2(thickness, 0.0f);
bottomLeft -= new Vector2(0.0f, thickness);
break;
case OutlinePosition.Outside:
topLeft -= new Vector2(thickness);
topRight -= new Vector2(0.0f, thickness);
bottomLeft -= new Vector2(thickness, 0.0f);
actualSize += new Vector2(thickness * 2.0f);
break;
}
Matrix rotate = Matrix.CreateRotationZ(rotation);
topLeft = Vector2.Transform(topLeft, rotate) + position;
topRight = Vector2.Transform(topRight, rotate) + position;
bottomLeft = Vector2.Transform(bottomLeft, rotate) + position;
Rectangle srcRect = new Rectangle(0, 0, 1, 1);
sb.Draw(solidWhiteTexture, topLeft, srcRect, clr, rotation, Vector2.Zero, new Vector2(thickness, actualSize.Y), SpriteEffects.None, depth);
sb.Draw(solidWhiteTexture, topLeft, srcRect, clr, rotation, Vector2.Zero, new Vector2(actualSize.X, thickness), SpriteEffects.None, depth);
sb.Draw(solidWhiteTexture, topRight, srcRect, clr, rotation, Vector2.Zero, new Vector2(thickness, actualSize.Y), SpriteEffects.None, depth);
sb.Draw(solidWhiteTexture, bottomLeft, srcRect, clr, rotation, Vector2.Zero, new Vector2(actualSize.X, thickness), SpriteEffects.None, depth);
}
public static void DrawFilledRectangle(SpriteBatch sb, Vector2 position, Vector2 size, Vector2 pivot, float rotation, Color clr, float depth = 0.0f)
{
Rectangle srcRect = new Rectangle(0, 0, 1, 1);
sb.Draw(solidWhiteTexture, position, srcRect, clr, rotation, (pivot/size), size, SpriteEffects.None, depth);
}
public static void DrawFilledRectangle(SpriteBatch sb, RectangleF rect, Color clr, float depth = 0.0f)
{
DrawFilledRectangle(sb, rect.Location, rect.Size, clr, depth);
@@ -1788,9 +1853,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)
{
@@ -2162,9 +2227,9 @@ namespace Barotrauma
return msgBox;
}
#endregion
#endregion
#region Element positioning
#region Element positioning
private static List<T> CreateElements<T>(int count, RectTransform parent, Func<RectTransform, T> constructor,
Vector2? relativeSize = null, Point? absoluteSize = null,
Anchor anchor = Anchor.TopLeft, Pivot? pivot = null, Point? minSize = null, Point? maxSize = null,
@@ -2363,9 +2428,9 @@ namespace Barotrauma
}
}
#endregion
#endregion
#region Misc
#region Misc
public static void TogglePauseMenu()
{
if (Screen.Selected == GameMain.MainMenuScreen) { return; }
@@ -2603,6 +2668,6 @@ namespace Barotrauma
if (!isSavingIndicatorEnabled) { return; }
timeUntilSavingIndicatorDisabled = delay;
}
#endregion
#endregion
}
}

View File

@@ -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);
@@ -757,9 +758,9 @@ namespace Barotrauma
toolTipBlock.DrawManually(spriteBatch);
}
public static void DrawToolTip(SpriteBatch spriteBatch, RichString toolTip, Rectangle targetElement)
public static void DrawToolTip(SpriteBatch spriteBatch, RichString toolTip, Rectangle targetElement, Anchor anchor = Anchor.BottomCenter, Pivot pivot = Pivot.TopLeft)
{
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);
@@ -774,7 +775,10 @@ namespace Barotrauma
toolTipBlock.UserData = toolTip;
}
toolTipBlock.RectTransform.AbsoluteOffset = new Point(targetElement.Center.X, targetElement.Bottom);
toolTipBlock.RectTransform.AbsoluteOffset =
RectTransform.CalculateAnchorPoint(anchor, targetElement) +
RectTransform.CalculatePivotOffset(pivot, toolTipBlock.RectTransform.NonScaledSize);
if (toolTipBlock.Rect.Right > GameMain.GraphicsWidth - 10)
{
toolTipBlock.RectTransform.AbsoluteOffset -= new Point(toolTipBlock.Rect.Width, 0);

View File

@@ -111,6 +111,7 @@ namespace Barotrauma
}
public void ReceiveTextInput(string text) { }
public void ReceiveCommandInput(char command) { }
public void ReceiveEditingInput(string text, int start, int length) { }
public void ReceiveSpecialInput(Keys key)
{
@@ -121,9 +122,7 @@ namespace Barotrauma
listBox.ReceiveSpecialInput(key);
GUI.KeyboardDispatcher.Subscriber = this;
break;
case Keys.Enter:
case Keys.Space:
case Keys.Escape:
default:
GUI.KeyboardDispatcher.Subscriber = null;
break;
}

View File

@@ -150,7 +150,6 @@ namespace Barotrauma
}
}
// TODO: fix implicit hiding
public override bool Selected
{
get { return isSelected; }
@@ -1348,6 +1347,7 @@ namespace Barotrauma
}
public void ReceiveTextInput(string text) { }
public void ReceiveCommandInput(char command) { }
public void ReceiveEditingInput(string text, int start, int length) { }
public void ReceiveSpecialInput(Keys key)
{
@@ -1365,9 +1365,7 @@ namespace Barotrauma
case Keys.Right:
if (isHorizontal && AllowArrowKeyScroll) { SelectNext(playSelectSound: PlaySelectSound.Yes); }
break;
case Keys.Enter:
case Keys.Space:
case Keys.Escape:
default:
GUI.KeyboardDispatcher.Subscriber = null;
break;
}

View File

@@ -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 };

View File

@@ -179,6 +179,11 @@ namespace Barotrauma
private set;
}
/// <summary>
/// If enabled, the value wraps around to Max when you go below Min, and vice versa
/// </summary>
public bool WrapAround;
public float valueStep;
private float pressedTimer;
@@ -403,13 +408,19 @@ namespace Barotrauma
{
if (MinValueFloat != null)
{
floatValue = Math.Max(floatValue, MinValueFloat.Value);
MinusButton.Enabled = floatValue > MinValueFloat;
floatValue =
WrapAround && MinValueFloat.HasValue && floatValue < MinValueFloat.Value ?
MaxValueFloat.Value :
Math.Max(floatValue, MinValueFloat.Value);
MinusButton.Enabled = WrapAround || floatValue > MinValueFloat;
}
if (MaxValueFloat != null)
{
floatValue = Math.Min(floatValue, MaxValueFloat.Value);
PlusButton.Enabled = floatValue < MaxValueFloat;
floatValue =
WrapAround && MaxValueFloat.HasValue && floatValue > MaxValueFloat.Value ?
MinValueFloat.Value :
Math.Min(floatValue, MaxValueFloat.Value);
PlusButton.Enabled = WrapAround || floatValue < MaxValueFloat;
}
}
@@ -417,16 +428,16 @@ namespace Barotrauma
{
if (MinValueInt != null && intValue < MinValueInt.Value)
{
intValue = Math.Max(intValue, MinValueInt.Value);
intValue = WrapAround && MaxValueInt.HasValue ? MaxValueInt.Value : Math.Max(intValue, MinValueInt.Value);
UpdateText();
}
if (MaxValueInt != null && intValue > MaxValueInt.Value)
{
intValue = Math.Min(intValue, MaxValueInt.Value);
intValue = WrapAround && MinValueInt.HasValue ? MinValueInt.Value : Math.Min(intValue, MaxValueInt.Value);
UpdateText();
}
PlusButton.Enabled = MaxValueInt == null || intValue < MaxValueInt;
MinusButton.Enabled = MinValueInt == null || intValue > MinValueInt;
PlusButton.Enabled = WrapAround || MaxValueInt == null || intValue < MaxValueInt;
MinusButton.Enabled = WrapAround || MinValueInt == null || intValue > MinValueInt;
}
private void UpdateText()

View File

@@ -8,6 +8,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -45,16 +46,17 @@ namespace Barotrauma
}
}
private ScalableFont cjkFont;
private ImmutableDictionary<TextManager.SpeciallyHandledCharCategory, ScalableFont> specialHandlingFonts;
public ScalableFont CjkFont
public ScalableFont GetFontForCategory(TextManager.SpeciallyHandledCharCategory category)
{
get
{
if (Language != GameSettings.CurrentConfig.Language) { LoadFont(); }
if (font.IsCJK) { return font; }
return cjkFont;
}
if (Language != GameSettings.CurrentConfig.Language) { LoadFont(); }
if (font.SpeciallyHandledCharCategory.HasFlag(category)) { return font; }
return specialHandlingFonts.TryGetValue(category, out var resultFont)
? resultFont
: specialHandlingFonts.TryGetValue(TextManager.SpeciallyHandledCharCategory.CJK, out resultFont)
? resultFont
: font;
}
public LanguageIdentifier Language { get; private set; }
@@ -70,40 +72,68 @@ namespace Barotrauma
string fontPath = GetFontFilePath(element);
uint size = GetFontSize(element);
bool dynamicLoading = GetFontDynamicLoading(element);
bool isCJK = GetIsCJK(element);
var shcc = GetShcc(element);
font?.Dispose();
cjkFont?.Dispose();
font = new ScalableFont(fontPath, size, GameMain.Instance.GraphicsDevice, dynamicLoading, isCJK)
specialHandlingFonts?.Values.ForEach(f => f.Dispose());
font = new ScalableFont(
fontPath,
size,
GameMain.Instance.GraphicsDevice,
dynamicLoading,
shcc)
{
ForceUpperCase = element.GetAttributeBool("forceuppercase", false)
};
if (!isCJK)
var fallbackFonts = new Dictionary<TextManager.SpeciallyHandledCharCategory, ScalableFont>();
foreach (var flag in TextManager.SpeciallyHandledCharCategories)
{
cjkFont = ExtractCjkFont(element)
?? new ScalableFont("Content/Fonts/NotoSans/NotoSansCJKsc-Bold.otf",
font.Size, GameMain.Instance.GraphicsDevice, dynamicLoading: true, isCJK: true);
cjkFont.ForceUpperCase = font.ForceUpperCase;
if (shcc.HasFlag(flag)) { continue; }
var extractedFont = ExtractFont(flag, element);
if (extractedFont is null) { continue; }
fallbackFonts.Add(flag, extractedFont);
}
fallbackFonts.Values.ForEach(ff => ff.ForceUpperCase = font.ForceUpperCase);
specialHandlingFonts = fallbackFonts.ToImmutableDictionary();
Language = GameSettings.CurrentConfig.Language;
}
public override void Dispose()
{
font?.Dispose(); font = null;
cjkFont?.Dispose(); cjkFont = null;
font?.Dispose();
font = null;
specialHandlingFonts?.Values.ForEach(f => f.Dispose());
specialHandlingFonts = null;
}
private ScalableFont ExtractCjkFont(ContentXElement element)
private ScalableFont ExtractFont(TextManager.SpeciallyHandledCharCategory flag, ContentXElement element)
{
foreach (var subElement in element.Elements().Reverse())
{
if (subElement.NameAsIdentifier() != "override") { continue; }
if (subElement.GetAttributeBool("iscjk", false))
if (ScalableFont.ExtractShccFromXElement(subElement).HasFlag(flag))
{
return new ScalableFont(subElement, GameMain.Instance.GraphicsDevice);
}
}
return null;
ScalableFont hardcodedFallback(string path)
=> new ScalableFont(
path,
font.Size,
GameMain.Instance.GraphicsDevice,
dynamicLoading: true,
speciallyHandledCharCategory: flag);
return flag switch
{
TextManager.SpeciallyHandledCharCategory.CJK
=> hardcodedFallback("Content/Fonts/NotoSans/NotoSansCJKsc-Bold.otf"),
TextManager.SpeciallyHandledCharCategory.Cyrillic
=> hardcodedFallback("Content/Fonts/Oswald-Bold.ttf"),
_ => null
};
}
private string GetFontFilePath(ContentXElement element)
@@ -154,21 +184,21 @@ namespace Barotrauma
return element.GetAttributeBool("dynamicloading", false);
}
private bool GetIsCJK(XElement element)
private TextManager.SpeciallyHandledCharCategory GetShcc(XElement element)
{
foreach (var subElement in element.Elements())
{
if (IsValidOverride(subElement))
{
return subElement.GetAttributeBool("iscjk", false);
return ScalableFont.ExtractShccFromXElement(subElement);
}
}
return element.GetAttributeBool("iscjk", false);
return ScalableFont.ExtractShccFromXElement(element);
}
private bool IsValidOverride(XElement element)
{
if (!element.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { return false; }
if (!element.IsOverride()) { return false; }
var languages = element.GetAttributeIdentifierArray("language", Array.Empty<Identifier>());
return languages.Any(l => l.ToLanguageIdentifier() == GameSettings.CurrentConfig.Language);
}
@@ -191,26 +221,26 @@ namespace Barotrauma
private ScalableFont GetFontForStr(LocalizedString str) => GetFontForStr(str.Value);
public ScalableFont GetFontForStr(string str) =>
TextManager.IsCJK(str) ? Prefabs.ActivePrefab.CjkFont : Prefabs.ActivePrefab.Font;
Prefabs.ActivePrefab.GetFontForCategory(TextManager.GetSpeciallyHandledCategories(str));
public void DrawString(SpriteBatch sb, LocalizedString text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth)
public void DrawString(SpriteBatch sb, LocalizedString text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects spriteEffects, float layerDepth)
{
DrawString(sb, text.Value, position, color, rotation, origin, scale, se, layerDepth);
DrawString(sb, text.Value, position, color, rotation, origin, scale, spriteEffects, layerDepth);
}
public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth)
public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects spriteEffects, float layerDepth)
{
GetFontForStr(text).DrawString(sb, text, position, color, rotation, origin, scale, se, layerDepth);
GetFontForStr(text).DrawString(sb, text, position, color, rotation, origin, scale, spriteEffects, layerDepth);
}
public void DrawString(SpriteBatch sb, LocalizedString text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects se, float layerDepth, Alignment alignment = Alignment.TopLeft)
public void DrawString(SpriteBatch sb, LocalizedString text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects spriteEffects, float layerDepth, Alignment alignment = Alignment.TopLeft)
{
DrawString(sb, text.Value, position, color, rotation, origin, scale, se, layerDepth, alignment);
DrawString(sb, text.Value, position, color, rotation, origin, scale, spriteEffects, layerDepth, alignment);
}
public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects se, float layerDepth, Alignment alignment = Alignment.TopLeft, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit)
public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects spriteEffects, float layerDepth, Alignment alignment = Alignment.TopLeft, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit)
{
GetFontForStr(text).DrawString(sb, text, position, color, rotation, origin, scale, se, layerDepth, alignment, forceUpperCase);
GetFontForStr(text).DrawString(sb, text, position, color, rotation, origin, scale, spriteEffects, layerDepth, alignment, forceUpperCase);
}
public void DrawString(SpriteBatch sb, LocalizedString text, Vector2 position, Color color, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit, bool italics = false)
@@ -223,9 +253,9 @@ namespace Barotrauma
GetFontForStr(text).DrawString(sb, text, position, color, forceUpperCase, italics);
}
public void DrawStringWithColors(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects se, float layerDepth, in ImmutableArray<RichTextData>? richTextData, int rtdOffset = 0, Alignment alignment = Alignment.TopLeft, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit)
public void DrawStringWithColors(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects spriteEffects, float layerDepth, in ImmutableArray<RichTextData>? richTextData, int rtdOffset = 0, Alignment alignment = Alignment.TopLeft, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit)
{
GetFontForStr(text).DrawStringWithColors(sb, text, position, color, rotation, origin, scale, se, layerDepth, richTextData, rtdOffset, alignment, forceUpperCase);
GetFontForStr(text).DrawStringWithColors(sb, text, position, color, rotation, origin, scale, spriteEffects, layerDepth, richTextData, rtdOffset, alignment, forceUpperCase);
}
public Vector2 MeasureString(LocalizedString str, bool removeExtraSpacing = false)

View File

@@ -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.
/// </summary>
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,8 +586,14 @@ namespace Barotrauma
if (!HasColorHighlight)
{
string textToShow = Censor ? censoredText : (Wrap ? wrappedText.Value : text.SanitizedValue);
Color colorToShow = currentTextColor * (currentTextColor.A / 255.0f);
if (TextManager.DebugDraw)
{
if (!text.NestedStr.Loaded || text.NestedStr.Language == LanguageIdentifier.None)
{
colorToShow = Color.Magenta;
}
}
if (Shadow)
{
@@ -597,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,

View File

@@ -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;
@@ -77,12 +77,12 @@ namespace Barotrauma
private int selectionEndIndex;
private bool IsLeftToRight => selectionStartIndex <= selectionEndIndex;
private GUICustomComponent caretAndSelectionRenderer;
private readonly GUICustomComponent caretAndSelectionRenderer;
private bool mouseHeldInside;
private readonly Memento<string> memento = new Memento<string>();
// Skip one update cycle, fixes Enter key instantly deselecting the chatbox
private bool skipUpdate;
@@ -199,6 +199,7 @@ namespace Barotrauma
base.Font = value;
if (textBlock == null) { return; }
textBlock.Font = value;
imePreviewTextHandler.Font = Font;
}
}
@@ -263,6 +264,10 @@ namespace Barotrauma
public override bool PlaySoundOnSelect { get; set; } = true;
private readonly IMEPreviewTextHandler imePreviewTextHandler;
public bool IsIMEActive => imePreviewTextHandler is { HasText: true };
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)
@@ -274,6 +279,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;
@@ -305,18 +311,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;
};
}
@@ -391,14 +396,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;
@@ -579,6 +586,7 @@ namespace Barotrauma
{
CaretIndex = Math.Min(Text.Length, CaretIndex + input.Length);
OnTextChanged?.Invoke(this, Text);
imePreviewTextHandler?.Reset();
}
}
@@ -607,10 +615,12 @@ namespace Barotrauma
public void ReceiveCommandInput(char command)
{
if (Text == null) Text = "";
if (IsIMEActive) { return; }
if (Text == null) { Text = ""; }
// Prevent alt gr from triggering any of these as that combination is often needed for special characters
if (PlayerInput.IsAltDown()) return;
if (PlayerInput.IsAltDown()) { return; }
switch (command)
{
@@ -684,8 +694,21 @@ namespace Barotrauma
}
}
public void ReceiveEditingInput(string text, int start, int length)
{
if (string.IsNullOrEmpty(text))
{
imePreviewTextHandler.Reset();
return;
}
imePreviewTextHandler.UpdateText(text, start, length);
}
public void ReceiveSpecialInput(Keys key)
{
if (IsIMEActive) { return; }
switch (key)
{
case Keys.Left:
@@ -874,6 +897,11 @@ namespace Barotrauma
}
}
public void DrawIMEPreview(SpriteBatch spriteBatch)
{
imePreviewTextHandler.DrawIMEPreview(spriteBatch, CaretScreenPos, textBlock);
}
private void CalculateSelection()
{
string textDrawn = Censor ? textBlock.CensoredText : WrappedText;

View File

@@ -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);

View File

@@ -0,0 +1,90 @@
#nullable enable
using System.Collections.Immutable;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace Barotrauma
{
public sealed class IMEPreviewTextHandler
{
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; }
private string previewText = string.Empty;
private Vector2 textSize;
private bool isSectioned;
private ImmutableArray<RichTextData>? richTextData;
public IMEPreviewTextHandler(GUIFont font)
{
Font = font;
}
public void Reset()
{
textSize = Vector2.Zero;
previewText = string.Empty;
richTextData = null;
isSectioned = false;
}
public void UpdateText(string text, int start, int length)
{
isSectioned = start >= 0 && length > 0;
richTextData = null;
if (string.IsNullOrEmpty(text))
{
Reset();
return;
}
previewText = text;
textSize = Font.MeasureString(text);
if (!isSectioned) { return; }
string coloredText = ToolBox.ColorSectionOfString(text, start, length, GUIStyle.Orange);
RichString richString = RichString.Rich(coloredText);
previewText = richString.SanitizedValue;
richTextData = richString.RichTextData;
}
public void DrawIMEPreview(SpriteBatch spriteBatch, Vector2 position, GUITextBlock textBlock)
{
if (!HasText) { return; }
int inflate = GUI.IntScale(3);
RectangleF rect = new RectangleF(position, 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.DrawStringWithColors(spriteBatch,
text: previewText,
position: position,
color: isSectioned ? GUIStyle.TextColorNormal : GUIStyle.Orange,
rotation: 0.0f,
origin: Vector2.Zero,
scale: 1f,
spriteEffects: SpriteEffects.None,
layerDepth: 0,
richTextData: richTextData,
alignment: textBlock.TextAlignment,
forceUpperCase: textBlock.ForceUpperCase);
}
}
}

View File

@@ -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<CrewElement> HealElements;
public CrewHealList(GUIListBox healList, GUIComponent panel)
public CrewHealList(GUIListBox healList, GUIComponent panel, GUIComponent treatAllButton)
{
Panel = panel;
HealList = healList;
TreatAllButton = treatAllButton;
HealElements = new List<CrewElement>();
}
}
@@ -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<MedicalClinic.NetAffliction>());
CreatePendingHealElement(healList.HealList.Content, crewMember, healList, ImmutableArray<MedicalClinic.NetAffliction>.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<CharacterInfo> crew = MedicalClinic.GetCrewCharacters();
@@ -334,12 +336,21 @@ namespace Barotrauma
healList.HealList.Content.RemoveChild(element.UIElement);
}
IEnumerable<CrewElement> orderedList = healList.HealElements.OrderBy(element => element.Target.Character?.HealthPercentage ?? 100);
IEnumerable<CrewElement> 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<AfflictionPrefab, float> afflictionAndStrength = new Dictionary<AfflictionPrefab, float>();
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<SortMode>())
// {
// 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.NetAffliction>() };
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<MedicalClinic.NetAffliction> 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<MedicalClinic.NetAffliction> afflictions = request.Afflictions.Where(a => !medicalClinic.IsAfflictionPending(crewMember, a)).ToImmutableArray();
if (!afflictions.Any()) { return true; }
@@ -845,9 +858,9 @@ namespace Barotrauma
GUITextBlock prefabBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), topTextLayout.RectTransform), prefab.Name, font: GUIStyle.SubHeadingFont);
Color textColor = Color.Lerp(GUIStyle.Orange, GUIStyle.Red, (int)affliction.AfflictionSeverity / 2f);
Color textColor = Color.Lerp(GUIStyle.Orange, GUIStyle.Red, affliction.Strength / affliction.Prefab.MaxStrength);
LocalizedString vitalityText = TextManager.GetWithVariable("medicalclinic.vitalitydifference", "[amount]", (-affliction.Strength).ToString());
LocalizedString vitalityText = affliction.VitalityDecrease == 0 ? string.Empty : TextManager.GetWithVariable("medicalclinic.vitalitydifference", "[amount]", (-affliction.VitalityDecrease).ToString());
GUITextBlock vitalityBlock = new GUITextBlock(new RectTransform(new Vector2(0.25f, 1f), topTextLayout.RectTransform), vitalityText, textAlignment: Alignment.Center)
{
TextColor = textColor,
@@ -856,7 +869,7 @@ namespace Barotrauma
AutoScaleHorizontal = true
};
LocalizedString severityText = TextManager.Get($"AfflictionStrength{affliction.AfflictionSeverity}");
LocalizedString severityText = Affliction.GetStrengthText(affliction.Strength, affliction.Prefab.MaxStrength);
GUITextBlock severityBlock = new GUITextBlock(new RectTransform(new Vector2(0.25f, 1f), topTextLayout.RectTransform), severityText, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont)
{
TextColor = textColor,
@@ -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<MedicalClinic.NetAffliction>()
Afflictions = ImmutableArray<MedicalClinic.NetAffliction>.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 =>
{

View File

@@ -134,7 +134,7 @@ namespace Barotrauma
set => hadSellSubPermissions = value;
}
private bool HasPermissionToUseTab(StoreTab tab)
private static bool HasPermissionToUseTab(StoreTab tab)
{
return tab switch
{
@@ -278,6 +278,7 @@ namespace Barotrauma
RefreshBuying(updateOwned: false);
RefreshSelling(updateOwned: false);
RefreshSellingFromSub(updateOwned: false);
SetConfirmButtonBehavior();
needsRefresh = false;
}
@@ -475,6 +476,7 @@ namespace Barotrauma
};
List<MapEntityCategory> itemCategories = Enum.GetValues(typeof(MapEntityCategory)).Cast<MapEntityCategory>().ToList();
itemCategories.Remove(MapEntityCategory.None);
//don't show categories with no buyable items
itemCategories.RemoveAll(c => !ItemPrefab.Prefabs.Any(ep => ep.Category.HasFlag(c) && ep.CanBeBought));
itemCategoryButtons.Clear();
@@ -507,6 +509,7 @@ namespace Barotrauma
{
btn.RectTransform.SizeChanged += () =>
{
if (btn.Frame.sprites == null) { return; }
var sprite = btn.Frame.sprites[GUIComponent.ComponentState.None].First();
btn.RectTransform.NonScaledSize = new Point(btn.Rect.Width, (int)(btn.Rect.Width * ((float)sprite.Sprite.SourceRect.Height / sprite.Sprite.SourceRect.Width)));
};
@@ -617,9 +620,9 @@ namespace Barotrauma
Stretch = true
};
var shoppingCrateListContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.8f), shoppingCrateInventoryContainer.RectTransform), style: null);
shoppingCrateBuyList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false };
shoppingCrateSellList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false };
shoppingCrateSellFromSubList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false };
shoppingCrateBuyList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false, KeepSpaceForScrollBar = true };
shoppingCrateSellList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false, KeepSpaceForScrollBar = true };
shoppingCrateSellFromSubList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false, KeepSpaceForScrollBar = true };
var relevantBalanceContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), shoppingCrateInventoryContainer.RectTransform), isHorizontal: true)
{
@@ -745,7 +748,7 @@ namespace Barotrauma
} ?? Enumerable.Empty<PurchasedItem>();
foreach (var button in itemCategoryButtons)
{
if (!(button.UserData is MapEntityCategory category))
if (button.UserData is not MapEntityCategory category)
{
continue;
}
@@ -859,18 +862,17 @@ namespace Barotrauma
float prevBuyListScroll = storeBuyList.BarScroll;
float prevShoppingCrateScroll = shoppingCrateBuyList.BarScroll;
int dailySpecialCount = ActiveStore.DailySpecials.Count;
if ((storeDailySpecialsGroup != null) != ActiveStore.DailySpecials.Any() || dailySpecialCount != prevDailySpecialCount)
int dailySpecialCount = ActiveStore?.DailySpecials.Count(s => s.CanCharacterBuy()) ?? 0;
if ((ActiveStore == null && storeDailySpecialsGroup != null) || (storeDailySpecialsGroup != null) != ActiveStore.DailySpecials.Any() || dailySpecialCount != prevDailySpecialCount)
{
if (storeDailySpecialsGroup == null || dailySpecialCount != prevDailySpecialCount)
storeBuyList.RemoveChild(storeDailySpecialsGroup?.Parent);
if (ActiveStore != null && (storeDailySpecialsGroup == null || dailySpecialCount != prevDailySpecialCount))
{
storeBuyList.RemoveChild(storeDailySpecialsGroup?.Parent);
storeDailySpecialsGroup = CreateDealsGroup(storeBuyList, dailySpecialCount);
storeDailySpecialsGroup.Parent.SetAsFirstChild();
}
else
{
storeBuyList.RemoveChild(storeDailySpecialsGroup.Parent);
storeDailySpecialsGroup = null;
}
storeBuyList.RecalculateChildren();
@@ -879,20 +881,22 @@ namespace Barotrauma
bool hasPermissions = HasTabPermissions(StoreTab.Buy);
var existingItemFrames = new HashSet<GUIComponent>();
foreach (PurchasedItem item in ActiveStore.Stock)
if (ActiveStore != null)
{
CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity);
}
foreach (ItemPrefab itemPrefab in ActiveStore.DailySpecials)
{
if (ActiveStore.Stock.Any(pi => pi.ItemPrefab == itemPrefab)) { continue; }
CreateOrUpdateItemFrame(itemPrefab, 0);
foreach (PurchasedItem item in ActiveStore.Stock)
{
CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity);
}
foreach (ItemPrefab itemPrefab in ActiveStore.DailySpecials)
{
if (ActiveStore.Stock.Any(pi => pi.ItemPrefab == itemPrefab)) { continue; }
CreateOrUpdateItemFrame(itemPrefab, 0);
}
}
void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int quantity)
{
if (itemPrefab.CanBeBoughtFrom(ActiveStore, out PriceInfo priceInfo))
if (itemPrefab.CanBeBoughtFrom(ActiveStore, out PriceInfo priceInfo) && itemPrefab.CanCharacterBuy())
{
bool isDailySpecial = ActiveStore.DailySpecials.Contains(itemPrefab);
var itemFrame = isDailySpecial ?
@@ -945,11 +949,11 @@ namespace Barotrauma
float prevSellListScroll = storeSellList.BarScroll;
float prevShoppingCrateScroll = shoppingCrateSellList.BarScroll;
int requestedGoodsCount = ActiveStore.RequestedGoods.Count;
if ((storeRequestedGoodGroup != null) != ActiveStore.RequestedGoods.Any() || requestedGoodsCount != prevRequestedGoodsCount)
int requestedGoodsCount = ActiveStore?.RequestedGoods.Count ?? 0;
if ((ActiveStore == null && storeRequestedGoodGroup != null) || (storeRequestedGoodGroup != null) != ActiveStore.RequestedGoods.Any() || requestedGoodsCount != prevRequestedGoodsCount)
{
storeSellList.RemoveChild(storeRequestedGoodGroup?.Parent);
if (storeRequestedGoodGroup == null || requestedGoodsCount != prevRequestedGoodsCount)
if (ActiveStore != null && (storeRequestedGoodGroup == null || requestedGoodsCount != prevRequestedGoodsCount))
{
storeRequestedGoodGroup = CreateDealsGroup(storeSellList, requestedGoodsCount);
storeRequestedGoodGroup.Parent.SetAsFirstChild();
@@ -964,14 +968,17 @@ namespace Barotrauma
bool hasPermissions = HasTabPermissions(StoreTab.Sell);
var existingItemFrames = new HashSet<GUIComponent>();
foreach (PurchasedItem item in itemsToSell)
if (ActiveStore != null)
{
CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity);
}
foreach (var requestedGood in ActiveStore.RequestedGoods)
{
if (itemsToSell.Any(pi => pi.ItemPrefab == requestedGood)) { continue; }
CreateOrUpdateItemFrame(requestedGood, 0);
foreach (PurchasedItem item in itemsToSell)
{
CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity);
}
foreach (var requestedGood in ActiveStore.RequestedGoods)
{
if (itemsToSell.Any(pi => pi.ItemPrefab == requestedGood)) { continue; }
CreateOrUpdateItemFrame(requestedGood, 0);
}
}
void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int itemQuantity)
@@ -1029,11 +1036,11 @@ namespace Barotrauma
float prevSellListScroll = storeSellFromSubList.BarScroll;
float prevShoppingCrateScroll = shoppingCrateSellFromSubList.BarScroll;
int requestedGoodsCount = ActiveStore.RequestedGoods.Count;
if ((storeRequestedSubGoodGroup != null) != ActiveStore.RequestedGoods.Any() || requestedGoodsCount != prevSubRequestedGoodsCount)
int requestedGoodsCount = ActiveStore?.RequestedGoods.Count ?? 0;
if ((ActiveStore == null && storeRequestedSubGoodGroup != null) || (storeRequestedSubGoodGroup != null) != ActiveStore.RequestedGoods.Any() || requestedGoodsCount != prevSubRequestedGoodsCount)
{
storeSellFromSubList.RemoveChild(storeRequestedSubGoodGroup?.Parent);
if (storeRequestedSubGoodGroup == null || requestedGoodsCount != prevSubRequestedGoodsCount)
if (ActiveStore != null && (storeRequestedSubGoodGroup == null || requestedGoodsCount != prevSubRequestedGoodsCount))
{
storeRequestedSubGoodGroup = CreateDealsGroup(storeSellFromSubList, requestedGoodsCount);
storeRequestedSubGoodGroup.Parent.SetAsFirstChild();
@@ -1048,14 +1055,17 @@ namespace Barotrauma
bool hasPermissions = HasSellSubPermissions;
var existingItemFrames = new HashSet<GUIComponent>();
foreach (PurchasedItem item in itemsToSellFromSub)
if (ActiveStore != null)
{
CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity);
}
foreach (var requestedGood in ActiveStore.RequestedGoods)
{
if (itemsToSellFromSub.Any(pi => pi.ItemPrefab == requestedGood)) { continue; }
CreateOrUpdateItemFrame(requestedGood, 0);
foreach (PurchasedItem item in itemsToSellFromSub)
{
CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity);
}
foreach (var requestedGood in ActiveStore.RequestedGoods)
{
if (itemsToSellFromSub.Any(pi => pi.ItemPrefab == requestedGood)) { continue; }
CreateOrUpdateItemFrame(requestedGood, 0);
}
}
void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int itemQuantity)
@@ -1110,7 +1120,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)
{
@@ -1142,6 +1152,7 @@ namespace Barotrauma
public void RefreshItemsToSell()
{
itemsToSell.Clear();
if (ActiveStore == null) { return; }
var playerItems = CargoManager.GetSellableItems(Character.Controlled);
foreach (Item playerItem in playerItems)
{
@@ -1172,6 +1183,7 @@ namespace Barotrauma
public void RefreshItemsToSellFromSub()
{
itemsToSellFromSub.Clear();
if (ActiveStore == null) { return; }
var subItems = CargoManager.GetSellableItemsFromSub();
foreach (Item subItem in subItems)
{
@@ -1205,52 +1217,55 @@ namespace Barotrauma
bool hasPermissions = HasTabPermissions(tab);
HashSet<GUIComponent> existingItemFrames = new HashSet<GUIComponent>();
int totalPrice = 0;
foreach (PurchasedItem item in items)
if (ActiveStore != null)
{
if (!(item.ItemPrefab.GetPriceInfo(ActiveStore) is { } priceInfo)) { continue; }
GUINumberInput numInput = null;
if (!(listBox.Content.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab.Identifier == item.ItemPrefab.Identifier) is { } itemFrame))
foreach (PurchasedItem item in items)
{
itemFrame = CreateItemFrame(item, listBox, tab, forceDisable: !hasPermissions);
numInput = itemFrame.FindChild(c => c is GUINumberInput, recursive: true) as GUINumberInput;
}
else
{
itemFrame.UserData = item;
numInput = itemFrame.FindChild(c => c is GUINumberInput, recursive: true) as GUINumberInput;
if (!(item.ItemPrefab.GetPriceInfo(ActiveStore) is { } priceInfo)) { continue; }
GUINumberInput numInput = null;
if (!(listBox.Content.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab.Identifier == item.ItemPrefab.Identifier) is { } itemFrame))
{
itemFrame = CreateItemFrame(item, listBox, tab, forceDisable: !hasPermissions);
numInput = itemFrame.FindChild(c => c is GUINumberInput, recursive: true) as GUINumberInput;
}
else
{
itemFrame.UserData = item;
numInput = itemFrame.FindChild(c => c is GUINumberInput, recursive: true) as GUINumberInput;
if (numInput != null)
{
numInput.UserData = item;
numInput.Enabled = hasPermissions;
numInput.MaxValueInt = GetMaxAvailable(item.ItemPrefab, tab);
}
SetOwnedText(itemFrame);
SetItemFrameStatus(itemFrame, hasPermissions);
}
existingItemFrames.Add(itemFrame);
suppressBuySell = true;
if (numInput != null)
{
numInput.UserData = item;
numInput.Enabled = hasPermissions;
numInput.MaxValueInt = GetMaxAvailable(item.ItemPrefab, tab);
if (numInput.IntValue != item.Quantity) { itemFrame.Flash(GUIStyle.Green); }
numInput.IntValue = item.Quantity;
}
SetOwnedText(itemFrame);
SetItemFrameStatus(itemFrame, hasPermissions);
}
existingItemFrames.Add(itemFrame);
suppressBuySell = false;
suppressBuySell = true;
if (numInput != null)
{
if (numInput.IntValue != item.Quantity) { itemFrame.Flash(GUIStyle.Green); }
numInput.IntValue = item.Quantity;
}
suppressBuySell = false;
try
{
int price = tab switch
try
{
StoreTab.Buy => ActiveStore.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo),
StoreTab.Sell => ActiveStore.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo),
StoreTab.SellSub => ActiveStore.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo),
_ => throw new NotImplementedException()
};
totalPrice += item.Quantity * price;
}
catch (NotImplementedException e)
{
DebugConsole.LogError($"Error getting item price: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}");
int price = tab switch
{
StoreTab.Buy => ActiveStore.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo),
StoreTab.Sell => ActiveStore.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo),
StoreTab.SellSub => ActiveStore.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo),
_ => throw new NotImplementedException()
};
totalPrice += item.Quantity * price;
}
catch (NotImplementedException e)
{
DebugConsole.LogError($"Error getting item price: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}");
}
}
}
@@ -1287,7 +1302,7 @@ namespace Barotrauma
private void SortItems(GUIListBox list, SortingMethod sortingMethod)
{
if (CurrentLocation == null) { return; }
if (CurrentLocation == null || ActiveStore == null) { return; }
if (sortingMethod == SortingMethod.AlphabeticalAsc || sortingMethod == SortingMethod.AlphabeticalDesc)
{
@@ -1662,13 +1677,15 @@ namespace Barotrauma
{
OwnedItems.Clear();
if (ActiveStore == null) { return; }
// Add items on the sub(s)
if (Submarine.MainSub?.GetItems(true) is List<Item> subItems)
{
foreach (var subItem in subItems)
{
if (!subItem.Components.All(c => !(c is Holdable h) || !h.Attachable || !h.Attached)) { continue; }
if (!subItem.Components.All(c => !(c is Wire w) || w.Connections.All(c => c == null))) { continue; }
if (!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 +1718,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 +1746,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 +1858,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 +1872,7 @@ namespace Barotrauma
}
}
}
itemComponent.ToolTip = toolTip;
itemComponent.ToolTip = RichString.Rich(toolTip);
}
if (ownedLabel != null)
{
@@ -1995,11 +2008,23 @@ namespace Barotrauma
int totalPrice = 0;
foreach (var item in itemsToPurchase)
{
if (item?.ItemPrefab == null || !item.ItemPrefab.CanBeBoughtFrom(ActiveStore, out var priceInfo))
if (item is null) { continue; }
if (item.ItemPrefab == null || !item.ItemPrefab.CanBeBoughtFrom(ActiveStore, out var priceInfo))
{
itemsToRemove.Add(item);
continue;
}
if (item.ItemPrefab.DefaultPrice.RequiresUnlock)
{
if (!CargoManager.HasUnlockedStoreItem(item.ItemPrefab))
{
itemsToRemove.Add(item);
continue;
}
}
totalPrice += item.Quantity * ActiveStore.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo);
}
itemsToRemove.ForEach(i => itemsToPurchase.Remove(i));
@@ -2054,7 +2079,12 @@ namespace Barotrauma
private void SetShoppingCrateTotalText()
{
if (IsBuying)
if (ActiveStore == null)
{
shoppingCrateTotal.Text = TextManager.FormatCurrency(0);
shoppingCrateTotal.TextColor = Color.White;
}
else if (IsBuying)
{
shoppingCrateTotal.Text = TextManager.FormatCurrency(buyTotal);
shoppingCrateTotal.TextColor = Balance < buyTotal ? Color.Red : Color.White;
@@ -2074,7 +2104,11 @@ namespace Barotrauma
private void SetConfirmButtonBehavior()
{
if (IsBuying)
if (ActiveStore == null)
{
confirmButton.OnClicked = null;
}
else if (IsBuying)
{
confirmButton.ClickSound = GUISoundType.ConfirmTransaction;
confirmButton.Text = TextManager.Get("CampaignStore.Purchase");
@@ -2102,6 +2136,7 @@ namespace Barotrauma
private void SetConfirmButtonStatus()
{
confirmButton.Enabled =
ActiveStore != null &&
HasActiveTabPermissions() &&
ActiveShoppingCrateList.Content.RectTransform.Children.Any() &&
activeTab switch
@@ -2111,6 +2146,7 @@ namespace Barotrauma
StoreTab.SellSub => CurrentLocation != null && sellFromSubTotal <= ActiveStore.Balance,
_ => false
};
confirmButton.Visible = ActiveStore != null;
}
private void SetClearAllButtonStatus()
@@ -2169,7 +2205,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));
}
}
@@ -2184,29 +2220,32 @@ namespace Barotrauma
prevBalance = currBalance;
}
}
if (needsItemsToSellRefresh)
if (ActiveStore != null)
{
RefreshItemsToSell();
}
if (needsItemsToSellFromSubRefresh)
{
RefreshItemsToSellFromSub();
}
if (needsRefresh)
{
Refresh(updateOwned: ownedItemsUpdateTimer > 0.0f);
}
if (needsBuyingRefresh || HavePermissionsChanged(StoreTab.Buy))
{
RefreshBuying(updateOwned: ownedItemsUpdateTimer > 0.0f);
}
if (needsSellingRefresh || HavePermissionsChanged(StoreTab.Sell))
{
RefreshSelling(updateOwned: ownedItemsUpdateTimer > 0.0f);
}
if (needsSellingFromSubRefresh || HavePermissionsChanged(StoreTab.SellSub))
{
RefreshSellingFromSub(updateOwned: ownedItemsUpdateTimer > 0.0f, updateItemsToSellFromSub: sellableItemsFromSubUpdateTimer > 0.0f);
if (needsItemsToSellRefresh)
{
RefreshItemsToSell();
}
if (needsItemsToSellFromSubRefresh)
{
RefreshItemsToSellFromSub();
}
if (needsRefresh)
{
Refresh(updateOwned: ownedItemsUpdateTimer > 0.0f);
}
if (needsBuyingRefresh || HavePermissionsChanged(StoreTab.Buy))
{
RefreshBuying(updateOwned: ownedItemsUpdateTimer > 0.0f);
}
if (needsSellingRefresh || HavePermissionsChanged(StoreTab.Sell))
{
RefreshSelling(updateOwned: ownedItemsUpdateTimer > 0.0f);
}
if (needsSellingFromSubRefresh || HavePermissionsChanged(StoreTab.SellSub))
{
RefreshSellingFromSub(updateOwned: ownedItemsUpdateTimer > 0.0f, updateItemsToSellFromSub: sellableItemsFromSubUpdateTimer > 0.0f);
}
}
updateStopwatch.Stop();

View File

@@ -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)
@@ -673,7 +681,7 @@ namespace Barotrauma
{
if (GameMain.GameSession?.Campaign?.PendingSubmarineSwitch == null)
{
return Submarine.MainSub.Info;
return Submarine.MainSub?.Info;
}
else
{

View File

@@ -34,7 +34,7 @@ namespace Barotrauma
private List<CharacterTeamType> teamIDs;
private const string inLobbyString = "\u2022 \u2022 \u2022";
private GUIFrame pendingChangesFrame = null;
public static GUIFrame PendingChangesFrame = null;
public static Color OwnCharacterBGColor = Color.Gold * 0.7f;
private bool isTransferMenuOpen;
@@ -44,6 +44,7 @@ namespace Barotrauma
private float transferMenuOpenState;
private bool transferMenuStateCompleted;
private readonly HashSet<Identifier> registeredEvents = new HashSet<Identifier>();
private readonly TalentMenu talentMenu = new TalentMenu();
private class LinkedGUI
{
@@ -206,15 +207,8 @@ namespace Barotrauma
transferMenuButton.RectTransform.AbsoluteOffset = new Point(0, -pos - transferMenu.Rect.Height);
}
GameSession.UpdateTalentNotificationIndicator(talentPointNotification);
if (Character.Controlled?.Info is { } characterInfo && talentResetButton != null && talentApplyButton != null)
{
int talentCount = selectedTalents.Count - characterInfo.GetUnlockedTalentsInTree().Count();
talentResetButton.Enabled = talentApplyButton.Enabled = talentCount > 0;
if (talentApplyButton.Enabled && talentApplyButton.FlashTimer <= 0.0f)
{
talentApplyButton.Flash(GUIStyle.Orange);
}
}
talentMenu?.Update();
if (SelectedTab != InfoFrameTab.Crew) { return; }
if (linkedGUIList == null) { return; }
@@ -325,11 +319,11 @@ namespace Barotrauma
AbsoluteOffset = new Point(contentFrame.Rect.X, contentFrame.Rect.Bottom + GUI.IntScale(8))
}, style: null);
pendingChangesFrame = new GUIFrame(new RectTransform(Vector2.One, bottomDisclaimerFrame.RectTransform, Anchor.Center), style: null);
PendingChangesFrame = new GUIFrame(new RectTransform(Vector2.One, bottomDisclaimerFrame.RectTransform, Anchor.Center), style: null);
if (GameMain.NetLobbyScreen?.CampaignCharacterDiscarded ?? false)
{
NetLobbyScreen.CreateChangesPendingFrame(pendingChangesFrame);
NetLobbyScreen.CreateChangesPendingFrame(PendingChangesFrame);
}
SetBalanceText(balanceText, campaignMode.Bank.Balance);
@@ -403,7 +397,7 @@ namespace Barotrauma
CreateSubmarineInfo(infoFrameHolder, Submarine.MainSub);
break;
case InfoFrameTab.Talents:
CreateCharacterInfo(infoFrameHolder);
talentMenu.CreateGUI(infoFrameHolder, Character.Controlled ?? GameMain.Client?.Character);
break;
}
}
@@ -957,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)
{
@@ -1487,7 +1491,11 @@ namespace Barotrauma
GUIFrame missionFrame = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox");
int padding = (int)(0.0245f * missionFrame.Rect.Height);
GUIFrame missionFrameContent = new GUIFrame(new RectTransform(new Point(missionFrame.Rect.Width - padding * 2, missionFrame.Rect.Height - padding * 2), infoFrame.RectTransform, Anchor.Center), style: null);
Location location = GameMain.GameSession.EndLocation ?? GameMain.GameSession.StartLocation;
Location location = GameMain.GameSession.StartLocation;
if (Level.Loaded.Type == LevelData.LevelType.LocationConnection)
{
location ??= GameMain.GameSession.EndLocation;
}
GUILayoutGroup locationInfoContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), missionFrameContent.RectTransform))
{
@@ -1774,373 +1782,10 @@ namespace Barotrauma
sub.Info.CreateSpecsWindow(specsListBox, GUIStyle.Font, includeTitle: false, includeClass: false, includeDescription: true);
}
}
private Color unselectedColor = new Color(240, 255, 255, 225);
private Color unselectableColor = new Color(100, 100, 100, 225);
private Color pressedColor = new Color(60, 60, 60, 225);
private readonly List<(GUIButton button, GUIComponent icon)> talentButtons = new List<(GUIButton button, GUIComponent icon)>();
private readonly List<(Identifier talentTree, int index, GUIImage icon, GUIFrame background, GUIFrame backgroundGlow)> talentCornerIcons = new List<(Identifier talentTree, int index, GUIImage icon, GUIFrame background, GUIFrame backgroundGlow)>();
private List<Identifier> selectedTalents = new List<Identifier>();
private GUITextBlock experienceText;
private GUIProgressBar experienceBar;
private GUITextBlock talentPointText;
private GUIListBox skillListBox;
private GUIButton talentApplyButton,
talentResetButton;
private GUIImage talentPointNotification;
private readonly ImmutableDictionary<TalentTree.TalentTreeStageState, GUIComponentStyle> talentStageStyles = new Dictionary<TalentTree.TalentTreeStageState, GUIComponentStyle>
{
{ TalentTree.TalentTreeStageState.Invalid, GUIStyle.GetComponentStyle("TalentTreeLocked") },
{ TalentTree.TalentTreeStageState.Locked, GUIStyle.GetComponentStyle("TalentTreeLocked") },
{ TalentTree.TalentTreeStageState.Unlocked, GUIStyle.GetComponentStyle("TalentTreePurchased") },
{ TalentTree.TalentTreeStageState.Available, GUIStyle.GetComponentStyle("TalentTreeUnlocked") },
{ TalentTree.TalentTreeStageState.Highlighted, GUIStyle.GetComponentStyle("TalentTreeAvailable") },
}.ToImmutableDictionary();
private readonly ImmutableDictionary<TalentTree.TalentTreeStageState, Color> talentStageBackgroundColors = new Dictionary<TalentTree.TalentTreeStageState, Color>
{
{ TalentTree.TalentTreeStageState.Invalid, new Color(48,48,48,255) },
{ TalentTree.TalentTreeStageState.Locked, new Color(48,48,48,255) },
{ TalentTree.TalentTreeStageState.Unlocked, new Color(24,37,31,255) },
{ TalentTree.TalentTreeStageState.Available, new Color(50,47,33,255) },
{ TalentTree.TalentTreeStageState.Highlighted, new Color(50,47,33,255) },
}.ToImmutableDictionary();
private void CreateCharacterInfo(GUIFrame infoFrame)
{
infoFrame.ClearChildren();
talentButtons.Clear();
talentCornerIcons.Clear();
GUIFrame background = new GUIFrame(new RectTransform(Vector2.One, infoFrame.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), infoFrame.RectTransform, Anchor.Center), style: null);
GUIFrame content = new GUIFrame(new RectTransform(new Vector2(0.98f), frame.RectTransform, Anchor.Center), style: null);
GUIFrame characterSettingsFrame = null;
GUILayoutGroup characterLayout = null;
if (!(GameMain.NetworkMember is null))
{
characterSettingsFrame = new GUIFrame(new RectTransform(Vector2.One, frame.RectTransform), style: null) { Visible = false };
characterLayout = new GUILayoutGroup(new RectTransform(Vector2.One, characterSettingsFrame.RectTransform));
GUIFrame containerFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.9f), characterLayout.RectTransform), style: null);
GUIFrame playerFrame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.7f), containerFrame.RectTransform, Anchor.Center), style: null);
GameMain.NetLobbyScreen.CreatePlayerFrame(playerFrame, alwaysAllowEditing: true, createPendingText: false);
}
Character controlledCharacter = Character.Controlled;
CharacterInfo info = controlledCharacter?.Info ?? GameMain.Client?.CharacterInfo;
if (info == null) { return; }
Job job = info.Job;
GUILayoutGroup contentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), content.RectTransform, anchor: Anchor.Center), childAnchor: Anchor.TopCenter)
{
AbsoluteSpacing = GUI.IntScale(10),
Stretch = true
};
GUILayoutGroup topLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), contentLayout.RectTransform, Anchor.Center), isHorizontal: true);
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);
});
GUILayoutGroup nameLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1f), topLayout.RectTransform))
{
AbsoluteSpacing = GUI.IntScale(5),
CanBeFocused = true
};
GUITextBlock nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), nameLayout.RectTransform), info.Name, font: GUIStyle.SubHeadingFont);
if (!info.OmitJobInMenus)
{
nameBlock.TextColor = job.Prefab.UIColor;
GUITextBlock jobBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), nameLayout.RectTransform), job.Name, font: GUIStyle.SmallFont) { TextColor = job.Prefab.UIColor };
}
if (info.PersonalityTrait != null)
{
LocalizedString traitString = TextManager.AddPunctuation(':', TextManager.Get("PersonalityTrait"), info.PersonalityTrait.DisplayName);
Vector2 traitSize = GUIStyle.SmallFont.MeasureString(traitString);
GUITextBlock traitBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), traitString, font: GUIStyle.SmallFont);
traitBlock.RectTransform.NonScaledSize = traitSize.Pad(traitBlock.Padding).ToPoint();
}
IEnumerable<TalentPrefab> talentsOutsideTree = info.GetUnlockedTalentsOutsideTree().Select(e => TalentPrefab.TalentPrefabs.Find(c => c.Identifier == e));
if (talentsOutsideTree.Count() > 0)
{
//spacing
new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), nameLayout.RectTransform), style: null);
GUILayoutGroup extraTalentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), 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.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)
{
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();
foreach (var extraTalent in talentsOutsideTree)
{
var img = new GUIImage(new RectTransform(new Point(extraTalentList.Content.Rect.Height), extraTalentList.Content.RectTransform), sprite: extraTalent.Icon, scaleToFit: true)
{
ToolTip = RichString.Rich($"‖color:{Color.White.ToStringHex()}‖{extraTalent.DisplayName}‖color:end‖" + "\n\n" + extraTalent.Description),
Color = GUIStyle.Green
};
img.RectTransform.SizeChanged += () =>
{
img.RectTransform.MaxSize = new Point(img.Rect.Height);
};
}
}
GUILayoutGroup skillLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 1f), topLayout.RectTransform), childAnchor: Anchor.TopRight)
{
AbsoluteSpacing = GUI.IntScale(5),
Stretch = true
};
GUITextBlock skillBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillLayout.RectTransform), TextManager.Get("skills"), font: GUIStyle.SubHeadingFont);
skillListBox = new GUIListBox(new RectTransform(new Vector2(1f, 1f - skillBlock.RectTransform.RelativeSize.Y), skillLayout.RectTransform), style: null);
CreateSkillList(controlledCharacter, info, skillListBox);
new GUIFrame(new RectTransform(new Vector2(1f, 1f), contentLayout.RectTransform), style: "HorizontalLine");
GUIListBox talentTreeListBox = new GUIListBox(new RectTransform(new Vector2(1f, 0.6f), contentLayout.RectTransform, Anchor.TopCenter), isHorizontal: true, style: null);
if (controlledCharacter == null)
{
talentTreeListBox.Enabled = false;
}
else
{
if (!TalentTree.JobTalentTrees.TryGet(info.Job.Prefab.Identifier, out TalentTree talentTree)) { return; }
selectedTalents = info.GetUnlockedTalentsInTree().ToList();
List<GUITextBlock> subTreeNames = new List<GUITextBlock>();
foreach (var subTree in talentTree.TalentSubTrees)
{
GUIFrame subTreeFrame = new GUIFrame(new RectTransform(new Vector2(0.333f, 1f), talentTreeListBox.Content.RectTransform, anchor: Anchor.TopLeft), style: null);
GUILayoutGroup subTreeLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 1f), subTreeFrame.RectTransform, Anchor.Center), false, childAnchor: Anchor.TopCenter);
GUIFrame subtreeTitleFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.111f), subTreeLayoutGroup.RectTransform, anchor: Anchor.TopCenter), style: null);
int elementPadding = GUI.IntScale(8);
Point headerSize = subtreeTitleFrame.RectTransform.NonScaledSize;
GUIFrame subTreeTitleBackground = new GUIFrame(new RectTransform(new Point(headerSize.X - elementPadding, headerSize.Y), subtreeTitleFrame.RectTransform, anchor: Anchor.Center), style: "SubtreeHeader");
subTreeNames.Add(new GUITextBlock(new RectTransform(Vector2.One, subTreeTitleBackground.RectTransform, anchor: Anchor.TopCenter), subTree.DisplayName, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center));
for (int i = 0; i < 4; i++)
{
GUIFrame talentOptionFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.222f), subTreeLayoutGroup.RectTransform, anchor: Anchor.TopCenter), style: null);
Point talentFrameSize = talentOptionFrame.RectTransform.NonScaledSize;
GUIFrame talentBackground = new GUIFrame(new RectTransform(new Point(talentFrameSize.X - elementPadding, talentFrameSize.Y - elementPadding), talentOptionFrame.RectTransform, anchor: Anchor.Center), style: "TalentBackground")
{
Color = talentStageBackgroundColors[TalentTree.TalentTreeStageState.Locked]
};
GUIFrame talentBackgroundHighlight = new GUIFrame(new RectTransform(Vector2.One, talentBackground.RectTransform, anchor: Anchor.Center), style: "TalentBackgroundGlow") { Visible = false };
GUIImage cornerIcon = new GUIImage(new RectTransform(new Vector2(0.2f), talentOptionFrame.RectTransform, anchor: Anchor.BottomRight, scaleBasis: ScaleBasis.BothHeight) { MaxSize = new Point(16) }, style: null)
{
CanBeFocused = false,
Color = talentStageBackgroundColors[TalentTree.TalentTreeStageState.Locked]
};
Point iconSize = cornerIcon.RectTransform.NonScaledSize;
cornerIcon.RectTransform.AbsoluteOffset = new Point(iconSize.X / 2, iconSize.Y / 2);
if (subTree.TalentOptionStages.Length <= i) { continue; }
TalentOption talentOption = subTree.TalentOptionStages[i];
GUILayoutGroup talentOptionCenterGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.75f, 0.7f), talentOptionFrame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterLeft);
GUILayoutGroup talentOptionLayoutGroup = new GUILayoutGroup(new RectTransform(Vector2.One, talentOptionCenterGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true };
foreach (Identifier talentId in talentOption.TalentIdentifiers.OrderBy(t => t))
{
if (!TalentPrefab.TalentPrefabs.TryGet(talentId, out TalentPrefab talent)) { continue; }
GUIFrame talentFrame = new GUIFrame(new RectTransform(Vector2.One, talentOptionLayoutGroup.RectTransform), style: null)
{
CanBeFocused = false
};
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),
UserData = talent.Identifier,
PressedColor = pressedColor,
Enabled = controlledCharacter != null,
OnClicked = (button, userData) =>
{
// deselect other buttons in tier by removing their selected talents from pool
foreach (GUIButton guiButton in talentOptionLayoutGroup.GetAllChildren<GUIButton>())
{
if (guiButton.UserData is Identifier otherTalentIdentifier && guiButton != button)
{
if (!controlledCharacter.HasTalent(otherTalentIdentifier))
{
selectedTalents.Remove(otherTalentIdentifier);
}
}
}
Identifier talentIdentifier = (Identifier)userData;
if (TalentTree.IsViableTalentForCharacter(controlledCharacter, talentIdentifier, selectedTalents))
{
if (!selectedTalents.Contains(talentIdentifier))
{
selectedTalents.Add(talentIdentifier);
}
}
else if (!controlledCharacter.HasTalent(talentIdentifier))
{
selectedTalents.Remove(talentIdentifier);
}
UpdateTalentInfo();
return true;
},
};
talentButton.Color = talentButton.HoverColor = talentButton.PressedColor = talentButton.SelectedColor = talentButton.DisabledColor = Color.Transparent;
GUIComponent iconImage;
if (talent.Icon is null)
{
iconImage = new GUITextBlock(new RectTransform(Vector2.One, talentButton.RectTransform, anchor: Anchor.Center), text: "???", font: GUIStyle.LargeFont, textAlignment: Alignment.Center, style: null)
{
OutlineColor = GUIStyle.Red,
TextColor = GUIStyle.Red,
PressedColor = unselectableColor,
DisabledColor = unselectableColor,
CanBeFocused = false,
};
}
else
{
iconImage = new GUIImage(new RectTransform(Vector2.One, talentButton.RectTransform, anchor: Anchor.Center), sprite: talent.Icon, scaleToFit: true)
{
PressedColor = unselectableColor,
DisabledColor = unselectableColor * 0.5f,
CanBeFocused = false,
};
}
iconImage.Enabled = talentButton.Enabled;
talentButtons.Add((talentButton, iconImage));
}
talentCornerIcons.Add((subTree.Identifier, i, cornerIcon, talentBackground, talentBackgroundHighlight));
}
}
GUITextBlock.AutoScaleAndNormalize(subTreeNames);
GUILayoutGroup bottomLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.07f), contentLayout.RectTransform, Anchor.TopCenter), isHorizontal: true)
{
RelativeSpacing = 0.01f,
Stretch = true
};
GUILayoutGroup experienceLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.59f, 1f), bottomLayout.RectTransform));
GUIFrame experienceBarFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.5f), experienceLayout.RectTransform), style: null);
experienceBar = new GUIProgressBar(new RectTransform(new Vector2(1f, 1f), experienceBarFrame.RectTransform, Anchor.CenterLeft),
barSize: info.GetProgressTowardsNextLevel(), color: GUIStyle.Green)
{
IsHorizontal = true,
};
experienceText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), experienceBarFrame.RectTransform, anchor: Anchor.Center), "", font: GUIStyle.Font, textAlignment: Alignment.CenterRight)
{
Shadow = true,
ToolTip = TextManager.Get("experiencetooltip")
};
talentPointText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), experienceLayout.RectTransform, anchor: Anchor.Center), "", font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterRight) { AutoScaleVertical = true };
talentResetButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), bottomLayout.RectTransform), text: TextManager.Get("reset"), style: "GUIButtonFreeScale")
{
OnClicked = ResetTalentSelection
};
talentApplyButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), bottomLayout.RectTransform), text: TextManager.Get("applysettingsbutton"), style: "GUIButtonFreeScale")
{
OnClicked = ApplyTalentSelection,
};
GUITextBlock.AutoScaleAndNormalize(talentResetButton.TextBlock, talentApplyButton.TextBlock);
}
if (!(GameMain.NetworkMember is null))
{
GUIButton newCharacterBox = new GUIButton(new RectTransform(new Vector2(0.5f, 0.2f), skillLayout.RectTransform, Anchor.BottomRight),
text: GameMain.NetLobbyScreen.CampaignCharacterDiscarded ? TextManager.Get("settings") : TextManager.Get("createnew"), style: "GUIButtonSmall")
{
IgnoreLayoutGroups = false
};
newCharacterBox.TextBlock.AutoScaleHorizontal = true;
newCharacterBox.OnClicked = (button, o) =>
{
if (!GameMain.NetLobbyScreen.CampaignCharacterDiscarded)
{
GameMain.NetLobbyScreen.TryDiscardCampaignCharacter(() =>
{
newCharacterBox.Text = TextManager.Get("settings");
if (pendingChangesFrame != null)
{
NetLobbyScreen.CreateChangesPendingFrame(pendingChangesFrame);
}
OpenMenu();
});
return true;
}
OpenMenu();
return true;
void OpenMenu()
{
characterSettingsFrame!.Visible = true;
content.Visible = false;
}
};
if (!(characterLayout is null))
{
GUILayoutGroup characterCloseButtonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), characterLayout.RectTransform), childAnchor: Anchor.BottomCenter);
new GUIButton(new RectTransform(new Vector2(0.4f, 1f), characterCloseButtonLayout.RectTransform), TextManager.Get("ApplySettingsButton")) //TODO: Is this text appropriate for this circumstance for all languages?
{
OnClicked = (button, o) =>
{
GameMain.Client?.SendCharacterInfo(GameMain.Client.PendingName);
characterSettingsFrame!.Visible = false;
content.Visible = true;
return true;
}
};
}
}
UpdateTalentInfo();
}
private void CreateSkillList(Character character, CharacterInfo info, GUIListBox parent)
public static void CreateSkillList(Character character, CharacterInfo info, GUIListBox parent)
{
parent.Content.ClearChildren();
List<GUITextBlock> skillNames = new List<GUITextBlock>();
@@ -2154,10 +1799,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),
@@ -2168,123 +1813,17 @@ 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();
GUITextBlock.AutoScaleAndNormalize(skillNames);
}
private void UpdateTalentInfo()
{
Character controlledCharacter = Character.Controlled;
if (controlledCharacter?.Info == null) { return; }
if (SelectedTab != InfoFrameTab.Talents) { return; }
bool unlockedAllTalents = controlledCharacter.HasUnlockedAllTalents();
if (unlockedAllTalents)
{
experienceText.Text = string.Empty;
experienceBar.BarSize = 1f;
}
else
{
experienceText.Text = $"{controlledCharacter.Info.ExperiencePoints - controlledCharacter.Info.GetExperienceRequiredForCurrentLevel()} / {controlledCharacter.Info.GetExperienceRequiredToLevelUp() - controlledCharacter.Info.GetExperienceRequiredForCurrentLevel()}";
experienceBar.BarSize = controlledCharacter.Info.GetProgressTowardsNextLevel();
}
selectedTalents = TalentTree.CheckTalentSelection(controlledCharacter, selectedTalents);
string pointsLeft = controlledCharacter.Info.GetAvailableTalentPoints().ToString();
int talentCount = selectedTalents.Count - controlledCharacter.Info.GetUnlockedTalentsInTree().Count();
if (unlockedAllTalents)
{
talentPointText.SetRichText($"‖color:{XMLExtensions.ToStringHex(Color.Gray)}‖{TextManager.Get("talentmenu.alltalentsunlocked")}‖color:end‖");
}
else if (talentCount > 0)
{
string pointsUsed = $"‖color:{XMLExtensions.ColorToString(GUIStyle.Red)}‖{-talentCount}‖color:end‖";
LocalizedString localizedString = TextManager.GetWithVariables("talentmenu.points.spending", ("[amount]", pointsLeft), ("[used]", pointsUsed));
talentPointText.SetRichText(localizedString);
}
else
{
talentPointText.SetRichText(TextManager.GetWithVariable("talentmenu.points", "[amount]", pointsLeft));
}
foreach (var (talentTree, index, icon, frame, glow) in talentCornerIcons)
{
TalentTree.TalentTreeStageState state = TalentTree.GetTalentOptionStageState(controlledCharacter, talentTree, index, selectedTalents);
GUIComponentStyle newStyle = talentStageStyles[state];
icon.ApplyStyle(newStyle);
icon.Color = newStyle.Color;
frame.Color = talentStageBackgroundColors[state];
glow.Visible = state == TalentTree.TalentTreeStageState.Highlighted;
}
foreach (var talentButton in talentButtons)
{
Identifier talentIdentifier = (Identifier)talentButton.button.UserData;
bool unselectable = !TalentTree.IsViableTalentForCharacter(controlledCharacter, talentIdentifier, selectedTalents) || controlledCharacter.HasTalent(talentIdentifier);
Color newTalentColor = unselectable ? unselectableColor : unselectedColor;
Color hoverColor = Color.White;
if (controlledCharacter.HasTalent(talentIdentifier))
{
newTalentColor = GUIStyle.Green;
}
else if (selectedTalents.Contains(talentIdentifier))
{
newTalentColor = GUIStyle.Orange;
hoverColor = Color.Lerp(GUIStyle.Orange, Color.White, 0.7f);
}
talentButton.icon.Color = newTalentColor;
talentButton.icon.HoverColor = hoverColor;
}
CreateSkillList(controlledCharacter, controlledCharacter.Info, skillListBox);
}
private void ApplyTalents(Character controlledCharacter)
{
selectedTalents = TalentTree.CheckTalentSelection(controlledCharacter, selectedTalents);
foreach (Identifier talent in selectedTalents)
{
controlledCharacter.GiveTalent(talent);
if (GameMain.Client != null)
{
GameMain.Client.CreateEntityEvent(controlledCharacter, new Character.UpdateTalentsEventData());
}
}
selectedTalents = controlledCharacter.Info.GetUnlockedTalentsInTree().ToList();
UpdateTalentInfo();
}
private bool ApplyTalentSelection(GUIButton guiButton, object userData)
{
Character controlledCharacter = Character.Controlled;
ApplyTalents(controlledCharacter);
return true;
}
private bool ResetTalentSelection(GUIButton guiButton, object userData)
{
Character controlledCharacter = Character.Controlled;
if (controlledCharacter?.Info == null) { return false; }
selectedTalents = controlledCharacter.Info.GetUnlockedTalentsInTree().ToList();
UpdateTalentInfo();
return true;
}
public void OnExperienceChanged(Character character)
{
if (character != Character.Controlled) { return; }
UpdateTalentInfo();
talentMenu.UpdateTalentInfo();
}
public void OnClose()

View File

@@ -0,0 +1,817 @@
#nullable enable
using System;
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.TalentStages;
namespace Barotrauma
{
internal readonly record struct TalentShowCaseButton(ImmutableHashSet<TalentButton> Buttons,
GUIComponent IconComponent);
internal readonly record struct TalentButton(GUIComponent IconComponent,
TalentPrefab Prefab)
{
public Identifier Identifier => Prefab.Identifier;
}
internal readonly record struct TalentCornerIcon(Identifier TalentTree,
int Index,
GUIImage IconComponent,
GUIFrame BackgroundComponent,
GUIFrame GlowComponent);
internal readonly struct TalentTreeStyle
{
public readonly GUIComponentStyle ComponentStyle;
public readonly Color Color;
public TalentTreeStyle(string componentStyle, Color color)
{
ComponentStyle = GUIStyle.GetComponentStyle(componentStyle);
Color = color;
}
}
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),
lockedColor = new Color(48, 48, 48, 255),
unlockedColor = new Color(24, 37, 31, 255),
availableColor = new Color(50, 47, 33, 255);
private static readonly ImmutableDictionary<TalentStages, TalentTreeStyle> talentStageStyles =
new Dictionary<TalentStages, TalentTreeStyle>
{
[Invalid] = new TalentTreeStyle("TalentTreeLocked", lockedColor),
[Locked] = new TalentTreeStyle("TalentTreeLocked", lockedColor),
[Unlocked] = new TalentTreeStyle("TalentTreePurchased", unlockedColor),
[Available] = new TalentTreeStyle("TalentTreeUnlocked", availableColor),
[Highlighted] = new TalentTreeStyle("TalentTreeAvailable", availableColor)
}.ToImmutableDictionary();
private readonly HashSet<TalentButton> talentButtons = new HashSet<TalentButton>();
private readonly HashSet<TalentShowCaseButton> talentShowCaseButtons = new HashSet<TalentShowCaseButton>();
private readonly HashSet<GUIComponent> showCaseTalentFrames = new HashSet<GUIComponent>();
private readonly HashSet<TalentCornerIcon> talentCornerIcons = new HashSet<TalentCornerIcon>();
private HashSet<Identifier> selectedTalents = new HashSet<Identifier>();
private readonly Queue<Identifier> showCaseClosureQueue = new();
private GUIListBox? skillListBox;
private GUITextBlock? talentPointText;
private GUIProgressBar? experienceBar;
private GUITextBlock? experienceText;
private GUILayoutGroup? skillLayout;
private GUIButton? talentApplyButton,
talentResetButton;
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);
GUIFrame content = new GUIFrame(new RectTransform(new Vector2(0.98f), frame.RectTransform, Anchor.Center), style: null);
GUILayoutGroup contentLayout = new GUILayoutGroup(new RectTransform(Vector2.One, content.RectTransform, anchor: Anchor.Center), childAnchor: Anchor.TopCenter)
{
AbsoluteSpacing = GUI.IntScale(10),
Stretch = true
};
if (characterInfo is null) { return; }
CreateStatPanel(contentLayout, characterInfo);
new GUIFrame(new RectTransform(new Vector2(1f, 1f), contentLayout.RectTransform), style: "HorizontalLine");
if (JobTalentTrees.TryGet(characterInfo.Job.Prefab.Identifier, out TalentTree? talentTree))
{
CreateTalentMenu(contentLayout, characterInfo, talentTree!);
}
CreateFooter(contentLayout, characterInfo);
UpdateTalentInfo();
if (GameMain.NetworkMember != null && IsOwnCharacter(characterInfo))
{
CreateMultiplayerCharacterSettings(frame, content);
}
}
private void CreateMultiplayerCharacterSettings(GUIComponent parent, GUIComponent content)
{
if (skillLayout is null) { return; }
GUIFrame characterSettingsFrame = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform), style: null) { Visible = false };
GUILayoutGroup characterLayout = new GUILayoutGroup(new RectTransform(Vector2.One, characterSettingsFrame.RectTransform));
GUIFrame containerFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.9f), characterLayout.RectTransform), style: null);
GUIFrame playerFrame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.7f), containerFrame.RectTransform, Anchor.Center), style: null);
GameMain.NetLobbyScreen.CreatePlayerFrame(playerFrame, alwaysAllowEditing: true, createPendingText: false);
GUIButton newCharacterBox = new GUIButton(new RectTransform(new Vector2(0.5f, 0.2f), skillLayout.RectTransform, Anchor.BottomRight),
text: GameMain.NetLobbyScreen.CampaignCharacterDiscarded ? TextManager.Get("settings") : TextManager.Get("createnew"), style: "GUIButtonSmall")
{
IgnoreLayoutGroups = false,
TextBlock =
{
AutoScaleHorizontal = true
}
};
newCharacterBox.OnClicked = (button, o) =>
{
if (!GameMain.NetLobbyScreen.CampaignCharacterDiscarded)
{
GameMain.NetLobbyScreen.TryDiscardCampaignCharacter(() =>
{
newCharacterBox.Text = TextManager.Get("settings");
if (TabMenu.PendingChangesFrame != null)
{
NetLobbyScreen.CreateChangesPendingFrame(TabMenu.PendingChangesFrame);
}
OpenMenu();
});
return true;
}
OpenMenu();
return true;
void OpenMenu()
{
characterSettingsFrame!.Visible = true;
content.Visible = false;
}
};
GUILayoutGroup characterCloseButtonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), characterLayout.RectTransform), childAnchor: Anchor.BottomCenter);
new GUIButton(new RectTransform(new Vector2(0.4f, 1f), characterCloseButtonLayout.RectTransform), TextManager.Get("ApplySettingsButton")) //TODO: Is this text appropriate for this circumstance for all languages?
{
OnClicked = (button, o) =>
{
GameMain.Client?.SendCharacterInfo(GameMain.Client.PendingName);
characterSettingsFrame.Visible = false;
content.Visible = true;
return true;
}
};
}
private void CreateStatPanel(GUIComponent parent, CharacterInfo info)
{
Job job = info.Job;
GUILayoutGroup topLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), parent.RectTransform, Anchor.Center), isHorizontal: true);
new GUICustomComponent(new RectTransform(new Vector2(0.25f, 1f), topLayout.RectTransform), onDraw: (batch, component) =>
{
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))
{
AbsoluteSpacing = GUI.IntScale(5),
CanBeFocused = true
};
GUITextBlock nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), nameLayout.RectTransform), info.Name, font: GUIStyle.SubHeadingFont);
if (!info.OmitJobInMenus)
{
nameBlock.TextColor = job.Prefab.UIColor;
GUITextBlock jobBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), nameLayout.RectTransform), job.Name, font: GUIStyle.SmallFont) { TextColor = job.Prefab.UIColor };
}
if (info.PersonalityTrait != null)
{
LocalizedString traitString = TextManager.AddPunctuation(':', TextManager.Get("PersonalityTrait"), info.PersonalityTrait.DisplayName);
Vector2 traitSize = GUIStyle.SmallFont.MeasureString(traitString);
GUITextBlock traitBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), traitString, font: GUIStyle.SmallFont);
traitBlock.RectTransform.NonScaledSize = traitSize.Pad(traitBlock.Padding).ToPoint();
}
ImmutableHashSet<TalentPrefab?> talentsOutsideTree = info.GetUnlockedTalentsOutsideTree().Select(static e => TalentPrefab.TalentPrefabs.Find(c => c.Identifier == e)).ToImmutableHashSet();
if (talentsOutsideTree.Any())
{
//spacing
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.55f), nameLayout.RectTransform), childAnchor: Anchor.TopCenter);
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.7f), extraTalentLayout.RectTransform, anchor: Anchor.Center), isHorizontal: true)
{
AutoHideScrollBar = false,
ResizeContentToMakeSpaceForScrollBar = false
};
extraTalentList.ScrollBar.RectTransform.SetPosition(Anchor.BottomCenter, Pivot.TopCenter);
extraTalentLayout.Recalculate();
extraTalentList.ForceLayoutRecalculation();
foreach (var extraTalent in talentsOutsideTree)
{
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" + ToolBox.ExtendColorToPercentageSigns(extraTalent.Description.Value)),
Color = GUIStyle.Green
};
}
}
skillLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 1f), topLayout.RectTransform), childAnchor: Anchor.TopRight)
{
AbsoluteSpacing = GUI.IntScale(5),
Stretch = true
};
GUITextBlock skillBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillLayout.RectTransform), TextManager.Get("skills"), font: GUIStyle.SubHeadingFont);
skillListBox = new GUIListBox(new RectTransform(new Vector2(1f, 1f - skillBlock.RectTransform.RelativeSize.Y), skillLayout.RectTransform), style: null);
TabMenu.CreateSkillList(info.Character, info, skillListBox);
}
private void CreateTalentMenu(GUIComponent parent, CharacterInfo info, TalentTree tree)
{
GUIListBox mainList = new GUIListBox(new RectTransform(new Vector2(1f, 0.9f), parent.RectTransform, anchor: Anchor.TopCenter));
selectedTalents = info.GetUnlockedTalentsInTree().ToHashSet();
var specializationCount = tree.TalentSubTrees.Count(t => t.Type == TalentTreeType.Specialization);
List<GUITextBlock> subTreeNames = new List<GUITextBlock>();
foreach (var subTree in tree.TalentSubTrees)
{
GUIListBox talentList;
GUIComponent talentParent;
Vector2 treeSize;
switch (subTree.Type)
{
case TalentTreeType.Primary:
talentList = mainList;
treeSize = new Vector2(1f, 0.5f);
break;
case TalentTreeType.Specialization:
talentList = GetSpecializationList();
treeSize = new Vector2(Math.Max(0.333f, 1.0f / tree.TalentSubTrees.Count(t => t.Type == TalentTreeType.Specialization)), 1f);
break;
default:
throw new ArgumentOutOfRangeException($"Invalid TalentTreeType \"{subTree.Type}\"");
}
talentParent = talentList.Content;
GUILayoutGroup subTreeLayoutGroup = new GUILayoutGroup(new RectTransform(treeSize, talentParent.RectTransform), isHorizontal: false, childAnchor: Anchor.TopCenter)
{
Stretch = true
};
if (subTree.Type != TalentTreeType.Primary)
{
GUIFrame subtreeTitleFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.05f), subTreeLayoutGroup.RectTransform, anchor: Anchor.TopCenter)
{ MinSize = new Point(0, GUI.IntScale(30)) }, style: null);
subtreeTitleFrame.RectTransform.IsFixedSize = true;
int elementPadding = GUI.IntScale(8);
Point headerSize = subtreeTitleFrame.RectTransform.NonScaledSize;
GUIFrame subTreeTitleBackground = new GUIFrame(new RectTransform(new Point(headerSize.X - elementPadding, headerSize.Y), subtreeTitleFrame.RectTransform, anchor: Anchor.Center), style: "SubtreeHeader");
subTreeNames.Add(new GUITextBlock(new RectTransform(Vector2.One, subTreeTitleBackground.RectTransform, anchor: Anchor.TopCenter), subTree.DisplayName, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center));
}
int optionAmount = subTree.TalentOptionStages.Length;
for (int i = 0; i < optionAmount; i++)
{
TalentOption option = subTree.TalentOptionStages[i];
CreateTalentOption(subTreeLayoutGroup, subTree, i, option, info, specializationCount);
}
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)));
talentList.RectTransform.MinSize = new Point(0, talentList.Rect.Height);
}
}
var specializationList = GetSpecializationList();
//resize (scale up) children if there's less than 3 of them to make them cover the whole width of the menu
specializationList.Content.RectTransform.Resize(new Point(specializationList.Content.Children.Sum(static c => c.Rect.Width), specializationList.Rect.Height),
resizeChildren: specializationCount < 3);
//make room for scrollbar if there's more than the default amount of specializations
if (specializationCount > 3)
{
specializationList.RectTransform.MinSize = new Point(specializationList.Rect.Width, specializationList.Content.Rect.Height + (int)(specializationList.ScrollBar.Rect.Height * 0.9f));
}
GUITextBlock.AutoScaleAndNormalize(subTreeNames);
GUIListBox GetSpecializationList()
{
if (mainList.Content.Children.LastOrDefault() is GUIListBox specList)
{
return specList;
}
GUIListBox newSpecializationList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.5f), mainList.Content.RectTransform, Anchor.TopCenter), isHorizontal: true, style: null);
return newSpecializationList;
}
}
private void CreateTalentOption(GUIComponent parent, TalentSubTree subTree, int index, TalentOption talentOption, CharacterInfo info, int specializationCount)
{
int elementPadding = GUI.IntScale(8);
int height = GUI.IntScale((GameMain.GameSession?.Campaign == null ? 65 : 60) * (specializationCount > 3 ? 0.97f : 1.0f));
GUIFrame talentOptionFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.01f), parent.RectTransform, anchor: Anchor.TopCenter)
{ MinSize = new Point(0, height) }, style: null);
Point talentFrameSize = talentOptionFrame.RectTransform.NonScaledSize;
GUIFrame talentBackground = new GUIFrame(new RectTransform(new Point(talentFrameSize.X - elementPadding, talentFrameSize.Y - elementPadding), talentOptionFrame.RectTransform, anchor: Anchor.Center),
style: "TalentBackground")
{
Color = talentStageStyles[Locked].Color
};
GUIFrame talentBackgroundHighlight = new GUIFrame(new RectTransform(Vector2.One, talentBackground.RectTransform, anchor: Anchor.Center), style: "TalentBackgroundGlow") { Visible = false };
GUIImage cornerIcon = new GUIImage(new RectTransform(new Vector2(0.2f), talentOptionFrame.RectTransform, anchor: Anchor.BottomRight, scaleBasis: ScaleBasis.BothHeight) { MaxSize = new Point(16) }, style: null)
{
CanBeFocused = false,
Color = talentStageStyles[Locked].Color
};
Point iconSize = cornerIcon.RectTransform.NonScaledSize;
cornerIcon.RectTransform.AbsoluteOffset = new Point(iconSize.X / 2, iconSize.Y / 2);
GUILayoutGroup talentOptionCenterGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.6f, 0.9f), talentOptionFrame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterLeft);
GUILayoutGroup talentOptionLayoutGroup = new GUILayoutGroup(new RectTransform(Vector2.One, talentOptionCenterGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true };
HashSet<Identifier> talentOptionIdentifiers = talentOption.TalentIdentifiers.OrderBy(static t => t).ToHashSet();
HashSet<TalentButton> buttonsToAdd = new();
Dictionary<GUILayoutGroup, ImmutableHashSet<Identifier>> showCaseTalentParents = new();
Dictionary<Identifier, GUIComponent> showCaseTalentButtonsToAdd = new();
foreach (var (showCaseTalentIdentifier, talents) in talentOption.ShowCaseTalents)
{
talentOptionIdentifiers.Add(showCaseTalentIdentifier);
Point parentSize = talentBackground.RectTransform.NonScaledSize;
GUIFrame showCaseFrame = new GUIFrame(new RectTransform(new Point((int)(parentSize.X / 3f * (talents.Count - 1)), parentSize.Y)), style: "GUITooltip")
{
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);
GUILayoutGroup showcaseLayout = new GUILayoutGroup(new RectTransform(Vector2.One, showcaseCenterGroup.RectTransform), isHorizontal: true) { Stretch = true };
showCaseTalentParents.Add(showcaseLayout, talents);
showCaseTalentFrames.Add(showCaseFrame);
}
foreach (Identifier talentId in talentOptionIdentifiers)
{
if (!TalentPrefab.TalentPrefabs.TryGet(talentId, out TalentPrefab? talent)) { continue; }
bool isShowCaseTalent = talentOption.ShowCaseTalents.ContainsKey(talentId);
GUIComponent talentParent = talentOptionLayoutGroup;
foreach (var (key, value) in showCaseTalentParents)
{
if (value.Contains(talentId))
{
talentParent = key;
break;
}
}
GUIFrame talentFrame = new GUIFrame(new RectTransform(Vector2.One, talentParent.RectTransform), style: null)
{
CanBeFocused = false
};
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" + ToolBox.ExtendColorToPercentageSigns(talent.Description.Value)),
UserData = talent.Identifier,
PressedColor = pressedColor,
Enabled = info.Character != null,
OnClicked = (button, userData) =>
{
if (isShowCaseTalent)
{
foreach (GUIComponent component in showCaseTalentFrames)
{
if (component.UserData is Identifier showcaseIdentifier && showcaseIdentifier == talentId)
{
component.RectTransform.ScreenSpaceOffset = new Point((int)(button.Rect.Location.X - component.Rect.Width / 2f + button.Rect.Width / 2f), button.Rect.Location.Y - component.Rect.Height);
component.Visible = true;
}
else
{
component.Visible = false;
}
}
return true;
}
if (character is null) { return false; }
Identifier talentIdentifier = (Identifier)userData;
if (talentOption.MaxChosenTalents is 1)
{
// deselect other buttons in tier by removing their selected talents from pool
foreach (Identifier identifier in selectedTalents)
{
if (character.HasTalent(identifier) || identifier == talentId) { continue; }
if (talentOptionIdentifiers.Contains(identifier))
{
selectedTalents.Remove(identifier);
}
}
}
if (character.HasTalent(talentIdentifier))
{
return true;
}
else if (IsViableTalentForCharacter(info.Character, talentIdentifier, selectedTalents))
{
if (!selectedTalents.Contains(talentIdentifier))
{
selectedTalents.Add(talentIdentifier);
}
else
{
selectedTalents.Remove(talentIdentifier);
}
}
else
{
selectedTalents.Remove(talentIdentifier);
}
UpdateTalentInfo();
return true;
},
};
talentButton.Color = talentButton.HoverColor = talentButton.PressedColor = talentButton.SelectedColor = talentButton.DisabledColor = Color.Transparent;
GUIComponent iconImage;
if (talent.Icon is null)
{
iconImage = new GUITextBlock(new RectTransform(Vector2.One, talentButton.RectTransform, anchor: Anchor.Center), text: "???", font: GUIStyle.LargeFont, textAlignment: Alignment.Center, style: null)
{
OutlineColor = GUIStyle.Red,
TextColor = GUIStyle.Red,
PressedColor = unselectableColor,
DisabledColor = unselectableColor,
CanBeFocused = false,
};
}
else
{
iconImage = new GUIImage(new RectTransform(Vector2.One, talentButton.RectTransform, anchor: Anchor.Center), sprite: talent.Icon, scaleToFit: true)
{
Color = talent.ColorOverride.TryUnwrap(out Color color) ? color : Color.White,
PressedColor = unselectableColor,
DisabledColor = unselectableColor * 0.5f,
CanBeFocused = false,
};
}
iconImage.Enabled = talentButton.Enabled;
if (isShowCaseTalent)
{
showCaseTalentButtonsToAdd.Add(talentId, iconImage);
continue;
}
buttonsToAdd.Add(new TalentButton(iconImage, talent));
}
foreach (TalentButton button in buttonsToAdd)
{
talentButtons.Add(button);
}
foreach (var (key, value) in showCaseTalentButtonsToAdd)
{
HashSet<TalentButton> 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));
}
private void CreateFooter(GUIComponent parent, CharacterInfo info)
{
GUILayoutGroup bottomLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.07f), parent.RectTransform, Anchor.TopCenter), isHorizontal: true)
{
RelativeSpacing = 0.01f,
Stretch = true
};
GUILayoutGroup experienceLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.59f, 1f), bottomLayout.RectTransform));
GUIFrame experienceBarFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.5f), experienceLayout.RectTransform), style: null);
experienceBar = new GUIProgressBar(new RectTransform(new Vector2(1f, 1f), experienceBarFrame.RectTransform, Anchor.CenterLeft),
barSize: info.GetProgressTowardsNextLevel(), color: GUIStyle.Green)
{
IsHorizontal = true,
};
experienceText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), experienceBarFrame.RectTransform, anchor: Anchor.Center), "", font: GUIStyle.Font, textAlignment: Alignment.CenterRight)
{
Shadow = true,
ToolTip = TextManager.Get("experiencetooltip")
};
talentPointText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), experienceLayout.RectTransform, anchor: Anchor.Center), "", font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterRight)
{ AutoScaleVertical = true };
talentResetButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), bottomLayout.RectTransform), text: TextManager.Get("reset"), style: "GUIButtonFreeScale")
{
OnClicked = ResetTalentSelection
};
talentApplyButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), bottomLayout.RectTransform), text: TextManager.Get("applysettingsbutton"), style: "GUIButtonFreeScale")
{
OnClicked = ApplyTalentSelection,
};
GUITextBlock.AutoScaleAndNormalize(talentResetButton.TextBlock, talentApplyButton.TextBlock);
}
private bool ResetTalentSelection(GUIButton guiButton, object userData)
{
if (characterInfo is null) { return false; }
selectedTalents = characterInfo.GetUnlockedTalentsInTree().ToHashSet();
UpdateTalentInfo();
return true;
}
private void ApplyTalents(Character controlledCharacter)
{
foreach (Identifier talent in CheckTalentSelection(controlledCharacter, selectedTalents))
{
controlledCharacter.GiveTalent(talent);
if (GameMain.Client != null)
{
GameMain.Client.CreateEntityEvent(controlledCharacter, new Character.UpdateTalentsEventData());
}
}
UpdateTalentInfo();
}
private bool ApplyTalentSelection(GUIButton guiButton, object userData)
{
if (character is null) { return false; }
ApplyTalents(character);
return true;
}
public void UpdateTalentInfo()
{
if (character is null || characterInfo is null) { return; }
bool unlockedAllTalents = character.HasUnlockedAllTalents();
if (experienceBar is null || experienceText is null) { return; }
if (unlockedAllTalents)
{
experienceText.Text = string.Empty;
experienceBar.BarSize = 1f;
}
else
{
experienceText.Text = $"{characterInfo.ExperiencePoints - characterInfo.GetExperienceRequiredForCurrentLevel()} / {characterInfo.GetExperienceRequiredToLevelUp() - characterInfo.GetExperienceRequiredForCurrentLevel()}";
experienceBar.BarSize = characterInfo.GetProgressTowardsNextLevel();
}
selectedTalents = CheckTalentSelection(character, selectedTalents).ToHashSet();
string pointsLeft = characterInfo.GetAvailableTalentPoints().ToString();
int talentCount = selectedTalents.Count - characterInfo.GetUnlockedTalentsInTree().Count();
if (unlockedAllTalents)
{
talentPointText?.SetRichText($"‖color:{Color.Gray.ToStringHex()}‖{TextManager.Get("talentmenu.alltalentsunlocked")}‖color:end‖");
}
else if (talentCount > 0)
{
string pointsUsed = $"‖color:{XMLExtensions.ToStringHex(GUIStyle.Red)}‖{-talentCount}‖color:end‖";
LocalizedString localizedString = TextManager.GetWithVariables("talentmenu.points.spending", ("[amount]", pointsLeft), ("[used]", pointsUsed));
talentPointText?.SetRichText(localizedString);
}
else
{
talentPointText?.SetRichText(TextManager.GetWithVariable("talentmenu.points", "[amount]", pointsLeft));
}
foreach (TalentCornerIcon cornerIcon in talentCornerIcons)
{
TalentStages state = GetTalentOptionStageState(character, cornerIcon.TalentTree, cornerIcon.Index, selectedTalents);
TalentTreeStyle style = talentStageStyles[state];
GUIComponentStyle newStyle = style.ComponentStyle;
cornerIcon.IconComponent.ApplyStyle(newStyle);
cornerIcon.IconComponent.Color = newStyle.Color;
cornerIcon.BackgroundComponent.Color = style.Color;
cornerIcon.GlowComponent.Visible = state == Highlighted;
}
foreach (TalentButton talentButton in talentButtons)
{
TalentStages stage = GetTalentState(character, talentButton.Identifier, selectedTalents);
ApplyTalentIconColor(stage, talentButton.IconComponent, talentButton.Prefab.ColorOverride);
}
foreach (TalentShowCaseButton showCaseTalentButton in talentShowCaseButtons)
{
TalentStages collectiveTalentStage = GetCollectiveTalentState(character, showCaseTalentButton.Buttons, selectedTalents);
ApplyTalentIconColor(collectiveTalentStage, showCaseTalentButton.IconComponent, Option<Color>.None());
}
if (skillListBox is null) { return; }
TabMenu.CreateSkillList(character, characterInfo, skillListBox);
static TalentStages GetTalentState(Character character, Identifier talentIdentifier, IReadOnlyCollection<Identifier> selectedTalents)
{
bool unselectable = !IsViableTalentForCharacter(character, talentIdentifier, selectedTalents) || character.HasTalent(talentIdentifier);
TalentStages stage = unselectable ? Locked : Available;
if (unselectable)
{
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<Color> 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<Color> 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<TalentButton> buttons, IReadOnlyCollection<Identifier> selectedTalents)
{
HashSet<TalentStages> talentStages = new HashSet<TalentStages>();
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 (characterInfo is null || talentResetButton is null || talentApplyButton is null) { return; }
int talentCount = selectedTalents.Count - characterInfo.GetUnlockedTalentsInTree().Count();
talentResetButton.Enabled = talentApplyButton.Enabled = talentCount > 0;
if (talentApplyButton.Enabled && talentApplyButton.FlashTimer <= 0.0f)
{
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);
}
}
}

View File

@@ -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 };
@@ -433,8 +436,8 @@ namespace Barotrauma
Location location = Campaign.Map.CurrentLocation;
int hullRepairCost = Campaign.GetHullRepairCost();
int itemRepairCost = Campaign.GetItemRepairCost();
int hullRepairCost = CampaignMode.GetHullRepairCost();
int itemRepairCost = CampaignMode.GetItemRepairCost();
int shuttleRetrieveCost = CampaignMode.ShuttleReplaceCost;
if (location != null)
{

View File

@@ -30,7 +30,7 @@ namespace Barotrauma
Data = data,
OnClick = (GUITextBlock component, GUITextBlock.ClickableArea area) =>
{
GameMain.Instance.ShowOpenUrlInWebBrowserPrompt("https://gameanalytics.com/privacy/");
GameMain.ShowOpenUrlInWebBrowserPrompt("https://gameanalytics.com/privacy/");
}
});
}

View File

@@ -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;
@@ -15,6 +16,7 @@ using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Threading;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -317,7 +319,7 @@ namespace Barotrauma
GraphicsDeviceManager.SynchronizeWithVerticalRetrace = GameSettings.CurrentConfig.Graphics.VSync;
SetWindowMode(GameSettings.CurrentConfig.Graphics.DisplayMode);
defaultViewport = GraphicsDevice.Viewport;
defaultViewport = new Viewport(0, 0, GraphicsWidth, GraphicsHeight);
if (recalculateFontsAndStyles)
{
@@ -356,6 +358,7 @@ namespace Barotrauma
public void ResetViewPort()
{
GraphicsDevice.Viewport = defaultViewport;
GraphicsDevice.ScissorRectangle = defaultViewport.Bounds;
}
/// <summary>
@@ -378,6 +381,7 @@ namespace Barotrauma
Hyper.ComponentModel.HyperTypeDescriptionProvider.Add(typeof(Hull));
performanceCounterTimer = Stopwatch.StartNew();
ResetIMEWorkaround();
}
/// <summary>
@@ -467,10 +471,11 @@ namespace Barotrauma
LegacySteamUgcTransition.Prepare();
var contentPackageLoadRoutine = ContentPackageManager.Init();
foreach (var progress in contentPackageLoadRoutine)
foreach (var progress in contentPackageLoadRoutine
.Select(p => p.Result).Successes())
{
const float min = 1f, max = 70f;
TitleScreen.LoadState = MathHelper.Lerp(min, max, progress.Value);
TitleScreen.LoadState = MathHelper.Lerp(min, max, progress);
yield return CoroutineStatus.Running;
}
@@ -783,9 +788,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)
{
@@ -800,6 +805,10 @@ namespace Barotrauma
{
GUI.TogglePauseMenu();
}
else if (GameSession?.Campaign is { ShowCampaignUI: true, ForceMapUI: false })
{
GameSession.Campaign.ShowCampaignUI = false;
}
//open the pause menu if not controlling a character OR if the character has no UIs active that can be closed with ESC
else if ((Character.Controlled == null || !itemHudActive())
&& CharacterHealth.OpenHealthWindow == null
@@ -833,7 +842,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)
{
@@ -867,8 +876,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
@@ -1070,11 +1080,9 @@ namespace Barotrauma
}
// Update store stock when saving and quitting in an outpost (normally updated when CampaignMode.End() is called)
if (GameSession?.Campaign is SinglePlayerCampaign spCampaign && Level.IsLoadedFriendlyOutpost && spCampaign.Map?.CurrentLocation != null && spCampaign.CargoManager != null)
if (GameSession?.Campaign is SinglePlayerCampaign spCampaign && Level.IsLoadedFriendlyOutpost)
{
spCampaign.Map.CurrentLocation.AddStock(spCampaign.CargoManager.SoldItems);
spCampaign.CargoManager.ClearSoldItemsProjSpecific();
spCampaign.Map.CurrentLocation.RemoveStock(spCampaign.CargoManager.PurchasedItems);
spCampaign.UpdateStoreStock();
}
SaveUtil.SaveGame(GameSession.SavePath);
@@ -1090,10 +1098,9 @@ namespace Barotrauma
if (GameSession != null)
{
double roundDuration = Timing.TotalTime - GameSession.RoundStartTime;
GameAnalyticsManager.AddProgressionEvent(GameAnalyticsManager.ProgressionStatus.Fail,
GameSession.GameMode?.Preset.Identifier.Value ?? "none",
roundDuration);
GameSession.RoundDuration);
string eventId = "QuitRound:" + (GameSession.GameMode?.Preset.Identifier.Value ?? "none") + ":";
GameAnalyticsManager.AddDesignEvent(eventId + "EventManager:CurrentIntensity", GameSession.EventManager.CurrentIntensity);
foreach (var activeEvent in GameSession.EventManager.ActiveEvents)
@@ -1221,7 +1228,7 @@ namespace Barotrauma
base.OnExiting(sender, args);
}
public void ShowOpenUrlInWebBrowserPrompt(string url, string promptExtensionTag = null)
public static void ShowOpenUrlInWebBrowserPrompt(string url, string promptExtensionTag = null)
{
if (string.IsNullOrEmpty(url)) { return; }
if (GUIMessageBox.VisibleBox?.UserData as string == "verificationprompt") { return; }
@@ -1239,11 +1246,33 @@ namespace Barotrauma
};
msgBox.Buttons[0].OnClicked = (btn, userdata) =>
{
ToolBox.OpenFileWithShell(url);
try
{
ToolBox.OpenFileWithShell(url);
}
catch (Exception e)
{
DebugConsole.ThrowError($"Failed to open the url {url}", e);
}
msgBox.Close();
return true;
};
msgBox.Buttons[1].OnClicked = msgBox.Close;
}
/*
* On some systems, IME input is enabled by default, and being able to set the game to a state
* where it doesn't accept IME input on game launch seems very inconsistent.
* This function quickly cycles through IME input states and is called from couple different places
* to ensure that IME input is disabled properly when it's not needed.
*/
public static void ResetIMEWorkaround()
{
Rectangle rect = new Rectangle(0, 0, GraphicsWidth, GraphicsHeight);
TextInput.SetTextInputRect(rect);
TextInput.StartTextInput();
TextInput.SetTextInputRect(rect);
TextInput.StopTextInput();
}
}
}

View File

@@ -11,7 +11,15 @@ namespace Barotrauma
private List<SoldEntity> SoldEntities { get; } = new List<SoldEntity>();
// The bag slot is intentionally left out since we want to be able to sell items from there
private readonly List<InvSlotType> equipmentSlots = new List<InvSlotType>() { InvSlotType.Head, InvSlotType.InnerClothes, InvSlotType.OuterClothes, InvSlotType.Headset, InvSlotType.Card };
private static readonly HashSet<InvSlotType> equipmentSlots = new HashSet<InvSlotType>()
{
InvSlotType.Head,
InvSlotType.InnerClothes,
InvSlotType.OuterClothes,
InvSlotType.Headset,
InvSlotType.Card,
InvSlotType.HealthInterface
};
public IEnumerable<Item> GetSellableItems(Character character)
{

View File

@@ -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;
@@ -86,6 +87,8 @@ namespace Barotrauma
}
}
private static bool IsOwner(Client client) => client != null && client.IsOwner;
/// <summary>
/// There is a server-side implementation of the method in <see cref="MultiPlayerCampaign"/>
/// </summary>
@@ -97,10 +100,8 @@ namespace Barotrauma
return
GameMain.Client.HasPermission(permissions) ||
GameMain.Client.HasPermission(ClientPermissions.ManageCampaign) ||
GameMain.Client.ConnectedClients.Count == 1 ||
GameMain.Client.IsServerOwner ||
//allow managing if no-one with permissions is alive
GameMain.Client.ConnectedClients.None(c => c.InGame && c.Character is { IsIncapacitated: false, IsDead: false } && (c.IsOwner || c.HasPermission(permissions)));
AnyOneAllowedToManageCampaign(permissions);
}
public static bool AllowedToManageWallets()
@@ -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; }

View File

@@ -694,8 +694,18 @@ namespace Barotrauma
if (ShouldApply(NetFlags.SubList, id, requireUpToDateSave: false))
{
foreach (int ownedSubIndex in ownedSubIndices)
foreach (ushort ownedSubIndex in ownedSubIndices)
{
if (ownedSubIndex >= GameMain.Client.ServerSubmarines.Count)
{
string errorMsg = $"Error in {nameof(MultiPlayerCampaign.ClientRead)}. Owned submarine index was out of bounds. Index: {ownedSubIndex}, submarines: {string.Join(", ", GameMain.Client.ServerSubmarines.Select(s => s.Name))}";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce(
"MultiPlayerCampaign.ClientRead.OwnerSubIndexOutOfBounds" + ownedSubIndex,
GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
continue;
}
SubmarineInfo sub = GameMain.Client.ServerSubmarines[ownedSubIndex];
if (GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, NetLobbyScreen.SubmarineDeliveryData.Owned))
{

View File

@@ -265,8 +265,8 @@ namespace Barotrauma
private IEnumerable<CoroutineStatus> 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");
@@ -407,6 +407,11 @@ namespace Barotrauma
GUI.SetSavingIndicatorState(success);
crewDead = false;
if (success)
{
// Event history must be registered before ending the round or it will be cleared
GameMain.GameSession.EventManager.RegisterEventHistory();
}
GameMain.GameSession.EndRound("", traitorResults, transitionType);
var continueButton = GameMain.GameSession.RoundSummary?.ContinueButton;
RoundSummary roundSummary = null;
@@ -439,7 +444,7 @@ namespace Barotrauma
break;
}
Map.ProgressWorld(transitionType, (float)(Timing.TotalTime - GameMain.GameSession.RoundStartTime));
Map.ProgressWorld(transitionType, GameMain.GameSession.RoundDuration);
var endTransition = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, null,
transitionType == TransitionType.LeaveLocation ? Alignment.BottomCenter : Alignment.Center,
@@ -466,7 +471,6 @@ namespace Barotrauma
if (success)
{
GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine);
GameMain.GameSession.EventManager.RegisterEventHistory();
SaveUtil.SaveGame(GameMain.GameSession.SavePath);
}
else

View File

@@ -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<Segment> ActiveObjectives = new List<Segment>();
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<CoroutineStatus> 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
/// <summary>
/// Create the objective list that holds the objectives (called on start and on resolution change)
/// </summary>
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;
}
/// <summary>
/// Stops content running and adds the active segment to the objective list
/// </summary>
private void StopCurrentContentSegment()
{
if (!ActiveContentSegment.ObjectiveText.IsNullOrEmpty())
{
ActiveObjectives.Add(ActiveContentSegment);
AddToObjectiveList(ActiveContentSegment);
}
ContentRunning = false;
ActiveContentSegment = null;
}
/// <summary>
/// Adds the segment to the objective list
/// </summary>
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;
}
/// <summary>
// Creates and displays a tutorial info box
/// </summary>
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
}
}

View File

@@ -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);

View File

@@ -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)

View File

@@ -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<Pump>() == 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<GeneticMaterial>() 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;
}

View File

@@ -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<T>
{
public readonly Action<T> Callback;
public readonly DateTimeOffset Timeout;
public RequestAction(Action<T> callback, DateTimeOffset timeout)
{
Callback = callback;
Timeout = timeout;
}
}
public readonly struct AfflictionRequest
{
public readonly RequestResult Result;
public readonly ImmutableArray<NetAffliction> Afflictions;
public AfflictionRequest(RequestResult result, ImmutableArray<NetAffliction> afflictions)
{
Result = result;
Afflictions = afflictions;
}
}
public readonly struct PendingRequest
{
public readonly RequestResult Result;
public readonly ImmutableArray<NetCrewMember> CrewMembers;
public PendingRequest(RequestResult result, ImmutableArray<NetCrewMember> 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<T>(Action<T> Callback, DateTimeOffset Timeout);
public readonly record struct AfflictionRequest(RequestResult Result, ImmutableArray<NetAffliction> Afflictions);
public readonly record struct PendingRequest(RequestResult Result, NetCollection<NetCrewMember> CrewMembers);
public readonly record struct CallbackOnlyRequest(RequestResult Result);
public readonly record struct HealRequest(RequestResult Result, HealRequestResult HealResult);
private readonly List<RequestAction<AfflictionRequest>> afflictionRequests = new List<RequestAction<AfflictionRequest>>();
private readonly List<RequestAction<PendingRequest>> pendingHealRequests = new List<RequestAction<PendingRequest>>();
@@ -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<NetAffliction>.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<NetAffliction>.Empty)); });
UpdateQueue(pendingHealRequests, now, onTimeout: callback => { callback(new PendingRequest(RequestResult.Timeout, ImmutableArray<NetCrewMember>.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<NetAffliction>.Empty)); });
UpdateQueue(pendingHealRequests, now, onTimeout: static callback => { callback(new PendingRequest(RequestResult.Timeout, NetCollection<NetCrewMember>.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<CallbackOnlyRequest> callback) { callback(new CallbackOnlyRequest(RequestResult.Timeout)); }
static void CallbackOnlyTimeout(Action<CallbackOnlyRequest> callback) { callback(new CallbackOnlyRequest(RequestResult.Timeout)); }
}
public bool IsAfflictionPending(NetCrewMember character, NetAffliction affliction)
@@ -148,9 +96,9 @@ namespace Barotrauma
private static bool TryDequeue<T>(List<RequestAction<T>> requestQueue, out Action<T> result)
{
RequestAction<T>? 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<CallbackOnlyRequest> onReceived)
{
if (GameMain.IsSingleplayer)
{
AddEverythingToPending();
onReceived(new CallbackOnlyRequest(RequestResult.Success));
OnUpdate?.Invoke();
return;
}
addRequests.Add(new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout()));
ClientSend(null, NetworkHeader.ADD_EVERYTHING_TO_PENDING, DeliveryMethod.Reliable);
}
public void HealAllButtonAction(Action<HealRequest> onReceived)
{
if (GameMain.IsSingleplayer)
@@ -296,8 +258,11 @@ namespace Barotrauma
private void NewAdditonReceived(IReadMessage inc, MessageFlag flag)
{
NetCrewMember crewMember = INetSerializableStruct.Read<NetCrewMember>(inc);
InsertPendingCrewMember(crewMember);
var crewMembers = INetSerializableStruct.Read<NetCollection<NetCrewMember>>(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<NetAffliction>()
};
INetSerializableStruct crewMember = new NetCrewMember(info);
ClientSend(crewMember, NetworkHeader.REQUEST_AFFLICTIONS, DeliveryMethod.Unreliable);
}
@@ -337,17 +298,17 @@ namespace Barotrauma
NetCrewMember crewMember = INetSerializableStruct.Read<NetCrewMember>(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<NetPendingCrew>(inc);
var pendingCrew = INetSerializableStruct.Read<NetCollection<NetCrewMember>>(inc);
if (TryDequeue(pendingHealRequests, out var callback))
{
callback(new PendingRequest(RequestResult.Success, pendingCrew.CrewMembers.ToImmutableArray()));
callback(new PendingRequest(RequestResult.Success, pendingCrew));
}
}

View File

@@ -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<Segment> activeObjectives = new List<Segment>();
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();
}
/// <summary>
/// Create the objective list that holds the objectives (called on start and on resolution change)
/// </summary>
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], useExistingIndex: true);
}
screenSettings = new ScreenSettings(new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight), GUI.Scale, GameSettings.CurrentConfig.Graphics.DisplayMode);
}
/// <summary>
/// Stops content running and adds the active segment to the objective list
/// </summary>
private static void StopCurrentContentSegment()
{
if (!ActiveContentSegment.ObjectiveText.IsNullOrEmpty())
{
activeObjectives.Add(ActiveContentSegment);
AddToObjectiveList(ActiveContentSegment);
}
ContentRunning = false;
ActiveContentSegment = null;
}
/// <summary>
/// Adds the segment to the objective list
/// </summary>
private static void AddToObjectiveList(Segment segment, bool connectExisting = false, bool useExistingIndex = 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 childIndex = useExistingIndex ? activeObjectives.IndexOf(segment) :
activeObjectives.IndexOf(parentSegment) + activeObjectives.Count(s => s.ParentId == segment.ParentId);
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;
}
/// <summary>
// Creates and displays a tutorial info box
/// </summary>
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
}

View File

@@ -1,6 +1,7 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
@@ -117,7 +118,7 @@ namespace Barotrauma
private void UpdateBar()
{
double elapsedTime = (DateTime.Now - startTime).TotalSeconds;
if (msgBox != null && !msgBox.Closed && GUIMessageBox.MessageBoxes.Contains(msgBox))
if (msgBox is { Closed: false } && GUIMessageBox.MessageBoxes.Contains(msgBox))
{
if (msgBox.FindChild(TimerData, true) is GUIProgressBar bar)
{
@@ -129,7 +130,7 @@ namespace Barotrauma
int second = (int)Math.Ceiling(elapsedTime);
if (second > lastSecond)
{
if (msgBox != null && !msgBox.Closed)
if (msgBox is { Closed: false })
{
SoundPlayer.PlayUISound(GUISoundType.PopupMenu);
}
@@ -137,6 +138,19 @@ namespace Barotrauma
}
}
private static void CloseLingeringPopups()
{
foreach (GUIComponent box in GUIMessageBox.MessageBoxes.ToImmutableArray())
{
if (box is not GUIMessageBox msgBox) { continue; }
if (msgBox.UserData is PromptData or ResultData)
{
msgBox.Close();
}
}
}
public static void ClientRead(IReadMessage inc)
{
ReadyCheckState state = (ReadyCheckState)inc.ReadByte();
@@ -154,6 +168,8 @@ namespace Barotrauma
switch (state)
{
case ReadyCheckState.Start:
CloseLingeringPopups();
bool isOwn = false;
byte authorId = 0;
@@ -175,8 +191,8 @@ namespace Barotrauma
clients.Add(inc.ReadByte());
}
ReadyCheck rCheck = new ReadyCheck(clients,
DateTimeOffset.FromUnixTimeSeconds(startTime).LocalDateTime,
ReadyCheck rCheck = new ReadyCheck(clients,
DateTimeOffset.FromUnixTimeSeconds(startTime).LocalDateTime,
DateTimeOffset.FromUnixTimeSeconds(endTime).LocalDateTime);
crewManager.ActiveReadyCheck = rCheck;
@@ -224,7 +240,7 @@ namespace Barotrauma
if (IsFinished) { return; }
IsFinished = true;
int readyCount = Clients.Count(pair => pair.Value == ReadyStatus.Yes);
int readyCount = Clients.Count(static pair => pair.Value == ReadyStatus.Yes);
int totalCount = Clients.Count;
GameMain.Client.AddChatMessage(ChatMessage.Create(string.Empty, readyCheckStatus(readyCount, totalCount).Value, ChatMessageType.Server, null));
}
@@ -238,31 +254,29 @@ namespace Barotrauma
if (resultsBox == null || resultsBox.Closed || !GUIMessageBox.MessageBoxes.Contains(resultsBox)) { return; }
if (resultsBox.Content.FindChild(UserListData) is GUIListBox userList)
if (resultsBox.Content.FindChild(UserListData) is not GUIListBox userList) { return; }
// for some reason FindChild doesn't work here?
foreach (GUIComponent child in userList.Content.Children)
{
// for some reason FindChild doesn't work here?
foreach (GUIComponent child in userList.Content.Children)
if (child.UserData is not byte b || b != id) { continue; }
if (child.GetChild<GUILayoutGroup>().FindChild(ReadySpriteData) is not GUIImage image) { continue; }
string style;
switch (status)
{
if (!(child.UserData is byte b) || b != id) { continue; }
if (child.GetChild<GUILayoutGroup>().FindChild(ReadySpriteData) is GUIImage image)
{
string style;
switch (status)
{
case ReadyStatus.Yes:
style = "MissionCompletedIcon";
break;
case ReadyStatus.No:
style = "MissionFailedIcon";
break;
default:
return;
}
image.ApplyStyle(GUIStyle.GetComponentStyle(style));
}
case ReadyStatus.Yes:
style = "MissionCompletedIcon";
break;
case ReadyStatus.No:
style = "MissionFailedIcon";
break;
default:
return;
}
image.ApplyStyle(GUIStyle.GetComponentStyle(style));
}
}

View File

@@ -506,7 +506,7 @@ namespace Barotrauma
private LocalizedString GetHeaderText(bool gameOver, CampaignMode.TransitionType transitionType)
{
string locationName = Submarine.MainSub.AtEndExit ? endLocation?.Name : startLocation?.Name;
string locationName = Submarine.MainSub is { AtEndExit: true } ? endLocation?.Name : startLocation?.Name;
string textTag;
if (gameOver)

View File

@@ -774,7 +774,6 @@ namespace Barotrauma
}
else
{
bool isEquippable = item.AllowedSlots.Any(s => s != InvSlotType.Any);
var selectedContainer = character.SelectedItem?.GetComponent<ItemContainer>();
if (selectedContainer != null &&
@@ -802,8 +801,7 @@ namespace Barotrauma
}
else if (character.HeldItems.Any(i =>
i.OwnInventory != null &&
/*disallow putting into equipped item if the item is equippable (equip as the quick action instead)*/
((i.OwnInventory.CanBePut(item) && (allowInventorySwap || !isEquippable)) || (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;
}
@@ -973,11 +971,11 @@ namespace Barotrauma
//don't allow swapping if we're moving items into an item with 1 slot holding a stack of items
//(in that case, the quick action should just fill up the stack)
bool disallowSwapping =
heldItem.OwnInventory.Capacity == 1 &&
(heldItem.OwnInventory.Capacity == 1 || heldItem.OwnInventory.Container.HasSubContainers) &&
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++)
@@ -1133,7 +1131,7 @@ namespace Barotrauma
GUI.DrawRectangle(spriteBatch, inventoryArea, new Color(30,30,30,100), isFilled: true);
var lockIcon = GUIStyle.GetComponentStyle("LockIcon")?.GetDefaultSprite();
lockIcon?.Draw(spriteBatch, inventoryArea.Center.ToVector2(), scale: Math.Min(inventoryArea.Height / lockIcon.size.Y * 0.7f, 1.0f));
if (inventoryArea.Contains(PlayerInput.MousePosition))
if (inventoryArea.Contains(PlayerInput.MousePosition) && character.LockHands)
{
GUIComponent.DrawToolTip(spriteBatch, TextManager.Get("handcuffed"), new Rectangle(inventoryArea.Center - new Point(inventoryArea.Height / 2), new Point(inventoryArea.Height)));
}

View File

@@ -25,14 +25,40 @@ namespace Barotrauma.Items.Components
foreach (Node node in nodes)
{
GameMain.ParticleManager.CreateParticle("swirlysmoke", node.WorldPosition, Vector2.Zero);
if (node.ParentIndex > -1)
{
CreateParticlesBetween(nodes[node.ParentIndex].WorldPosition, node.WorldPosition);
}
}
foreach (var character in charactersInRange)
{
CreateParticlesBetween(character.character.WorldPosition, character.node.WorldPosition);
}
static void CreateParticlesBetween(Vector2 start, Vector2 end)
{
const float ParticleInterval = 50.0f;
Vector2 diff = end - start;
float dist = diff.Length();
Vector2 normalizedDiff = MathUtils.NearlyEqual(dist, 0.0f) ? Vector2.Zero : diff / dist;
for (float x = 0.0f; x < dist; x += ParticleInterval)
{
var spark = GameMain.ParticleManager.CreateParticle("ElectricShock", start + normalizedDiff * x, Vector2.Zero);
if (spark != null)
{
spark.Size *= 0.3f;
}
}
}
}
public void DrawElectricity(SpriteBatch spriteBatch)
{
if (timer <= 0.0f) { return; }
for (int i = 0; i < nodes.Count; i++)
{
if (nodes[i].Length <= 1.0f) continue;
if (nodes[i].Length <= 1.0f) { continue; }
var node = nodes[i];
electricitySprite.Draw(spriteBatch,
(i + frameOffset) % electricitySprite.FrameCount,
@@ -46,10 +72,16 @@ namespace Barotrauma.Items.Components
if (GameMain.DebugDraw)
{
for (int i = 0; i < nodes.Count; i++)
for (int i = 1; i < nodes.Count; i++)
{
if (nodes[i].Length <= 1.0f) continue;
GUI.DrawRectangle(spriteBatch, new Vector2(nodes[i].WorldPosition.X, -nodes[i].WorldPosition.Y), Vector2.One * 5, Color.LightCyan, isFilled: true);
GUI.DrawLine(spriteBatch,
new Vector2(nodes[i].WorldPosition.X, -nodes[i].WorldPosition.Y),
new Vector2(nodes[nodes[i].ParentIndex].WorldPosition.X, -nodes[nodes[i].ParentIndex].WorldPosition.Y),
Color.LightCyan,
width: 3);
if (nodes[i].Length <= 1.0f) { continue; }
GUI.DrawRectangle(spriteBatch, new Vector2(nodes[i].WorldPosition.X, -nodes[i].WorldPosition.Y), Vector2.One * 10, Color.LightCyan, isFilled: true);
}
}
}

View File

@@ -114,8 +114,8 @@ namespace Barotrauma.Items.Components
{
if (chargeSound != null)
{
chargeSoundChannel = SoundPlayer.PlaySound(chargeSound.Sound, item.WorldPosition, chargeSound.Volume, chargeSound.Range, ignoreMuffling: chargeSound.IgnoreMuffling);
if (chargeSoundChannel != null) chargeSoundChannel.Looping = true;
chargeSoundChannel = SoundPlayer.PlaySound(chargeSound.Sound, item.WorldPosition, chargeSound.Volume, chargeSound.Range, ignoreMuffling: chargeSound.IgnoreMuffling, freqMult: chargeSound.GetRandomFrequencyMultiplier());
if (chargeSoundChannel != null) { chargeSoundChannel.Looping = true; }
}
}
else if (chargeSoundChannel != null)

View File

@@ -224,68 +224,59 @@ namespace Barotrauma.Items.Components
if (character == null) { return false; }
if (character == Character.Controlled)
{
if (targetSections.Count == 0) { return false; }
Spray(deltaTime);
Spray(character, deltaTime, applyColors: targetSections.Count > 0);
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);

View File

@@ -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<ItemContainer>())
{
if (ic.hideItems) continue;
if (ic.hideItems) { continue; }
ic.DrawContainedItems(spriteBatch, containedSpriteDepth);
}

View File

@@ -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;

View File

@@ -14,7 +14,10 @@ namespace Barotrauma.Items.Components
private GUIFrame selectedItemFrame;
private GUIFrame selectedItemReqsFrame;
private GUITextBlock amountTextMin, amountTextMax;
private GUIScrollBar amountInput;
public GUIButton ActivateButton
{
get { return activateButton; }
@@ -160,14 +163,46 @@ namespace Barotrauma.Items.Components
new GUICustomComponent(new RectTransform(Vector2.One, inputInventoryHolder.RectTransform), DrawInputOverLay) { CanBeFocused = false };
// === ACTIVATE BUTTON === //
var buttonFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 0.8f), inputArea.RectTransform), childAnchor: Anchor.CenterRight);
activateButton = new GUIButton(new RectTransform(new Vector2(1f, 0.6f), buttonFrame.RectTransform),
TextManager.Get(CreateButtonText), style: "DeviceButtonFixedSize")
var buttonFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 0.9f), inputArea.RectTransform))
{
Stretch = true,
RelativeSpacing = 0.05f
};
var amountInputHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.4f), buttonFrame.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft)
{
Stretch = true
};
amountTextMin = new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), amountInputHolder.RectTransform), "1", textAlignment: Alignment.Center);
amountInput = new GUIScrollBar(new RectTransform(new Vector2(0.7f, 1.0f), amountInputHolder.RectTransform), barSize: 0.1f, style: "GUISlider")
{
OnMoved = (GUIScrollBar scrollBar, float barScroll) =>
{
scrollBar.Step = 1.0f / Math.Max(scrollBar.Range.Y - 1, 1);
AmountToFabricate = (int)MathF.Round(scrollBar.BarScrollValue);
RefreshActivateButtonText();
if (GameMain.Client != null)
{
item.CreateClientEvent(this);
}
return true;
}
};
amountTextMax = new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), amountInputHolder.RectTransform), "1", textAlignment: Alignment.Center);
activateButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.6f), buttonFrame.RectTransform),
TextManager.Get(CreateButtonText), style: "DeviceButton")
{
OnClicked = StartButtonClicked,
UserData = selectedItem,
Enabled = false
};
};
//spacing
new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), buttonFrame.RectTransform), style: null);
}
else
{
@@ -192,6 +227,21 @@ namespace Barotrauma.Items.Components
CreateRecipes();
}
private void RefreshActivateButtonText()
{
if (amountInput == null)
{
activateButton.Text = TextManager.Get(IsActive ? "FabricatorCancel" : CreateButtonText);
}
else
{
activateButton.Text =
IsActive ?
$"{TextManager.Get("FabricatorCancel")} ({amountRemaining})" :
$"{TextManager.Get(CreateButtonText)} ({AmountToFabricate})";
}
}
partial void CreateRecipes()
{
itemList.Content.RectTransform.ClearChildren();
@@ -247,7 +297,7 @@ namespace Barotrauma.Items.Components
outputContainer.Inventory.RectTransform = outputInventoryHolder.RectTransform;
}
private LocalizedString GetRecipeNameAndAmount(FabricationRecipe fabricationRecipe)
private static LocalizedString GetRecipeNameAndAmount(FabricationRecipe fabricationRecipe)
{
if (fabricationRecipe == null) { return ""; }
if (fabricationRecipe.Amount > 1)
@@ -269,7 +319,7 @@ namespace Barotrauma.Items.Components
partial void SelectProjSpecific(Character character)
{
var nonItems = itemList.Content.Children.Where(c => !(c.UserData is FabricationRecipe)).ToList();
var nonItems = itemList.Content.Children.Where(c => c.UserData is not FabricationRecipe).ToList();
nonItems.ForEach(i => itemList.Content.RemoveChild(i));
itemList.Content.RectTransform.SortChildren((c1, c2) =>
@@ -277,17 +327,20 @@ namespace Barotrauma.Items.Components
var item1 = c1.GUIComponent.UserData as FabricationRecipe;
var item2 = c2.GUIComponent.UserData as FabricationRecipe;
int itemPlacement1 = FabricationDegreeOfSuccess(character, item1.RequiredSkills) >= 0.5f ? 0 : -1;
int itemPlacement2 = FabricationDegreeOfSuccess(character, item2.RequiredSkills) >= 0.5f ? 0 : -1;
itemPlacement1 += item1.RequiresRecipe && !character.HasRecipeForItem(item1.TargetItem.Identifier) ? -2 : 0;
itemPlacement2 += item2.RequiresRecipe && !character.HasRecipeForItem(item2.TargetItem.Identifier) ? -2 : 0;
int itemPlacement1 = calculatePlacement(item1);
int itemPlacement2 = calculatePlacement(item2);
if (itemPlacement1 != itemPlacement2)
{
return itemPlacement1 > itemPlacement2 ? -1 : 1;
}
int calculatePlacement(FabricationRecipe recipe)
{
int placement = FabricationDegreeOfSuccess(character, recipe.RequiredSkills) >= 0.5f ? 0 : -1;
placement += recipe.RequiresRecipe && !AnyOneHasRecipeForItem(character, recipe.TargetItem) ? -2 : 0;
return placement;
}
return string.Compare(item1.DisplayName.Value, item2.DisplayName.Value);
});
@@ -322,7 +375,9 @@ namespace Barotrauma.Items.Components
AutoScaleHorizontal = true,
CanBeFocused = false
};
var firstRequiresRecipe = itemList.Content.Children.FirstOrDefault(c => c.UserData is FabricationRecipe fabricableItem && (fabricableItem.RequiresRecipe && !character.HasRecipeForItem(fabricableItem.TargetItem.Identifier)));
var firstRequiresRecipe = itemList.Content.Children.FirstOrDefault(c =>
c.UserData is FabricationRecipe fabricableItem &&
fabricableItem.RequiresRecipe && !AnyOneHasRecipeForItem(character, fabricableItem.TargetItem));
if (firstRequiresRecipe != null)
{
requiresRecipeText.RectTransform.RepositionChildInHierarchy(itemList.Content.RectTransform.GetChildIndex(firstRequiresRecipe.RectTransform));
@@ -567,7 +622,7 @@ namespace Barotrauma.Items.Components
bool recipeVisible = false;
foreach (GUIComponent child in itemList.Content.Children.Reverse())
{
if (!(child.UserData is FabricationRecipe recipe))
if (child.UserData is not FabricationRecipe recipe)
{
if (child.Enabled)
{
@@ -598,9 +653,23 @@ namespace Barotrauma.Items.Components
{
this.selectedItem = selectedItem;
int max = Math.Max(selectedItem.TargetItem.MaxStackSize / selectedItem.Amount, 1);
if (amountInput != null)
{
float prevBarScroll = amountInput.BarScroll;
amountInput.Range = new Vector2(1, max);
amountInput.BarScroll = prevBarScroll;
amountTextMax.Text = max.ToString();
amountInput.Enabled = amountTextMax.Enabled = max > 1;
AmountToFabricate = Math.Min((int)amountInput.BarScrollValue, max);
}
RefreshActivateButtonText();
selectedItemFrame.ClearChildren();
selectedItemReqsFrame.ClearChildren();
var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.9f), selectedItemFrame.RectTransform, Anchor.Center)) { RelativeSpacing = 0.03f };
var paddedReqFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.9f), selectedItemReqsFrame.RectTransform, Anchor.Center)) { RelativeSpacing = 0.03f };
@@ -734,7 +803,9 @@ namespace Barotrauma.Items.Components
outputSlot.Flash(GUIStyle.Red);
return false;
}
amountRemaining = AmountToFabricate;
if (GameMain.Client != null)
{
pendingFabricatedItem = fabricatedItem != null ? null : selectedItem;
@@ -777,7 +848,7 @@ namespace Barotrauma.Items.Components
{
foreach (GUIComponent child in itemList.Content.Children)
{
if (!(child.UserData is FabricationRecipe recipe)) { continue; }
if (child.UserData is not FabricationRecipe recipe) { continue; }
if (recipe != selectedItem &&
(child.Rect.Y > itemList.Rect.Bottom || child.Rect.Bottom < itemList.Rect.Y))
@@ -811,11 +882,14 @@ namespace Barotrauma.Items.Components
{
uint recipeHash = pendingFabricatedItem?.RecipeHash ?? 0;
msg.WriteUInt32(recipeHash);
msg.WriteRangedInteger(AmountToFabricate, 1, MaxAmountToFabricate);
}
public void ClientEventRead(IReadMessage msg, float sendingTime)
{
FabricatorState newState = (FabricatorState)msg.ReadByte();
int amountToFabricate = msg.ReadRangedInteger(0, MaxAmountToFabricate);
int amountRemaining = msg.ReadRangedInteger(0, MaxAmountToFabricate);
float newTimeUntilReady = msg.ReadSingle();
uint recipeHash = msg.ReadUInt32();
UInt16 userID = msg.ReadUInt16();
@@ -828,6 +902,8 @@ namespace Barotrauma.Items.Components
}
State = newState;
this.amountToFabricate = amountToFabricate;
this.amountRemaining = amountRemaining;
if (newState == FabricatorState.Stopped || recipeHash == 0)
{
CancelFabricating();

View File

@@ -144,6 +144,9 @@ namespace Barotrauma.Items.Components
partial class MiniMap : Powered
{
private Dictionary<Hull, HullData> hullDatas;
private DateTime resetDataTime;
private GUIFrame submarineContainer;
private GUIFrame? hullInfoFrame;
@@ -226,6 +229,8 @@ namespace Barotrauma.Items.Components
partial void InitProjSpecific()
{
hullDatas = new Dictionary<Hull, HullData>();
SetDefaultMode();
noPowerTip = TextManager.Get("SteeringNoPowerTip");
@@ -408,6 +413,8 @@ namespace Barotrauma.Items.Components
var wire = it.GetComponent<Wire>();
if (wire != null && wire.Connections.Any(c => c != null)) { return false; }
if (it.Container?.GetComponent<ItemContainer>() is { DrawInventory: false }) { return false; }
if (it.HasTag("traitormissionitem")) { return false; }
return true;
@@ -549,6 +556,34 @@ namespace Barotrauma.Items.Components
CreateHUD();
}
//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)
{
foreach (HullData hullData in hullDatas.Values)
{
if (!hullData.Distort)
{
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);
}
if (cardRefreshTimer > cardRefreshDelay)
{
if (item.Submarine is { } sub)
{
UpdateIDCards(sub);
}
cardRefreshTimer = 0;
}
else
{
cardRefreshTimer += deltaTime;
}
if (scissorComponent != null)
{
if (PlayerInput.PrimaryMouseButtonDown() && currentMode != MiniMapMode.HullStatus)
@@ -952,11 +987,8 @@ namespace Barotrauma.Items.Components
component.Color = borderComponent.OutlineColor = NoPowerColor;
}
if (Voltage < MinVoltage) { continue; }
if (!component.Visible) { continue; }
if (!(entity is Hull hull)) { continue; }
if (entity is not Hull hull) { continue; }
if (!submarineContainer.Rect.Contains(component.Rect))
{
if (hull.Submarine.Info.Type != SubmarineType.Player)
@@ -966,6 +998,8 @@ namespace Barotrauma.Items.Components
}
}
if (Voltage < MinVoltage) { continue; }
hullDatas.TryGetValue(hull, out HullData? hullData);
if (hullData is null)
{
@@ -1119,7 +1153,7 @@ namespace Barotrauma.Items.Components
if (it.GetComponent<PowerContainer>() is { } battery)
{
int batteryCapacity = (int)(battery.Charge / battery.Capacity * 100f);
int batteryCapacity = (int)(battery.Charge / battery.GetCapacity() * 100f);
line2 = TextManager.GetWithVariable("statusmonitor.battery.tooltip", "[amount]", batteryCapacity.ToString());
}
else if (it.GetComponent<PowerTransfer>() is { } powerTransfer)
@@ -1734,6 +1768,67 @@ namespace Barotrauma.Items.Components
return new MiniMapHullData(scaledPolygon, worldRect, parentRect.Size, snappedRectangles, hullRefs.ToImmutableArray());
}
public override void ReceiveSignal(Signal signal, Connection connection)
{
Item source = signal.source;
if (source == null || source.CurrentHull == null) { return; }
Hull sourceHull = source.CurrentHull;
if (!hullDatas.TryGetValue(sourceHull, out HullData? hullData))
{
hullData = new HullData();
hullDatas.Add(sourceHull, hullData);
}
if (hullData.Distort) { return; }
switch (connection.Name)
{
case "water_data_in":
//cheating a bit because water detectors don't actually send the water level
bool fromWaterDetector = source.GetComponent<WaterDetector>() != null;
hullData.ReceivedWaterAmount = null;
hullData.LastWaterDataTime = Timing.TotalTime;
if (fromWaterDetector)
{
hullData.ReceivedWaterAmount = WaterDetector.GetWaterPercentage(sourceHull);
}
foreach (var linked in sourceHull.linkedTo)
{
if (linked is not Hull linkedHull) { continue; }
if (!hullDatas.TryGetValue(linkedHull, out HullData? linkedHullData))
{
linkedHullData = new HullData();
hullDatas.Add(linkedHull, linkedHullData);
}
linkedHullData.ReceivedWaterAmount = null;
if (fromWaterDetector)
{
linkedHullData.ReceivedWaterAmount = WaterDetector.GetWaterPercentage(linkedHull);
}
}
break;
case "oxygen_data_in":
if (!float.TryParse(signal.value, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float oxy))
{
oxy = Rand.Range(0.0f, 100.0f);
}
hullData.ReceivedOxygenAmount = oxy;
hullData.LastOxygenDataTime = Timing.TotalTime;
foreach (var linked in sourceHull.linkedTo)
{
if (linked is not Hull linkedHull) { continue; }
if (!hullDatas.TryGetValue(linkedHull, out HullData? linkedHullData))
{
linkedHullData = new HullData();
hullDatas.Add(linkedHull, linkedHullData);
}
linkedHullData.ReceivedOxygenAmount = oxy;
}
break;
}
}
protected override void RemoveComponentSpecific()
{
base.RemoveComponentSpecific();

View File

@@ -29,7 +29,11 @@ namespace Barotrauma.Items.Components
private Sprite tempRangeIndicator;
private Sprite graphLine;
//private GUIFrame graph;
private GUICustomComponent graph;
private GUIFrame inventoryWindow;
private GUILayoutGroup buttonArea;
private GUIFrame infographic;
private Color optimalRangeColor = new Color(74,238,104,255);
private Color offRangeColor = Color.Orange;
@@ -66,6 +70,8 @@ namespace Barotrauma.Items.Components
};
public override bool RecreateGUIOnResolutionChange => true;
public bool TriggerInfographic { get; set; }
partial void InitProjSpecific(ContentXElement element)
{
@@ -122,7 +128,7 @@ namespace Barotrauma.Items.Components
//left column
//----------------------------------------------------------
GUIFrame inventoryWindow = new GUIFrame(new RectTransform(new Vector2(0.1f, 0.75f), GuiFrame.RectTransform, Anchor.TopLeft, Pivot.TopRight)
inventoryWindow = new GUIFrame(new RectTransform(new Vector2(0.1f, 0.75f), GuiFrame.RectTransform, Anchor.TopLeft, Pivot.TopRight)
{
MinSize = new Point(85, 220),
RelativeOffset = new Vector2(-0.02f, 0)
@@ -255,7 +261,7 @@ namespace Barotrauma.Items.Components
};
TurbineOutputScrollBar.Frame.UserData = UIHighlightAction.ElementId.TurbineOutputSlider;
var buttonArea = new GUILayoutGroup(new RectTransform(new Vector2(1, 0.2f), columnLeft.RectTransform))
buttonArea = new GUILayoutGroup(new RectTransform(new Vector2(1, 0.2f), columnLeft.RectTransform))
{
Stretch = true,
RelativeSpacing = 0.02f
@@ -390,7 +396,9 @@ namespace Barotrauma.Items.Components
ToolTip = TextManager.Get("reactor.temperatureboostup"),
OnClicked = (_, __) =>
{
applyTemperatureBoost(TemperatureBoostAmount, temperatureBoostSoundUp);
unsentChanges = true;
sendUpdateTimer = 0.0f;
ApplyTemperatureBoost(TemperatureBoostAmount);
return true;
}
};
@@ -401,25 +409,13 @@ namespace Barotrauma.Items.Components
ToolTip = TextManager.Get("reactor.temperatureboostdown"),
OnClicked = (_, __) =>
{
applyTemperatureBoost(-TemperatureBoostAmount, temperatureBoostSoundDown);
unsentChanges = true;
sendUpdateTimer = 0.0f;
ApplyTemperatureBoost(-TemperatureBoostAmount);
return true;
}
};
void applyTemperatureBoost(float amount, RoundSound sound)
{
temperatureBoost = amount;
if (sound != null)
{
SoundPlayer.PlaySound(
sound.Sound,
item.WorldPosition,
sound.Volume,
sound.Range,
hullGuess: item.CurrentHull);
}
}
var graphArea = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 1.0f), bottomRightArea.RectTransform))
{
Stretch = true,
@@ -436,8 +432,8 @@ namespace Barotrauma.Items.Components
LocalizedString kW = TextManager.Get("kilowatt");
loadText.TextGetter += () => $"{loadStr.Replace("[kw]", ((int)Load).ToString())} {kW}";
var graph = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), graphArea.RectTransform), style: "InnerFrameRed");
new GUICustomComponent(new RectTransform(new Vector2(0.9f, 0.98f), graph.RectTransform, Anchor.Center), DrawGraph, null);
var graphFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), graphArea.RectTransform), style: "InnerFrameRed");
graph = new GUICustomComponent(new RectTransform(new Vector2(0.9f, 0.98f), graphFrame.RectTransform, Anchor.Center), DrawGraph, null);
var outputText = new GUITextBlock(new RectTransform(relativeTextSize, graphArea.RectTransform),
"Output", textColor: outputColor, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft)
@@ -448,6 +444,41 @@ namespace Barotrauma.Items.Components
outputText.TextGetter += () => $"{outputStr.Replace("[kw]", ((int)-currPowerConsumption).ToString())} {kW}";
InitInventoryUI();
// Infographic overlay ---------------------
int buttonHeight = (int)(GUIStyle.ItemFrameMargin.Y * 0.4f);
var helpButtonRt = new RectTransform(new Point(buttonHeight), parent: GuiFrame.RectTransform, anchor: Anchor.TopRight)
{
AbsoluteOffset = new Point(buttonHeight / 4),
MinSize = new Point(buttonHeight)
};
new GUIButton(helpButtonRt, "", style: "HelpIcon")
{
OnClicked = (_, _) =>
{
CreateInfrographic();
return true;
}
};
}
private void ApplyTemperatureBoost(float amount)
{
if (Math.Abs(temperatureBoost) <= TemperatureBoostAmount * 0.9f &&
Math.Abs(amount) > TemperatureBoostAmount * 0.9f)
{
var sound = amount > 0 ? temperatureBoostSoundUp : temperatureBoostSoundDown;
if (sound != null)
{
SoundPlayer.PlaySound(
sound.Sound,
item.WorldPosition,
sound.Volume,
sound.Range,
freqMult: sound.GetRandomFrequencyMultiplier(),
hullGuess: item.CurrentHull);
}
}
temperatureBoost = amount;
}
private void InitInventoryUI()
@@ -469,7 +500,6 @@ namespace Barotrauma.Items.Components
InitInventoryUI();
}
private void DrawTempMeter(SpriteBatch spriteBatch, GUICustomComponent container)
{
Vector2 meterPos = new Vector2(container.Rect.X, container.Rect.Y);
@@ -518,7 +548,6 @@ namespace Barotrauma.Items.Components
DrawGraph(loadGraph, spriteBatch, graphRect, Math.Max(10000.0f, maxLoad), xOffset, loadColor);
}
private void UpdateGraph(float deltaTime)
{
graphTimer += deltaTime * 1000.0f;
@@ -645,6 +674,12 @@ namespace Barotrauma.Items.Components
}
}
}
if (TriggerInfographic)
{
CreateInfrographic();
TriggerInfographic = false;
}
}
private void DrawMeter(SpriteBatch spriteBatch, Rectangle rect, Sprite meterSprite, float value, Vector2 range, Vector2 optimalRange, Vector2 allowedRange)
@@ -760,7 +795,96 @@ namespace Barotrauma.Items.Components
spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect;
spriteBatch.Begin(SpriteSortMode.Deferred);
}
private enum InfographicArrowStyle { Straight, Curved };
private void CreateInfrographic()
{
if (infographic != null) { return; }
var dimColor = Color.Lerp(Color.Black, Color.TransparentBlack, 0.25f);
// Dim reactor interface
infographic = new GUIFrame(new RectTransform(Vector2.One, GuiFrame.RectTransform))
{
CanBeFocused = false,
Color = dimColor
};
// Dim inventory window
new GUIFrame(new RectTransform(inventoryWindow.Rect.Size, infographic.RectTransform) { AbsoluteOffset = inventoryWindow.Rect.Location - GuiFrame.Rect.Location}, color: dimColor)
{
CanBeFocused = false
};
int arrowSize = (int)(70 * GUI.Scale);
var arrows = new Dictionary<string, GUIImage>()
{
{ "fuelslots", CreateArrow(InfographicArrowStyle.Curved, inventoryWindow, Anchor.TopLeft, Pivot.TopRight, SpriteEffects.FlipVertically) },
{ "temperature", CreateArrow(InfographicArrowStyle.Straight, temperatureBoostDownButton, Anchor.Center, Pivot.Center) },
{ "automaticcontrol", CreateArrow(InfographicArrowStyle.Curved, AutoTempSwitch, Anchor.TopRight, Pivot.BottomRight, rotationDegrees: 90f) },
{ "power", CreateArrow(InfographicArrowStyle.Straight, PowerButton, Anchor.BottomCenter, Pivot.TopCenter) }
};
CreateArrow(InfographicArrowStyle.Straight, FissionRateScrollBar, Anchor.Center, Pivot.Center);
CreateArrow(InfographicArrowStyle.Straight, TurbineOutputScrollBar, Anchor.Center, Pivot.Center);
CreateArrow(InfographicArrowStyle.Straight, graph, Anchor.TopLeft, Pivot.TopLeft, SpriteEffects.FlipHorizontally, additionalOffset: new Point(arrowSize / 2, 0));
CreateArrow(InfographicArrowStyle.Straight, graph, Anchor.BottomLeft, Pivot.BottomLeft, SpriteEffects.FlipHorizontally | SpriteEffects.FlipVertically, additionalOffset: new Point(arrowSize / 2, 0));
new GUICustomComponent(new RectTransform(Vector2.One, infographic.RectTransform),
onDraw: (sb, c) =>
{
DrawToolTip("fuelslots", Anchor.TopLeft, Pivot.BottomCenter, arrows["fuelslots"]);
DrawToolTip("fissionrate", Anchor.TopCenter, Pivot.TopCenter, buttonArea);
DrawToolTip("temperature", Anchor.BottomLeft, Pivot.TopLeft, arrows["temperature"]);
DrawToolTip("automaticcontrol", Anchor.TopLeft, Pivot.CenterRight, arrows["automaticcontrol"]);
DrawToolTip("power", Anchor.BottomCenter, Pivot.TopCenter, arrows["power"]);
DrawToolTip("load", Anchor.CenterLeft, Pivot.CenterLeft, graph);
void DrawToolTip(string textTag, Anchor anchor, Pivot pivot, GUIComponent targetComponent)
{
GUIComponent.DrawToolTip(sb,
TextManager.Get($"infographic.reactor.{textTag}"),
targetComponent.Rect,
anchor: anchor,
pivot: pivot);
}
})
{
CanBeFocused = false
};
var closeButtonRt = new RectTransform(new Point(200, 50).Multiply(GUI.Scale), infographic.RectTransform, Anchor.TopRight, Pivot.BottomRight)
{
AbsoluteOffset = new Point(0, -50).Multiply(GUI.Scale)
};
new GUIButton(closeButtonRt, TextManager.Get("close"))
{
OnClicked = (_, _) =>
{
CloseInfographic(Character.Controlled);
return true;
}
};
item.OnDeselect += CloseInfographic;
GUIImage CreateArrow(InfographicArrowStyle arrowStyle, GUIComponent parent, Anchor anchor, Pivot pivot, SpriteEffects spriteEffects = SpriteEffects.None, float rotationDegrees = 0f, Point? additionalOffset = null)
{
Point offset = (additionalOffset ?? Point.Zero) + RectTransform.CalculateAnchorPoint(anchor, parent.Rect) - GuiFrame.Rect.Location;
var rt = new RectTransform(new Point(arrowSize), infographic.RectTransform, pivot: pivot)
{
AbsoluteOffset = offset
};
string style = arrowStyle == InfographicArrowStyle.Straight ? "InfographicArrow" : "InfographicArrowCurved";
return new GUIImage(rt, style)
{
Rotation = MathHelper.ToRadians(rotationDegrees),
SpriteEffects = spriteEffects
};
}
void CloseInfographic(Character character)
{
if (character != Character.Controlled) { return; }
GuiFrame.RemoveChild(infographic);
infographic = null;
item.OnDeselect -= CloseInfographic;
}
}
protected override void RemoveComponentSpecific()
{
base.RemoveComponentSpecific();
@@ -780,6 +904,7 @@ namespace Barotrauma.Items.Components
msg.WriteBoolean(PowerOn);
msg.WriteRangedSingle(TargetFissionRate, 0.0f, 100.0f, 8);
msg.WriteRangedSingle(TargetTurbineOutput, 0.0f, 100.0f, 8);
msg.WriteRangedSingle(temperatureBoost, -TemperatureBoostAmount, TemperatureBoostAmount, 8);
correctionTimer = CorrectionDelay;
}
@@ -788,7 +913,7 @@ namespace Barotrauma.Items.Components
{
if (correctionTimer > 0.0f)
{
StartDelayedCorrection(msg.ExtractBits(1 + 1 + 8 + 8 + 8 + 8), sendingTime);
StartDelayedCorrection(msg.ExtractBits(1 + 1 + 8 + 8 + 8 + 8 + 8), sendingTime);
return;
}
@@ -798,6 +923,7 @@ namespace Barotrauma.Items.Components
TargetFissionRate = msg.ReadRangedSingle(0.0f, 100.0f, 8);
TargetTurbineOutput = msg.ReadRangedSingle(0.0f, 100.0f, 8);
degreeOfSuccess = msg.ReadRangedSingle(0.0f, 1.0f, 8);
ApplyTemperatureBoost(msg.ReadRangedSingle(-TemperatureBoostAmount, TemperatureBoostAmount, 8));
if (Math.Abs(FissionRateScrollBar.BarScroll - TargetFissionRate / 100.0f) > 0.01f)
{

View File

@@ -802,11 +802,12 @@ namespace Barotrauma.Items.Components
if (passivePingRadius > 0.0f)
{
if (activePingsCount == 0) { disruptedDirections.Clear(); }
//emit "pings" from nearby sound-emitting AITargets to reveal what's around them
foreach (AITarget t in AITarget.List)
{
if (t.Entity is Character c && !c.IsUnconscious && c.Params.HideInSonar) { continue; }
if (t.SoundRange <= 0.0f || float.IsNaN(t.SoundRange) || float.IsInfinity(t.SoundRange)) { continue; }
float distSqr = Vector2.DistanceSquared(t.WorldPosition, transducerCenter);
if (distSqr > t.SoundRange * t.SoundRange * 2) { continue; }
@@ -814,9 +815,12 @@ namespace Barotrauma.Items.Components
if (dist > prevPassivePingRadius * Range && dist <= passivePingRadius * Range && Rand.Int(sonarBlips.Count) < 500)
{
Ping(t.WorldPosition, transducerCenter,
Math.Min(t.SoundRange, range * 0.5f) * displayScale, 0, displayScale, Math.Min(t.SoundRange, range * 0.5f),
passive: true, pingStrength: 0.5f);
sonarBlips.Add(new SonarBlip(t.WorldPosition, 1.0f, 1.0f));
t.SoundRange * displayScale, 0, displayScale, range,
passive: true, pingStrength: 0.5f, needsToBeInSector: t);
if (t.IsWithinSector(transducerCenter))
{
sonarBlips.Add(new SonarBlip(t.WorldPosition, fadeTimer: 1.0f, scale: MathHelper.Clamp(t.SoundRange / 2000, 1.0f, 5.0f)));
}
}
}
}
@@ -1276,7 +1280,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));
@@ -1345,7 +1349,7 @@ namespace Barotrauma.Items.Components
}
private void Ping(Vector2 pingSource, Vector2 transducerPos, float pingRadius, float prevPingRadius, float displayScale, float range, bool passive,
float pingStrength = 1.0f)
float pingStrength = 1.0f, AITarget needsToBeInSector = null)
{
float prevPingRadiusSqr = prevPingRadius * prevPingRadius;
float pingRadiusSqr = pingRadius * pingRadius;
@@ -1357,25 +1361,25 @@ namespace Barotrauma.Items.Components
new Vector2(item.CurrentHull.WorldRect.X, item.CurrentHull.WorldRect.Y),
new Vector2(item.CurrentHull.WorldRect.Right, item.CurrentHull.WorldRect.Y),
pingSource, transducerPos,
pingRadius, prevPingRadius, 50.0f, 5.0f, range, 2.0f, passive);
pingRadius, prevPingRadius, 50.0f, 5.0f, range, 2.0f, passive, needsToBeInSector: needsToBeInSector);
CreateBlipsForLine(
new Vector2(item.CurrentHull.WorldRect.X, item.CurrentHull.WorldRect.Y - item.CurrentHull.Rect.Height),
new Vector2(item.CurrentHull.WorldRect.Right, item.CurrentHull.WorldRect.Y - item.CurrentHull.Rect.Height),
pingSource, transducerPos,
pingRadius, prevPingRadius, 50.0f, 5.0f, range, 2.0f, passive);
pingRadius, prevPingRadius, 50.0f, 5.0f, range, 2.0f, passive, needsToBeInSector: needsToBeInSector);
CreateBlipsForLine(
new Vector2(item.CurrentHull.WorldRect.X, item.CurrentHull.WorldRect.Y),
new Vector2(item.CurrentHull.WorldRect.X, item.CurrentHull.WorldRect.Y - item.CurrentHull.Rect.Height),
pingSource, transducerPos,
pingRadius, prevPingRadius, 50.0f, 5.0f, range, 2.0f, passive);
pingRadius, prevPingRadius, 50.0f, 5.0f, range, 2.0f, passive, needsToBeInSector: needsToBeInSector);
CreateBlipsForLine(
new Vector2(item.CurrentHull.WorldRect.Right, item.CurrentHull.WorldRect.Y),
new Vector2(item.CurrentHull.WorldRect.Right, item.CurrentHull.WorldRect.Y - item.CurrentHull.Rect.Height),
pingSource, transducerPos,
pingRadius, prevPingRadius, 50.0f, 5.0f, range, 2.0f, passive);
pingRadius, prevPingRadius, 50.0f, 5.0f, range, 2.0f, passive, needsToBeInSector: needsToBeInSector);
return;
}
@@ -1404,7 +1408,8 @@ namespace Barotrauma.Items.Components
end + submarine.WorldPosition,
pingSource, transducerPos,
pingRadius, prevPingRadius,
200.0f, 2.0f, range, 1.0f, passive);
200.0f, 2.0f, range, 1.0f, passive,
needsToBeInSector: needsToBeInSector);
}
}
@@ -1417,7 +1422,8 @@ namespace Barotrauma.Items.Components
new Vector2(pingSource.X + range, Level.Loaded.Size.Y),
pingSource, transducerPos,
pingRadius, prevPingRadius,
250.0f, 150.0f, range, pingStrength, passive);
250.0f, 150.0f, range, pingStrength, passive,
needsToBeInSector: needsToBeInSector);
}
if (pingSource.Y - Level.Loaded.BottomPos < range)
{
@@ -1426,7 +1432,8 @@ namespace Barotrauma.Items.Components
new Vector2(pingSource.X + range, Level.Loaded.BottomPos),
pingSource, transducerPos,
pingRadius, prevPingRadius,
250.0f, 150.0f, range, pingStrength, passive);
250.0f, 150.0f, range, pingStrength, passive,
needsToBeInSector: needsToBeInSector);
}
List<Voronoi2.VoronoiCell> cells = Level.Loaded.GetCells(pingSource, 7);
@@ -1448,7 +1455,8 @@ namespace Barotrauma.Items.Components
pingSource, transducerPos,
pingRadius, prevPingRadius,
350.0f, 3.0f * (Math.Abs(facingDot) + 1.0f), range, pingStrength, passive,
blipType : cell.IsDestructible ? BlipType.Destructible : BlipType.Default);
blipType : cell.IsDestructible ? BlipType.Destructible : BlipType.Default,
needsToBeInSector: needsToBeInSector);
}
}
}
@@ -1458,14 +1466,13 @@ namespace Barotrauma.Items.Components
if (item.CurrentHull == null && item.Prefab.SonarSize > 0.0f)
{
float pointDist = ((item.WorldPosition - pingSource) * displayScale).LengthSquared();
if (pointDist > prevPingRadiusSqr && pointDist < pingRadiusSqr)
{
var blip = new SonarBlip(
item.WorldPosition + Rand.Vector(item.Prefab.SonarSize),
MathHelper.Clamp(item.Prefab.SonarSize, 0.1f, pingStrength),
MathHelper.Clamp(item.Prefab.SonarSize * 0.1f, 0.1f, 10.0f));
if (!passive && !CheckBlipVisibility(blip, transducerPos)) continue;
if (!IsVisible(blip)) { continue; }
sonarBlips.Add(blip);
}
}
@@ -1488,7 +1495,7 @@ namespace Barotrauma.Items.Components
c.WorldPosition,
MathHelper.Clamp(c.Mass, 0.1f, pingStrength),
MathHelper.Clamp(c.Mass * 0.03f, 0.1f, 2.0f));
if (!passive && !CheckBlipVisibility(blip, transducerPos)) { continue; }
if (!IsVisible(blip)) { continue; }
sonarBlips.Add(blip);
HintManager.OnSonarSpottedCharacter(Item, c);
}
@@ -1505,19 +1512,29 @@ namespace Barotrauma.Items.Components
if (pointDist > prevPingRadiusSqr && pointDist < pingRadiusSqr)
{
var blip = new SonarBlip(
limb.WorldPosition + Rand.Vector(limb.Mass / 10.0f),
MathHelper.Clamp(limb.Mass, 0.1f, pingStrength),
limb.WorldPosition + Rand.Vector(limb.Mass / 10.0f),
MathHelper.Clamp(limb.Mass, 0.1f, pingStrength),
MathHelper.Clamp(limb.Mass * 0.1f, 0.1f, 2.0f));
if (!passive && !CheckBlipVisibility(blip, transducerPos)) { continue; }
if (!IsVisible(blip)) { continue; }
sonarBlips.Add(blip);
HintManager.OnSonarSpottedCharacter(Item, c);
}
}
}
bool IsVisible(SonarBlip blip)
{
if (!passive && !CheckBlipVisibility(blip, transducerPos)) { return false; }
if (needsToBeInSector != null)
{
if (!needsToBeInSector.IsWithinSector(blip.Position)) { return false; }
}
return true;
}
}
private void CreateBlipsForLine(Vector2 point1, Vector2 point2, Vector2 pingSource, Vector2 transducerPos, float pingRadius, float prevPingRadius,
float lineStep, float zStep, float range, float pingStrength, bool passive, BlipType blipType = BlipType.Default)
float lineStep, float zStep, float range, float pingStrength, bool passive, BlipType blipType = BlipType.Default, AITarget needsToBeInSector = null)
{
lineStep /= zoom;
zStep /= zoom;
@@ -1563,12 +1580,17 @@ namespace Barotrauma.Items.Components
Vector2 pos = point + Rand.Vector(150.0f / zoom) + pingDirection * z / displayScale;
float fadeTimer = alpha * (1.0f - displayPointDist / range);
int minDist = (int)(200 / zoom);
sonarBlips.RemoveAll(b => b.FadeTimer < fadeTimer && Math.Abs(pos.X - b.Position.X) < minDist && Math.Abs(pos.Y - b.Position.Y) < minDist);
if (needsToBeInSector != null)
{
if (!needsToBeInSector.IsWithinSector(pos)) { continue; }
}
var blip = new SonarBlip(pos, fadeTimer, 1.0f + ((displayPointDist + z) / DisplayRadius), blipType);
if (!passive && !CheckBlipVisibility(blip, transducerPos)) { continue; }
int minDist = (int)(200 / zoom);
sonarBlips.RemoveAll(b => b.FadeTimer < fadeTimer && Math.Abs(pos.X - b.Position.X) < minDist && Math.Abs(pos.Y - b.Position.Y) < minDist);
sonarBlips.Add(blip);
zStep += 0.5f / zoom;

View File

@@ -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,15 +382,26 @@ 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))
{
// Docking to an outpost
var subsToLeaveBehind = campaign.GetSubsToLeaveBehind(Item.Submarine);
var subsToLeaveBehind = CampaignMode.GetSubsToLeaveBehind(Item.Submarine);
if (subsToLeaveBehind.Any())
{
enterOutpostPrompt = new GUIMessageBox(
@@ -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;
}

View File

@@ -97,7 +97,7 @@ namespace Barotrauma.Items.Components
var chargeText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1), chargeTextContainer.RectTransform, Anchor.CenterRight),
"", textColor: GUIStyle.TextColorNormal, font: GUIStyle.Font, textAlignment: Alignment.CenterRight)
{
TextGetter = () => $"{(int)MathF.Round(charge)}/{(int)capacity} {kWmin} ({(int)MathF.Round(MathUtils.Percentage(charge, capacity))} %)"
TextGetter = () => $"{(int)MathF.Round(charge)}/{(int)adjustedCapacity} {kWmin} ({(int)MathF.Round(MathUtils.Percentage(charge, adjustedCapacity))} %)"
};
if (chargeText.TextSize.X > chargeText.Rect.Width) { chargeText.Font = GUIStyle.SmallFont; }
@@ -108,7 +108,7 @@ namespace Barotrauma.Items.Components
{
ProgressGetter = () =>
{
return capacity <= 0.0f ? 1.0f : charge / capacity;
return adjustedCapacity <= 0.0f ? 1.0f : charge / adjustedCapacity;
}
};
}
@@ -126,42 +126,50 @@ namespace Barotrauma.Items.Components
{
if (chargeIndicator != null)
{
float chargeRatio = charge / capacity;
float chargeRatio = charge / adjustedCapacity;
chargeIndicator.Color = ToolBox.GradientLerp(chargeRatio, Color.Red, Color.Orange, Color.Green);
}
}
public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1)
{
if (indicatorSize.X <= 1.0f || indicatorSize.Y <= 1.0f) { return; }
Vector2 scaledIndicatorSize = indicatorSize * item.Scale;
if (scaledIndicatorSize.X <= 2.0f || scaledIndicatorSize.Y <= 2.0f) { return; }
const float outlineThickness = 1.0f;
Vector2 itemSize = new Vector2(item.Sprite.SourceRect.Width, item.Sprite.SourceRect.Height) * item.Scale;
Vector2 indicatorPos = -itemSize / 2 + indicatorPosition * item.Scale;
if (item.FlippedX && item.Prefab.CanSpriteFlipX) { indicatorPos.X = -indicatorPos.X - indicatorSize.X * item.Scale; }
if (item.FlippedY && item.Prefab.CanSpriteFlipY) { indicatorPos.Y = -indicatorPos.Y - indicatorSize.Y * item.Scale; }
Vector2 indicatorPos = -itemSize / 2.0f + indicatorPosition * item.Scale;
Vector2 itemPosition = new Vector2(item.DrawPosition.X, -item.DrawPosition.Y);
Vector2 flip = new Vector2(item.FlippedX && item.Prefab.CanSpriteFlipX ? -1.0f : 1.0f, item.FlippedY && item.Prefab.CanSpriteFlipY ? -1.0f : 1.0f);
Matrix rotate = Matrix.CreateRotationZ(item.RotationRad);
Vector2 center = Vector2.Transform((indicatorPos + (scaledIndicatorSize * 0.5f)) * flip, rotate) + itemPosition;
if (charge > 0 && capacity > 0)
if (charge > 0 && adjustedCapacity > 0)
{
float chargeRatio = MathHelper.Clamp(charge / capacity, 0.0f, 1.0f);
float chargeRatio = MathHelper.Clamp(charge / adjustedCapacity, 0.0f, 1.0f);
Color indicatorColor = ToolBox.GradientLerp(chargeRatio, Color.Red, Color.Orange, Color.Green);
if (!isHorizontal)
Vector2 indicatorCenter = (indicatorPos + (scaledIndicatorSize * 0.5f)) * flip;
Vector2 indicatorSize;
if (isHorizontal)
{
GUI.DrawRectangle(spriteBatch,
new Vector2(item.DrawPosition.X, -item.DrawPosition.Y + ((indicatorSize.Y * item.Scale) * (1.0f - chargeRatio))) + indicatorPos,
new Vector2(indicatorSize.X * item.Scale, (indicatorSize.Y * item.Scale) * chargeRatio), indicatorColor, true,
depth: item.SpriteDepth - 0.00001f);
float indicatorLength = (scaledIndicatorSize.X - outlineThickness * 2.0f) * chargeRatio;
indicatorCenter.X += -scaledIndicatorSize.X * 0.5f + (flipIndicator ? scaledIndicatorSize.X - outlineThickness - indicatorLength * 0.5f : outlineThickness + indicatorLength * 0.5f);
indicatorSize = new Vector2(indicatorLength, scaledIndicatorSize.Y);
}
else
{
GUI.DrawRectangle(spriteBatch,
new Vector2(item.DrawPosition.X, -item.DrawPosition.Y) + indicatorPos,
new Vector2((indicatorSize.X * item.Scale) * chargeRatio, indicatorSize.Y * item.Scale), indicatorColor, true,
depth: item.SpriteDepth - 0.00001f);
float indicatorLength = (scaledIndicatorSize.Y - outlineThickness * 2.0f) * chargeRatio;
indicatorCenter.Y += -scaledIndicatorSize.Y * 0.5f + (flipIndicator ? outlineThickness + indicatorLength * 0.5f : scaledIndicatorSize.Y - outlineThickness - indicatorLength * 0.5f);
indicatorSize = new Vector2(scaledIndicatorSize.X, indicatorLength);
}
indicatorCenter = Vector2.Transform(indicatorCenter, rotate) + itemPosition;
GUI.DrawFilledRectangle(spriteBatch, indicatorCenter, indicatorSize, indicatorSize * 0.5f, item.RotationRad, indicatorColor, item.SpriteDepth - 0.00001f);
}
GUI.DrawRectangle(spriteBatch,
new Vector2(item.DrawPosition.X, -item.DrawPosition.Y) + indicatorPos,
indicatorSize * item.Scale, Color.Black, depth: item.SpriteDepth - 0.000015f);
GUI.DrawRectangle(spriteBatch, center, scaledIndicatorSize, scaledIndicatorSize * 0.5f, item.RotationRad, Color.Black, item.SpriteDepth - 0.000015f, outlineThickness, GUI.OutlinePosition.Inside);
}
public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData)
@@ -185,7 +193,7 @@ namespace Barotrauma.Items.Components
rechargeSpeedSlider.BarScroll = rechargeRate;
}
#endif
Charge = msg.ReadRangedSingle(0.0f, 1.0f, 8) * capacity;
Charge = msg.ReadRangedSingle(0.0f, 1.0f, 8) * adjustedCapacity;
}
}
}

View File

@@ -35,6 +35,13 @@ namespace Barotrauma.Items.Components
if (GuiFrame == null) { return; }
originalMaxSize = GuiFrame.RectTransform.MaxSize;
originalRelativeSize = GuiFrame.RectTransform.RelativeSize;
CreateGUI();
}
protected override void CreateGUI()
{
if (GuiFrame == null) { return; }
CheckForLabelOverlap();
var content = new GUICustomComponent(new RectTransform(Vector2.One, GuiFrame.RectTransform), DrawConnections, null)
{
@@ -43,8 +50,8 @@ namespace Barotrauma.Items.Components
content.RectTransform.SetAsFirstChild();
//prevents inputs from going through the GUICustomComponent to the drag handle
dragArea = new GUIFrame(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center)
{ AbsoluteOffset = GUIStyle.ItemFrameOffset }, style: null);
dragArea = new GUIFrame(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center)
{ AbsoluteOffset = GUIStyle.ItemFrameOffset }, style: null);
}
public void TriggerRewiringSound()
@@ -121,12 +128,6 @@ namespace Barotrauma.Items.Components
}
}
protected override void OnResolutionChanged()
{
if (GuiFrame == null) { return; }
CheckForLabelOverlap();
}
private void CheckForLabelOverlap()
{
GuiFrame.RectTransform.MaxSize = originalMaxSize;

View File

@@ -324,7 +324,7 @@ namespace Barotrauma.Items.Components
if (Character.Controlled != null)
{
Character.Controlled.FocusedItem = null;
Character.Controlled.ResetInteract = true;
Character.Controlled.DisableInteract = true;
Character.Controlled.ClearInputs();
}
//cancel dragging
@@ -401,7 +401,7 @@ namespace Barotrauma.Items.Components
{
if (Character.Controlled != null)
{
Character.Controlled.ResetInteract = true;
Character.Controlled.DisableInteract = true;
Character.Controlled.ClearInputs();
}
int closestSectionIndex = selectedWire.GetClosestSectionIndex(mousePos, sectionSelectDist, out _);
@@ -431,7 +431,7 @@ namespace Barotrauma.Items.Components
{
if (Character.Controlled != null)
{
Character.Controlled.ResetInteract = true;
Character.Controlled.DisableInteract = true;
Character.Controlled.ClearInputs();
}
draggingWire = selectedWire;

View File

@@ -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));
}

View File

@@ -212,14 +212,14 @@ namespace Barotrauma.Items.Components
{
if (moveSoundChannel == null && startMoveSound != null)
{
moveSoundChannel = SoundPlayer.PlaySound(startMoveSound.Sound, item.WorldPosition, startMoveSound.Volume, startMoveSound.Range, ignoreMuffling: startMoveSound.IgnoreMuffling);
moveSoundChannel = SoundPlayer.PlaySound(startMoveSound.Sound, item.WorldPosition, startMoveSound.Volume, startMoveSound.Range, ignoreMuffling: startMoveSound.IgnoreMuffling, freqMult: startMoveSound.GetRandomFrequencyMultiplier());
}
else if (moveSoundChannel == null || !moveSoundChannel.IsPlaying)
{
if (moveSound != null)
{
moveSoundChannel.FadeOutAndDispose();
moveSoundChannel = SoundPlayer.PlaySound(moveSound.Sound, item.WorldPosition, moveSound.Volume, moveSound.Range, ignoreMuffling: moveSound.IgnoreMuffling);
moveSoundChannel = SoundPlayer.PlaySound(moveSound.Sound, item.WorldPosition, moveSound.Volume, moveSound.Range, ignoreMuffling: moveSound.IgnoreMuffling, freqMult: moveSound.GetRandomFrequencyMultiplier());
if (moveSoundChannel != null) moveSoundChannel.Looping = true;
}
}
@@ -231,7 +231,7 @@ namespace Barotrauma.Items.Components
if (endMoveSound != null && moveSoundChannel.Sound != endMoveSound.Sound)
{
moveSoundChannel.FadeOutAndDispose();
moveSoundChannel = SoundPlayer.PlaySound(endMoveSound.Sound, item.WorldPosition, endMoveSound.Volume, endMoveSound.Range, ignoreMuffling: endMoveSound.IgnoreMuffling);
moveSoundChannel = SoundPlayer.PlaySound(endMoveSound.Sound, item.WorldPosition, endMoveSound.Volume, endMoveSound.Range, ignoreMuffling: endMoveSound.IgnoreMuffling, freqMult: endMoveSound.GetRandomFrequencyMultiplier());
if (moveSoundChannel != null) moveSoundChannel.Looping = false;
}
else if (!moveSoundChannel.IsPlaying)
@@ -260,7 +260,7 @@ namespace Barotrauma.Items.Components
{
if (chargeSound != null)
{
chargeSoundChannel = SoundPlayer.PlaySound(chargeSound.Sound, item.WorldPosition, chargeSound.Volume, chargeSound.Range, ignoreMuffling: chargeSound.IgnoreMuffling);
chargeSoundChannel = SoundPlayer.PlaySound(chargeSound.Sound, item.WorldPosition, chargeSound.Volume, chargeSound.Range, ignoreMuffling: chargeSound.IgnoreMuffling, freqMult: chargeSound.GetRandomFrequencyMultiplier());
if (chargeSoundChannel != null) chargeSoundChannel.Looping = true;
}
}
@@ -411,6 +411,14 @@ namespace Barotrauma.Items.Components
SpriteEffects.None, newDepth);
}
if (GameMain.DebugDraw)
{
Vector2 firingPos = GetRelativeFiringPosition();
firingPos.Y = -firingPos.Y;
GUI.DrawLine(spriteBatch, firingPos - Vector2.UnitX * 5, firingPos + Vector2.UnitX * 5, Color.Red);
GUI.DrawLine(spriteBatch, firingPos - Vector2.UnitY * 5, firingPos + Vector2.UnitY * 5, Color.Red);
}
if (!editing || GUI.DisableHUD || !item.IsSelected) { return; }
const float widgetRadius = 60.0f;
@@ -581,7 +589,7 @@ namespace Barotrauma.Items.Components
var battery = recipient.Item?.GetComponent<PowerContainer>();
if (battery == null || battery.Item.Condition <= 0.0f) { continue; }
availableCharge += battery.Charge;
availableCapacity += battery.Capacity;
availableCapacity += battery.GetCapacity();
}
}

View File

@@ -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<DamageModifier> damageModifiers, IReadOnlyDictionary<Identifier, float> 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);

View File

@@ -1608,22 +1608,79 @@ namespace Barotrauma
{
containedState = item.Condition / item.MaxCondition;
}
else if (itemContainer.ShowTotalStackCapacityInContainedStateIndicator)
{
containedState = itemContainer.Inventory.AllItems.Count() / (float)(itemContainer.GetMaxStackSize(0) * itemContainer.Capacity);
}
else
{
var containedItem = itemContainer.Inventory.slots[Math.Max(itemContainer.ContainedStateIndicatorSlot, 0)].FirstOrDefault();
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)
int targetSlot = Math.Max(itemContainer.ContainedStateIndicatorSlot, 0);
ItemSlot containedItemSlot = null;
if (targetSlot < itemContainer.Inventory.slots.Length)
{
int maxStackSize = Math.Min(containedItem.Prefab.MaxStackSize, itemContainer.GetMaxStackSize(0));
if (maxStackSize > 1 || containedItem.Prefab.HideConditionBar)
containedItemSlot = itemContainer.Inventory.slots[targetSlot];
}
if (containedItemSlot != null)
{
Item containedItem = containedItemSlot.FirstOrDefault();
if (itemContainer.ShowTotalStackCapacityInContainedStateIndicator)
{
containedState = itemContainer.Inventory.slots[0].Items.Count / (float)maxStackSize;
if (containedItem == null)
{
// No item on the defined slot, check if the items on other slots can be used.
containedItem = containedItemSlot.FirstOrDefault() ?? itemContainer.Inventory.AllItems.FirstOrDefault(it => itemContainer.CanBeContained(it, targetSlot));
}
if (containedItem != null)
{
int ignoredItemCount = 0;
var subContainableItems = itemContainer.AllSubContainableItems;
float capacity = itemContainer.GetMaxStackSize(targetSlot);
if (subContainableItems != null)
{
bool useMainContainerCapacity = true;
foreach (Item it in itemContainer.Inventory.AllItems)
{
// Ignore all items in the sub containers.
foreach (RelatedItem ri in subContainableItems)
{
if (ri.MatchesItem(containedItem))
{
// The target item is in a subcontainer -> inverse the logic.
useMainContainerCapacity = false;
break;
}
if (ri.MatchesItem(it))
{
ignoredItemCount++;
}
}
if (!useMainContainerCapacity) { break; }
}
if (useMainContainerCapacity)
{
capacity *= itemContainer.MainContainerCapacity;
}
else
{
// Ignore all items in the main container.
ignoredItemCount = itemContainer.Inventory.AllItems.Count(it => subContainableItems.Any(ri => !ri.MatchesItem(it)));
capacity *= itemContainer.Capacity - itemContainer.MainContainerCapacity;
}
}
int itemCount = itemContainer.Inventory.AllItems.Count() - ignoredItemCount;
containedState = Math.Min(itemCount / Math.Max(capacity, 1), 1);
}
}
else
{
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 || itemContainer.HasSubContainers))
{
int maxStackSize = Math.Min(containedItem.Prefab.MaxStackSize, itemContainer.GetMaxStackSize(targetSlot));
if (maxStackSize > 1 || containedItem.Prefab.HideConditionBar)
{
containedState = containedItemSlot.Items.Count / (float)maxStackSize;
}
}
}
}
}

View File

@@ -242,6 +242,11 @@ namespace Barotrauma
return false;
}
if (parentInventory?.Owner is Character character && character.InvisibleTimer > 0.0f)
{
return false;
}
Rectangle extents;
if (cachedVisibleExtents.HasValue)
{
@@ -1251,11 +1256,9 @@ namespace Barotrauma
{
foreach (ItemComponent ic in components)
{
if (ic.DisplayMsg.IsNullOrEmpty()) { continue; }
if (!ic.CanBePicked && !ic.CanBeSelected) { continue; }
if (ic is Holdable holdable && !holdable.CanBeDeattached()) { continue; }
if (ic is ConnectionPanel connectionPanel && !connectionPanel.CanRewire()) { continue; }
Color color = Color.Gray;
if (ic.HasRequiredItems(character, false))
{
@@ -1268,6 +1271,7 @@ namespace Barotrauma
color = Color.Cyan;
}
}
if (ic.DisplayMsg.IsNullOrEmpty()) { continue; }
texts.Add(new ColoredText(ic.DisplayMsg.Value, color, false, false));
}
}
@@ -1282,7 +1286,8 @@ namespace Barotrauma
{
foreach (ItemComponent ic in activeHUDs)
{
if (ic.GuiFrame == null || !ic.CanBeSelected) { continue; }
if (ic.GuiFrame == null) { continue; }
if (!ic.CanBeSelected && !ic.DrawHudWhenEquipped) { continue; }
ic.GuiFrame.RectTransform.ScreenSpaceOffset = Point.Zero;
if (ic.UseAlternativeLayout)
{
@@ -1415,6 +1420,15 @@ namespace Barotrauma
case EventType.ChangeProperty:
ReadPropertyChange(msg, false);
break;
case EventType.ItemStat:
byte length = msg.ReadByte();
for (int i = 0; i < length; i++)
{
var statIdentifier = INetSerializableStruct.Read<ItemStatManager.TalentStatIdentifier>(msg);
var statValue = msg.ReadSingle();
StatManager.ApplyStat(statIdentifier, statValue);
}
break;
case EventType.Upgrade:
Identifier identifier = msg.ReadIdentifier();
byte level = msg.ReadByte();

View File

@@ -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<DamageModifier> wearableDamageModifiers;
private IReadOnlyDictionary<Identifier, float> 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<ContainedItemSprite>();
var decorativeSpriteGroups = new Dictionary<int, List<DecorativeSprite>>();
var wearableDamageModifiers = new List<DamageModifier>();
var wearableSkillModifiers = new Dictionary<Identifier, float>();
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();
@@ -208,6 +238,26 @@ namespace Barotrauma
DecorativeSpriteGroups = decorativeSpriteGroups.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray())).ToImmutableDictionary();
}
public bool CanCharacterBuy()
{
if (DefaultPrice == null) { return false; }
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)
{
Vector2 position = Submarine.MouseToWorldGrid(cam, Submarine.MainSub);
@@ -313,15 +363,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);
}
}
}

View File

@@ -168,11 +168,11 @@ namespace Barotrauma
}
else if (position.X < 0.0f)
{
obstacleDiff = Vector2.UnitX;
obstacleDiff = -Vector2.UnitX;
}
else if (position.X > Level.Loaded.Size.X)
{
obstacleDiff = -Vector2.UnitX;
obstacleDiff = Vector2.UnitX;
}
else
{
@@ -183,7 +183,7 @@ namespace Barotrauma
foreach (Voronoi2.VoronoiCell cell in cells)
{
Vector2 diff = cell.Center - position;
if (diff.LengthSquared() > 5000.0f * 5000.0f) continue;
if (diff.LengthSquared() > 5000.0f * 5000.0f) { continue; }
obstacleDiff += diff;
cellCount++;
}

View File

@@ -226,7 +226,7 @@ namespace Barotrauma
{
if (SoundChannels[i] == null || !SoundChannels[i].IsPlaying)
{
SoundChannels[i] = roundSound.Sound.Play(roundSound.Volume, roundSound.Range, soundPos);
SoundChannels[i] = roundSound.Sound.Play(roundSound.Volume, roundSound.Range, roundSound.GetRandomFrequencyMultiplier(), soundPos);
}
SoundChannels[i].Position = new Vector3(soundPos.X, soundPos.Y, 0.0f);
}

View File

@@ -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));
}

View File

@@ -343,38 +343,31 @@ namespace Barotrauma.Lights
{
SolidColorEffect.CurrentTechnique = SolidColorEffect.Techniques["SolidVertexColor"];
spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, effect: SolidColorEffect, transformMatrix: spriteBatchTransform);
foreach (Character character in Character.CharacterList)
{
if (character.CurrentHull == null || !character.Enabled || !character.IsVisible) { continue; }
if (Character.Controlled?.FocusedCharacter == character) { continue; }
Color lightColor = character.CurrentHull.AmbientLight == Color.TransparentBlack ?
Color.Black :
character.CurrentHull.AmbientLight.Multiply(character.CurrentHull.AmbientLight.A / 255.0f).Opaque();
foreach (Limb limb in character.AnimController.Limbs)
{
if (limb.DeformSprite != null) { continue; }
limb.Draw(spriteBatch, cam, lightColor);
}
}
DrawCharacters(spriteBatch, cam, drawDeformSprites: false);
spriteBatch.End();
DeformableSprite.Effect.CurrentTechnique = DeformableSprite.Effect.Techniques["DeformShaderSolidVertexColor"];
DeformableSprite.Effect.CurrentTechnique.Passes[0].Apply();
spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, transformMatrix: spriteBatchTransform);
DrawCharacters(spriteBatch, cam, drawDeformSprites: true);
spriteBatch.End();
}
static void DrawCharacters(SpriteBatch spriteBatch, Camera cam, bool drawDeformSprites)
{
foreach (Character character in Character.CharacterList)
{
if (character.CurrentHull == null || !character.Enabled || !character.IsVisible) { continue; }
if (character.CurrentHull == null || !character.Enabled || !character.IsVisible || character.InvisibleTimer > 0.0f) { continue; }
if (Character.Controlled?.FocusedCharacter == character) { continue; }
Color lightColor = character.CurrentHull.AmbientLight == Color.TransparentBlack ?
Color.Black :
character.CurrentHull.AmbientLight.Multiply(character.CurrentHull.AmbientLight.A / 255.0f).Opaque();
foreach (Limb limb in character.AnimController.Limbs)
{
if (limb.DeformSprite == null) { continue; }
if (drawDeformSprites == (limb.DeformSprite == null)) { continue; }
limb.Draw(spriteBatch, cam, lightColor);
}
}
spriteBatch.End();
}
DeformableSprite.Effect.CurrentTechnique = DeformableSprite.Effect.Techniques["DeformShader"];

View File

@@ -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,27 @@ 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();
List<(SubmarineClass subClass, int tier)> overrideTiers = null;
if (HighlightedLocation.CanHaveSubsForSale())
{
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));
}
}
}
int subAvailabilityTextCount = (highestSubTier > 0 ? 1 : 0) + (overrideTiers?.Count ?? 0);
size.Y += subAvailabilityTextCount * 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 +726,54 @@ 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);
}
if (overrideTiers != null)
{
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)

View File

@@ -1042,19 +1042,11 @@ namespace Barotrauma
protected static void PositionEditingHUD()
{
int maxHeight = 100;
if (Screen.Selected == GameMain.SubEditorScreen)
{
editingHUD.RectTransform.SetPosition(Anchor.TopRight);
editingHUD.RectTransform.AbsoluteOffset = new Point(0, GameMain.SubEditorScreen.TopPanel.Rect.Bottom);
maxHeight = (GameMain.GraphicsHeight - GameMain.SubEditorScreen.EntityMenu.Rect.Height) - GameMain.SubEditorScreen.TopPanel.Rect.Bottom * 2 - 20;
}
else
{
editingHUD.RectTransform.SetPosition(Anchor.TopRight);
editingHUD.RectTransform.RelativeOffset = new Vector2(0.0f, (HUDLayoutSettings.CrewArea.Bottom + 10.0f) / (editingHUD.RectTransform.Parent ?? GUI.Canvas).Rect.Height);
maxHeight = HUDLayoutSettings.InventoryAreaLower.Y - HUDLayoutSettings.CrewArea.Bottom - 10;
}
int maxHeight =
Screen.Selected == GameMain.SubEditorScreen ?
GameMain.GraphicsHeight - GameMain.SubEditorScreen.EntityMenu.Rect.Height - GameMain.SubEditorScreen.TopPanel.Rect.Bottom * 2 - 20 :
HUDLayoutSettings.InventoryAreaLower.Y - HUDLayoutSettings.CrewArea.Bottom - 10;
var listBox = editingHUD.GetChild<GUIListBox>();
if (listBox != null)
@@ -1074,6 +1066,17 @@ namespace Barotrauma
MathHelper.Clamp(contentHeight + padding * 2, 50, maxHeight)), resizeChildren: false);
listBox.RectTransform.Resize(new Point(listBox.RectTransform.NonScaledSize.X, editingHUD.RectTransform.NonScaledSize.Y - padding * 2), resizeChildren: false);
}
editingHUD.RectTransform.SetPosition(Anchor.TopRight);
if (Screen.Selected == GameMain.SubEditorScreen)
{
editingHUD.RectTransform.AbsoluteOffset = new Point(0, GameMain.SubEditorScreen.TopPanel.Rect.Bottom);
}
else
{
editingHUD.RectTransform.AbsoluteOffset = new Point(
0,
HUDLayoutSettings.HealthBarAfflictionArea.Y - editingHUD.Rect.Height - GUI.IntScale(10));
}
}
public virtual void DrawEditing(SpriteBatch spriteBatch, Camera cam) { }

View File

@@ -6,9 +6,9 @@ namespace Barotrauma.Networking
{
partial class ChatMessage
{
public virtual void ClientWrite(IWriteMessage msg)
public virtual void ClientWrite(in SegmentTableWriter<ClientNetSegment> segmentTableWriter, IWriteMessage msg)
{
msg.WriteByte((byte)ClientNetObject.CHAT_MESSAGE);
segmentTableWriter.StartNewSegment(ClientNetSegment.ChatMessage);
msg.WriteUInt16(NetStateID);
msg.WriteRangedInteger((int)Type, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1);
msg.WriteRangedInteger((int)ChatMode, 0, Enum.GetValues(typeof(ChatMode)).Length - 1);

View File

@@ -1,12 +1,15 @@
using System.Diagnostics;
using System.IO.Pipes;
using System.Linq;
using System.Threading;
namespace Barotrauma.Networking
{
static partial class ChildServerRelay
{
public static Process Process;
public static bool IsProcessAlive => Process is { HasExited: false };
private static bool localHandlesDisposed;
private static AnonymousPipeServerStream writePipe;
private static AnonymousPipeServerStream readPipe;
@@ -44,18 +47,27 @@ namespace Barotrauma.Networking
localHandlesDisposed = true;
}
public static void ClosePipes()
public static void AttemptGracefulShutDown(int maxAttempts = 20)
{
writePipe?.Dispose(); writePipe = null;
readPipe?.Dispose(); readPipe = null;
shutDown = true;
status = StatusEnum.RequestedShutDown;
writeManualResetEvent?.Set();
int checks = 0;
while (Process is { HasExited: false })
{
if (checks >= maxAttempts)
{
DebugConsole.AddWarning("Server could not be shut down gracefully");
break;
}
Thread.Sleep(100);
checks++;
}
ForceShutDown();
}
public static void ShutDown()
public static void ForceShutDown()
{
Process?.Kill(); Process = null;
writePipe = null; readPipe = null;
PrivateShutDown();
}

View File

@@ -14,6 +14,16 @@ namespace Barotrauma.Networking
set;
}
private SoundChannel radioNoiseChannel;
private float radioNoise;
public float RadioNoise
{
get { return radioNoise; }
set { radioNoise = MathHelper.Clamp(value, 0.0f, 1.0f); }
}
private bool mutedLocally;
public bool MutedLocally
{
@@ -42,35 +52,64 @@ namespace Barotrauma.Networking
!HasPermission(ClientPermissions.Kick) &&
!HasPermission(ClientPermissions.Unban);
public void UpdateSoundPosition()
public void UpdateVoipSound()
{
if (VoipSound == null) { return; }
if (!VoipSound.IsPlaying)
if (VoipSound == null || !VoipSound.IsPlaying)
{
DebugConsole.Log("Destroying voipsound");
VoipSound.Dispose();
radioNoiseChannel?.Dispose();
radioNoiseChannel = null;
if (VoipSound != null)
{
DebugConsole.Log("Destroying voipsound");
VoipSound.Dispose();
}
VoipSound = null;
return;
return;
}
if (Screen.Selected is ModDownloadScreen)
{
VoipSound.Gain = 0.0f;
}
float gain = 1.0f;
float noiseGain = 0.0f;
Vector3? position = null;
if (character != null)
{
if (GameSettings.CurrentConfig.Audio.UseDirectionalVoiceChat)
{
VoipSound.SetPosition(new Vector3(character.WorldPosition.X, character.WorldPosition.Y, 0.0f));
position = new Vector3(character.WorldPosition.X, character.WorldPosition.Y, 0.0f);
}
else
{
VoipSound.SetPosition(null);
float dist = Vector3.Distance(new Vector3(character.WorldPosition, 0.0f), GameMain.SoundManager.ListenerPosition);
VoipSound.Gain = 1.0f - MathUtils.InverseLerp(VoipSound.Near, VoipSound.Far, dist);
gain = 1.0f - MathUtils.InverseLerp(VoipSound.Near, VoipSound.Far, dist);
}
if (RadioNoise > 0.0f)
{
noiseGain = gain * RadioNoise;
gain *= 1.0f - RadioNoise;
}
}
else
VoipSound.SetPosition(position);
VoipSound.Gain = gain;
if (noiseGain > 0.0f)
{
VoipSound.SetPosition(null);
VoipSound.Gain = 1.0f;
if (radioNoiseChannel == null || !radioNoiseChannel.IsPlaying)
{
radioNoiseChannel = SoundPlayer.PlaySound("radiostatic");
radioNoiseChannel.Category = "voip";
radioNoiseChannel.Looping = true;
}
radioNoiseChannel.Near = VoipSound.Near;
radioNoiseChannel.Far = VoipSound.Far;
radioNoiseChannel.Position = position;
radioNoiseChannel.Gain = noiseGain;
}
else if (radioNoiseChannel != null)
{
radioNoiseChannel.Gain = 0.0f;
}
}
@@ -158,6 +197,11 @@ namespace Barotrauma.Networking
VoipSound.Dispose();
VoipSound = null;
}
if (radioNoiseChannel != null)
{
radioNoiseChannel.Dispose();
radioNoiseChannel = null;
}
}
}
}

View File

@@ -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(
@@ -326,10 +332,27 @@ namespace Barotrauma.Networking
};
}
public void CreateServerCrashMessage()
{
// Close any message boxes that say "The server has crashed."
var basicServerCrashMsg = TextManager.Get($"{nameof(DisconnectReason)}.{nameof(DisconnectReason.ServerCrashed)}");
GUIMessageBox.MessageBoxes
.OfType<GUIMessageBox>()
.Where(mb => mb.Text?.Text == basicServerCrashMsg)
.ToArray()
.ForEach(mb => mb.Close());
// Open a new message box with the crash report path
if (GUIMessageBox.MessageBoxes.All(
mb => (mb as GUIMessageBox)?.Text?.Text != ChildServerRelay.CrashMessage))
{
var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage);
msgBox.Buttons[0].OnClicked += ReturnToPreviousMenu;
}
}
private bool ReturnToPreviousMenu(GUIButton button, object obj)
{
Quit();
Submarine.Unload();
GameMain.Client = null;
GameMain.GameSession = null;
@@ -443,7 +466,7 @@ namespace Barotrauma.Networking
foreach (Client c in ConnectedClients)
{
if (c.Character != null && c.Character.Removed) { c.Character = null; }
c.UpdateSoundPosition();
c.UpdateVoipSound();
}
if (VoipCapture.Instance != null)
@@ -479,15 +502,11 @@ namespace Barotrauma.Networking
}
catch (Exception e)
{
string errorMsg = "Error while reading a message from server. {" + e + "}. ";
string errorMsg = "Error while reading a message from server. ";
if (GameMain.Client == null) { errorMsg += "Client disposed."; }
errorMsg += "\n" + e.StackTrace.CleanupStackTrace();
if (e.InnerException != null)
{
errorMsg += "\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace.CleanupStackTrace();
}
AppendExceptionInfo(ref errorMsg, e);
GameAnalyticsManager.AddErrorEventOnce("GameClient.Update:CheckServerMessagesException" + e.TargetSite.ToString(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
DebugConsole.ThrowError("Error while reading a message from server.", e);
DebugConsole.ThrowError(errorMsg);
new GUIMessageBox(TextManager.Get("Error"), TextManager.GetWithVariables("MessageReadError", ("[message]", e.Message), ("[targetsite]", e.TargetSite.ToString())))
{
DisplayInLoadingScreens = true
@@ -529,14 +548,10 @@ namespace Barotrauma.Networking
{
if (GameMain.WindowActive)
{
if (ChildServerRelay.Process?.HasExited ?? true)
if (!ChildServerRelay.IsProcessAlive)
{
Quit();
if (!GUIMessageBox.MessageBoxes.Any(mb => (mb as GUIMessageBox)?.Text?.Text == ChildServerRelay.CrashMessage))
{
var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage);
msgBox.Buttons[0].OnClicked += ReturnToPreviousMenu;
}
CreateServerCrashMessage();
}
}
}
@@ -632,14 +647,8 @@ namespace Barotrauma.Networking
}
catch (Exception e)
{
string errorMsg = "Error while reading an ingame update message from server. {" + e + "}\n" + e.StackTrace.CleanupStackTrace();
if (e.InnerException != null)
{
errorMsg += "\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace.CleanupStackTrace();
}
#if DEBUG
DebugConsole.ThrowError("Error while reading an ingame update message from server.", e);
#endif
string errorMsg = "Error while reading an ingame update message from server.";
AppendExceptionInfo(ref errorMsg, e);
GameAnalyticsManager.AddErrorEventOnce("GameClient.ReadDataMessage:ReadIngameUpdate", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
throw;
}
@@ -868,22 +877,31 @@ namespace Barotrauma.Networking
}
byte missionCount = inc.ReadByte();
if (missionCount != GameMain.GameSession.Missions.Count())
{
string errorMsg = $"Mission equality check failed. Mission count doesn't match the server (server: {missionCount}, client: {GameMain.GameSession.Missions.Count()})";
throw new Exception(errorMsg);
}
List<Identifier> serverMissionIdentifiers = new List<Identifier>();
for (int i = 0; i < missionCount; i++)
{
serverMissionIdentifiers.Add(inc.ReadIdentifier());
}
if (missionCount != GameMain.GameSession.GameMode.Missions.Count())
{
string errorMsg =
$"Mission equality check failed. Mission count doesn't match the server. " +
$"Server: {string.Join(", ", serverMissionIdentifiers)}, " +
$"client: {string.Join(", ", GameMain.GameSession.GameMode.Missions.Select(m => m.Prefab.Identifier))}, " +
$"game session: {string.Join(", ", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))})";
GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:MissionsCountMismatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
throw new Exception(errorMsg);
}
if (missionCount > 0)
{
if (!GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier).OrderBy(id => id).SequenceEqual(serverMissionIdentifiers.OrderBy(id => id)))
if (!GameMain.GameSession.GameMode.Missions.Select(m => m.Prefab.Identifier).OrderBy(id => id).SequenceEqual(serverMissionIdentifiers.OrderBy(id => id)))
{
string errorMsg = $"Mission equality check failed. The mission selected at your end doesn't match the one loaded by the server (server: {string.Join(", ", serverMissionIdentifiers)}, client: {string.Join(", ", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))})";
string errorMsg =
$"Mission equality check failed. The mission selected at your end doesn't match the one loaded by the server " +
$"Server: {string.Join(", ", serverMissionIdentifiers)}, " +
$"client: {string.Join(", ", GameMain.GameSession.GameMode.Missions.Select(m => m.Prefab.Identifier))}, " +
$"game session: {string.Join(", ", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))})";
GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:MissionsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
throw new Exception(errorMsg);
}
@@ -939,13 +957,6 @@ namespace Barotrauma.Networking
GUI.ClearCursorWait();
ChildServerRelay.ShutDown();
if (SteamManager.IsInitialized)
{
Steamworks.SteamFriends.ClearRichPresence();
}
if (disconnectPacket.ShouldCreateAnalyticsEvent)
{
GameAnalyticsManager.AddErrorEventOnce(
@@ -971,11 +982,43 @@ namespace Barotrauma.Networking
}
else
{
ReturnToPreviousMenu(null, null);
new GUIMessageBox(TextManager.Get(wasConnected ? "ConnectionLost" : "CouldNotConnectToServer"), disconnectPacket.PopupMessage)
if (ClientPeer is SteamP2PClientPeer or SteamP2POwnerPeer)
{
DisplayInLoadingScreens = true
};
SteamManager.LeaveLobby();
}
GameMain.ModDownloadScreen.Reset();
ContentPackageManager.EnabledPackages.Restore();
CampaignMode.StartRoundCancellationToken?.Cancel();
if (SteamManager.IsInitialized)
{
Steamworks.SteamFriends.ClearRichPresence();
}
foreach (var fileTransfer in FileReceiver.ActiveTransfers.ToArray())
{
FileReceiver.StopTransfer(fileTransfer, deleteFile: true);
}
ChildServerRelay.AttemptGracefulShutDown();
GUIMessageBox.MessageBoxes.RemoveAll(c => c?.UserData is RoundSummary);
characterInfo?.Remove();
VoipClient?.Dispose();
VoipClient = null;
GameMain.Client = null;
GameMain.GameSession = null;
ReturnToPreviousMenu(null, null);
if (disconnectPacket.DisconnectReason != DisconnectReason.Disconnected)
{
new GUIMessageBox(TextManager.Get(wasConnected ? "ConnectionLost" : "CouldNotConnectToServer"), disconnectPacket.PopupMessage)
{
DisplayInLoadingScreens = true
};
}
}
}
@@ -1321,6 +1364,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;
@@ -1878,12 +1922,11 @@ namespace Barotrauma.Networking
private void ReadLobbyUpdate(IReadMessage inc)
{
ServerNetObject objHeader;
while ((objHeader = (ServerNetObject)inc.ReadByte()) != ServerNetObject.END_OF_MESSAGE)
SegmentTableReader<ServerNetSegment>.Read(inc, (segment, inc) =>
{
switch (objHeader)
switch (segment)
{
case ServerNetObject.SYNC_IDS:
case ServerNetSegment.SyncIds:
bool lobbyUpdated = inc.ReadBoolean();
inc.ReadPadBits();
@@ -2015,17 +2058,19 @@ namespace Barotrauma.Networking
lastSentChatMsgID = inc.ReadUInt16();
break;
case ServerNetObject.CLIENT_LIST:
case ServerNetSegment.ClientList:
ReadClientList(inc);
break;
case ServerNetObject.CHAT_MESSAGE:
case ServerNetSegment.ChatMessage:
ChatMessage.ClientRead(inc);
break;
case ServerNetObject.VOTE:
case ServerNetSegment.Vote:
Voting.ClientRead(inc);
break;
}
}
return SegmentTableReader<ServerNetSegment>.BreakSegmentReading.No;
});
}
readonly List<IServerSerializable> debugEntityList = new List<IServerSerializable>();
@@ -2035,117 +2080,106 @@ namespace Barotrauma.Networking
float sendingTime = inc.ReadSingle() - 0.0f;//TODO: reimplement inc.SenderConnection.RemoteTimeOffset;
ServerNetObject? prevObjHeader = null;
long prevBitPos = 0;
long prevBytePos = 0;
long prevBitLength = 0;
long prevByteLength = 0;
ServerNetObject? objHeader = null;
try
SegmentTableReader<ServerNetSegment>.Read(inc,
segmentDataReader: (segment, inc) =>
{
while ((objHeader = (ServerNetObject)inc.ReadByte()) != ServerNetObject.END_OF_MESSAGE)
switch (segment)
{
switch (objHeader)
{
case ServerNetObject.SYNC_IDS:
lastSentChatMsgID = inc.ReadUInt16();
LastSentEntityEventID = inc.ReadUInt16();
case ServerNetSegment.SyncIds:
lastSentChatMsgID = inc.ReadUInt16();
LastSentEntityEventID = inc.ReadUInt16();
bool campaignUpdated = inc.ReadBoolean();
inc.ReadPadBits();
if (campaignUpdated)
{
MultiPlayerCampaign.ClientRead(inc);
}
else if (GameMain.NetLobbyScreen.SelectedMode != GameModePreset.MultiPlayerCampaign)
{
GameMain.NetLobbyScreen.SetCampaignCharacterInfo(null);
}
break;
case ServerNetObject.ENTITY_POSITION:
inc.ReadPadBits(); //padding is required here to make sure any padding bits within tempBuffer are read correctly
bool isItem = inc.ReadBoolean(); inc.ReadPadBits();
UInt32 incomingUintIdentifier = inc.ReadUInt32();
UInt16 id = inc.ReadUInt16();
uint msgLength = inc.ReadVariableUInt32();
int msgEndPos = (int)(inc.BitPosition + msgLength * 8);
bool campaignUpdated = inc.ReadBoolean();
inc.ReadPadBits();
if (campaignUpdated)
{
MultiPlayerCampaign.ClientRead(inc);
}
else if (GameMain.NetLobbyScreen.SelectedMode != GameModePreset.MultiPlayerCampaign)
{
GameMain.NetLobbyScreen.SetCampaignCharacterInfo(null);
}
break;
case ServerNetSegment.EntityPosition:
inc.ReadPadBits(); //padding is required here to make sure any padding bits within tempBuffer are read correctly
bool isItem = inc.ReadBoolean(); inc.ReadPadBits();
UInt32 incomingUintIdentifier = inc.ReadUInt32();
UInt16 id = inc.ReadUInt16();
uint msgLength = inc.ReadVariableUInt32();
int msgEndPos = (int)(inc.BitPosition + msgLength * 8);
var entity = Entity.FindEntityByID(id) as IServerPositionSync;
if (msgEndPos > inc.LengthBits)
{
DebugConsole.ThrowError($"Error while reading a position update for the entity \"({entity?.ToString() ?? "null"})\". Message length exceeds the size of the buffer.");
return;
}
var entity = Entity.FindEntityByID(id) as IServerPositionSync;
if (msgEndPos > inc.LengthBits)
{
DebugConsole.ThrowError($"Error while reading a position update for the entity \"({entity?.ToString() ?? "null"})\". Message length exceeds the size of the buffer.");
return SegmentTableReader<ServerNetSegment>.BreakSegmentReading.Yes;
}
debugEntityList.Add(entity);
if (entity != null)
debugEntityList.Add(entity);
if (entity != null)
{
if (entity is Item != isItem)
{
if (entity is Item != isItem)
{
DebugConsole.AddWarning($"Received a potentially invalid ENTITY_POSITION message. Entity type does not match (server entity is {(isItem ? "an item" : "not an item")}, client entity is {(entity?.GetType().ToString() ?? "null")}). Ignoring the message...");
}
else if (entity is MapEntity { Prefab: { UintIdentifier: { } uintIdentifier } } me &&
uintIdentifier != incomingUintIdentifier)
{
DebugConsole.AddWarning($"Received a potentially invalid ENTITY_POSITION message."
+$"Entity identifier does not match (server entity is {MapEntityPrefab.List.FirstOrDefault(p => p.UintIdentifier == incomingUintIdentifier)?.Identifier.Value ?? "[not found]"}, "
+$"client entity is {me.Prefab.Identifier}). Ignoring the message...");
}
else
{
entity.ClientReadPosition(inc, sendingTime);
}
DebugConsole.AddWarning($"Received a potentially invalid ENTITY_POSITION message. Entity type does not match (server entity is {(isItem ? "an item" : "not an item")}, client entity is {(entity?.GetType().ToString() ?? "null")}). Ignoring the message...");
}
//force to the correct position in case the entity doesn't exist
//or the message wasn't read correctly for whatever reason
inc.BitPosition = msgEndPos;
inc.ReadPadBits();
break;
case ServerNetObject.CLIENT_LIST:
ReadClientList(inc);
break;
case ServerNetObject.ENTITY_EVENT:
case ServerNetObject.ENTITY_EVENT_INITIAL:
if (!EntityEventManager.Read(objHeader.Value, inc, sendingTime, debugEntityList))
else if (entity is MapEntity { Prefab: { UintIdentifier: { } uintIdentifier } } me &&
uintIdentifier != incomingUintIdentifier)
{
return;
DebugConsole.AddWarning($"Received a potentially invalid ENTITY_POSITION message."
+$"Entity identifier does not match (server entity is {MapEntityPrefab.List.FirstOrDefault(p => p.UintIdentifier == incomingUintIdentifier)?.Identifier.Value ?? "[not found]"}, "
+$"client entity is {me.Prefab.Identifier}). Ignoring the message...");
}
break;
case ServerNetObject.CHAT_MESSAGE:
ChatMessage.ClientRead(inc);
break;
default:
throw new Exception($"Unknown object header \"{objHeader}\"!)");
}
prevBitLength = inc.BitPosition - prevBitPos;
prevByteLength = inc.BytePosition - prevBytePos;
else
{
entity.ClientReadPosition(inc, sendingTime);
}
}
prevObjHeader = objHeader;
prevBitPos = inc.BitPosition;
prevBytePos = inc.BytePosition;
//force to the correct position in case the entity doesn't exist
//or the message wasn't read correctly for whatever reason
inc.BitPosition = msgEndPos;
inc.ReadPadBits();
break;
case ServerNetSegment.ClientList:
ReadClientList(inc);
break;
case ServerNetSegment.EntityEvent:
case ServerNetSegment.EntityEventInitial:
if (!EntityEventManager.Read(segment, inc, sendingTime, debugEntityList))
{
return SegmentTableReader<ServerNetSegment>.BreakSegmentReading.Yes;
}
break;
case ServerNetSegment.ChatMessage:
ChatMessage.ClientRead(inc);
break;
default:
throw new Exception($"Unknown segment \"{segment}\"!)");
}
}
catch (Exception ex)
return SegmentTableReader<ServerNetSegment>.BreakSegmentReading.No;
},
exceptionHandler: (segment, prevSegments, ex) =>
{
List<string> errorLines = new List<string>
{
ex.Message,
"Message length: " + inc.LengthBits + " (" + inc.LengthBytes + " bytes)",
"Read position: " + inc.BitPosition,
"Header: " + (objHeader != null ? objHeader.Value.ToString() : "Error occurred on the very first header!"),
prevObjHeader != null ? "Previous header: " + prevObjHeader : "Error occurred on the very first header!",
"Previous object was " + (prevBitLength) + " bits long (" + (prevByteLength) + " bytes)",
" "
$"Segment with error: {segment}"
};
if (prevSegments.Any())
{
errorLines.Add("Prev segments: " + string.Join(", ", prevSegments));
errorLines.Add(" ");
}
errorLines.Add(ex.StackTrace.CleanupStackTrace());
errorLines.Add(" ");
if (prevObjHeader == ServerNetObject.ENTITY_EVENT || prevObjHeader == ServerNetObject.ENTITY_EVENT_INITIAL ||
objHeader == ServerNetObject.ENTITY_EVENT || objHeader == ServerNetObject.ENTITY_EVENT_INITIAL ||
objHeader == ServerNetObject.ENTITY_POSITION || prevObjHeader == ServerNetObject.ENTITY_POSITION)
if (prevSegments.Concat(segment.ToEnumerable()).Any(s => s.Identifier
is ServerNetSegment.EntityPosition
or ServerNetSegment.EntityEvent
or ServerNetSegment.EntityEventInitial))
{
foreach (IServerSerializable ent in debugEntityList)
{
@@ -2159,34 +2193,18 @@ namespace Barotrauma.Networking
}
}
foreach (string line in errorLines)
{
DebugConsole.ThrowError(line);
}
errorLines.Add("Last console messages:");
for (int i = DebugConsole.Messages.Count - 1; i > Math.Max(0, DebugConsole.Messages.Count - 20); i--)
{
errorLines.Add("[" + DebugConsole.Messages[i].Time + "] " + DebugConsole.Messages[i].Text);
}
GameAnalyticsManager.AddErrorEventOnce("GameClient.ReadInGameUpdate", GameAnalyticsManager.ErrorSeverity.Critical, string.Join("\n", errorLines));
DebugConsole.ThrowError("Writing object data to \"networkerror_data.log\", please send this file to us at http://github.com/Regalis11/Barotrauma/issues");
using (FileStream fl = File.Open("networkerror_data.log", System.IO.FileMode.Create))
{
using (System.IO.BinaryWriter bw = new System.IO.BinaryWriter(fl))
using (System.IO.StreamWriter sw = new System.IO.StreamWriter(fl))
{
bw.Write(inc.Buffer, (int)(prevBytePos - prevByteLength), (int)(prevByteLength));
sw.WriteLine("");
foreach (string line in errorLines)
{
sw.WriteLine(line);
}
}
}
throw new Exception("Read error: please send us \"networkerror_data.log\"!");
}
throw new Exception(
$"Exception thrown while reading segment {segment.Identifier} at position {segment.Pointer}." +
(prevSegments.Any() ? $" Previous segments: {string.Join(", ", prevSegments)}" : ""),
ex);
});
}
private void SendLobbyUpdate()
@@ -2194,50 +2212,51 @@ namespace Barotrauma.Networking
IWriteMessage outmsg = new WriteOnlyMessage();
outmsg.WriteByte((byte)ClientPacketHeader.UPDATE_LOBBY);
outmsg.WriteByte((byte)ClientNetObject.SYNC_IDS);
outmsg.WriteUInt16(GameMain.NetLobbyScreen.LastUpdateID);
outmsg.WriteUInt16(ChatMessage.LastID);
outmsg.WriteUInt16(LastClientListUpdateID);
outmsg.WriteUInt16(nameId);
outmsg.WriteString(Name);
var jobPreferences = GameMain.NetLobbyScreen.JobPreferences;
if (jobPreferences.Count > 0)
using (var segmentTable = SegmentTableWriter<ClientNetSegment>.StartWriting(outmsg))
{
outmsg.WriteIdentifier(jobPreferences[0].Prefab.Identifier);
}
else
{
outmsg.WriteIdentifier(Identifier.Empty);
}
outmsg.WriteByte((byte)MultiplayerPreferences.Instance.TeamPreference);
if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0)
{
outmsg.WriteUInt16((UInt16)0);
}
else
{
outmsg.WriteUInt16(campaign.LastSaveID);
outmsg.WriteByte(campaign.CampaignID);
foreach (MultiPlayerCampaign.NetFlags netFlag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags)))
segmentTable.StartNewSegment(ClientNetSegment.SyncIds);
outmsg.WriteUInt16(GameMain.NetLobbyScreen.LastUpdateID);
outmsg.WriteUInt16(ChatMessage.LastID);
outmsg.WriteUInt16(LastClientListUpdateID);
outmsg.WriteUInt16(nameId);
outmsg.WriteString(Name);
var jobPreferences = GameMain.NetLobbyScreen.JobPreferences;
if (jobPreferences.Count > 0)
{
outmsg.WriteUInt16(campaign.GetLastUpdateIdForFlag(netFlag));
outmsg.WriteIdentifier(jobPreferences[0].Prefab.Identifier);
}
outmsg.WriteBoolean(GameMain.NetLobbyScreen.CampaignCharacterDiscarded);
}
chatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, lastSentChatMsgID));
for (int i = 0; i < chatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++)
{
if (outmsg.LengthBytes + chatMsgQueue[i].EstimateLengthBytesClient() > MsgConstants.MTU - 5)
else
{
//no more room in this packet
break;
outmsg.WriteIdentifier(Identifier.Empty);
}
chatMsgQueue[i].ClientWrite(outmsg);
}
outmsg.WriteByte((byte)ClientNetObject.END_OF_MESSAGE);
outmsg.WriteByte((byte)MultiplayerPreferences.Instance.TeamPreference);
if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0)
{
outmsg.WriteUInt16((UInt16)0);
}
else
{
outmsg.WriteUInt16(campaign.LastSaveID);
outmsg.WriteByte(campaign.CampaignID);
foreach (MultiPlayerCampaign.NetFlags netFlag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags)))
{
outmsg.WriteUInt16(campaign.GetLastUpdateIdForFlag(netFlag));
}
outmsg.WriteBoolean(GameMain.NetLobbyScreen.CampaignCharacterDiscarded);
}
chatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, lastSentChatMsgID));
for (int i = 0; i < chatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++)
{
if (outmsg.LengthBytes + chatMsgQueue[i].EstimateLengthBytesClient() > MsgConstants.MTU - 5)
{
//no more room in this packet
break;
}
chatMsgQueue[i].ClientWrite(segmentTable, outmsg);
}
}
if (outmsg.LengthBytes > MsgConstants.MTU)
{
DebugConsole.ThrowError($"Maximum packet size exceeded ({outmsg.LengthBytes} > {MsgConstants.MTU})");
@@ -2253,44 +2272,47 @@ namespace Barotrauma.Networking
outmsg.WriteBoolean(EntityEventManager.MidRoundSyncingDone);
outmsg.WritePadBits();
outmsg.WriteByte((byte)ClientNetObject.SYNC_IDS);
//outmsg.Write(GameMain.NetLobbyScreen.LastUpdateID);
outmsg.WriteUInt16(ChatMessage.LastID);
outmsg.WriteUInt16(EntityEventManager.LastReceivedID);
outmsg.WriteUInt16(LastClientListUpdateID);
using (var segmentTable = SegmentTableWriter<ClientNetSegment>.StartWriting(outmsg))
{
segmentTable.StartNewSegment(ClientNetSegment.SyncIds);
//outmsg.Write(GameMain.NetLobbyScreen.LastUpdateID);
outmsg.WriteUInt16(ChatMessage.LastID);
outmsg.WriteUInt16(EntityEventManager.LastReceivedID);
outmsg.WriteUInt16(LastClientListUpdateID);
if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0)
{
outmsg.WriteUInt16((UInt16)0);
}
else
{
outmsg.WriteUInt16(campaign.LastSaveID);
outmsg.WriteByte(campaign.CampaignID);
foreach (MultiPlayerCampaign.NetFlags flag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags)))
if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0)
{
outmsg.WriteUInt16(campaign.GetLastUpdateIdForFlag(flag));
outmsg.WriteUInt16((UInt16)0);
}
outmsg.WriteBoolean(GameMain.NetLobbyScreen.CampaignCharacterDiscarded);
}
Character.Controlled?.ClientWriteInput(outmsg);
GameMain.GameScreen.Cam?.ClientWrite(outmsg);
EntityEventManager.Write(outmsg, ClientPeer?.ServerConnection);
chatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, lastSentChatMsgID));
for (int i = 0; i < chatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++)
{
if (outmsg.LengthBytes + chatMsgQueue[i].EstimateLengthBytesClient() > MsgConstants.MTU - 5)
else
{
//not enough room in this packet
break;
}
chatMsgQueue[i].ClientWrite(outmsg);
}
outmsg.WriteUInt16(campaign.LastSaveID);
outmsg.WriteByte(campaign.CampaignID);
foreach (MultiPlayerCampaign.NetFlags flag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags)))
{
outmsg.WriteUInt16(campaign.GetLastUpdateIdForFlag(flag));
}
outmsg.WriteByte((byte)ClientNetObject.END_OF_MESSAGE);
outmsg.WriteBoolean(GameMain.NetLobbyScreen.CampaignCharacterDiscarded);
}
Character.Controlled?.ClientWriteInput(segmentTable, outmsg);
GameMain.GameScreen.Cam?.ClientWrite(segmentTable, outmsg);
EntityEventManager.Write(segmentTable, outmsg, ClientPeer?.ServerConnection);
chatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, lastSentChatMsgID));
for (int i = 0; i < chatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++)
{
if (outmsg.LengthBytes + chatMsgQueue[i].EstimateLengthBytesClient() > MsgConstants.MTU - 5)
{
//not enough room in this packet
break;
}
chatMsgQueue[i].ClientWrite(segmentTable, outmsg);
}
}
if (outmsg.LengthBytes > MsgConstants.MTU)
{
@@ -2523,7 +2545,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)}");
}
@@ -2557,46 +2579,10 @@ namespace Barotrauma.Networking
public void Quit()
{
GameMain.LuaCs.Stop();
if (ClientPeer is SteamP2PClientPeer || ClientPeer is SteamP2POwnerPeer)
{
SteamManager.LeaveLobby();
}
GameMain.ModDownloadScreen.Reset();
ContentPackageManager.EnabledPackages.Restore();
CampaignMode.StartRoundCancellationToken?.Cancel();
ClientPeer?.Close(PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected));
ClientPeer = null;
foreach (var fileTransfer in FileReceiver.ActiveTransfers.ToArray())
{
FileReceiver.StopTransfer(fileTransfer, deleteFile: true);
}
if (ChildServerRelay.Process != null)
{
int checks = 0;
while (ChildServerRelay.Process is { HasExited: false })
{
if (checks > 10)
{
ChildServerRelay.ShutDown();
}
Thread.Sleep(100);
checks++;
}
}
ChildServerRelay.ShutDown();
GUIMessageBox.MessageBoxes.RemoveAll(c => c?.UserData is RoundSummary);
characterInfo?.Remove();
VoipClient?.Dispose();
VoipClient = null;
GameMain.Client = null;
GameMain.GameSession = null;
}
public void SendCharacterInfo(string newName = null)
@@ -2604,7 +2590,6 @@ namespace Barotrauma.Networking
IWriteMessage msg = new WriteOnlyMessage();
msg.WriteByte((byte)ClientPacketHeader.UPDATE_CHARACTERINFO);
WriteCharacterInfo(msg, newName);
msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE);
ClientPeer?.Send(msg, DeliveryMethod.Reliable);
}
@@ -2644,9 +2629,11 @@ namespace Barotrauma.Networking
IWriteMessage msg = new WriteOnlyMessage();
msg.WriteByte((byte)ClientPacketHeader.UPDATE_LOBBY);
msg.WriteByte((byte)ClientNetObject.VOTE);
Voting.ClientWrite(msg, voteType, data);
msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE);
using (var segmentTable = SegmentTableWriter<ClientNetSegment>.StartWriting(msg))
{
segmentTable.StartNewSegment(ClientNetSegment.Vote);
Voting.ClientWrite(msg, voteType, data);
}
ClientPeer.Send(msg, DeliveryMethod.Reliable);
}
@@ -2762,7 +2749,6 @@ namespace Barotrauma.Networking
msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND);
msg.WriteUInt16((UInt16)ClientPermissions.ManageCampaign);
campaign.ClientWrite(msg);
msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE);
ClientPeer.Send(msg, DeliveryMethod.Reliable);
}
@@ -2811,7 +2797,6 @@ namespace Barotrauma.Networking
msg.WriteUInt16((UInt16)ClientPermissions.SelectSub);
msg.WriteBoolean(isShuttle); msg.WritePadBits();
msg.WriteString(sub.MD5Hash.StringRepresentation);
msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE);
ClientPeer.Send(msg, DeliveryMethod.Reliable);
}
@@ -2831,7 +2816,6 @@ namespace Barotrauma.Networking
msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND);
msg.WriteUInt16((UInt16)ClientPermissions.SelectMode);
msg.WriteUInt16((UInt16)modeIndex);
msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE);
ClientPeer.Send(msg, DeliveryMethod.Reliable);
}
@@ -3537,6 +3521,23 @@ namespace Barotrauma.Networking
eventErrorWritten = true;
}
private static void AppendExceptionInfo(ref string errorMsg, Exception e)
{
if (!errorMsg.EndsWith("\n")) { errorMsg += "\n"; }
errorMsg += e.Message + "\n";
var innermostException = e.GetInnermost();
if (innermostException != e)
{
// If available, only append the stacktrace of the innermost exception,
// because that's the most important one to fix
errorMsg += "Inner exception: " + innermostException.Message + "\n" + innermostException.StackTrace.CleanupStackTrace();
}
else
{
errorMsg += e.StackTrace.CleanupStackTrace();
}
}
#if DEBUG
public void ForceTimeOut()
{

View File

@@ -66,7 +66,7 @@ namespace Barotrauma.Networking
events.Add(newEvent);
}
public void Write(IWriteMessage msg, NetworkConnection serverConnection)
public void Write(in SegmentTableWriter<ClientNetSegment> segmentTable, IWriteMessage msg, NetworkConnection serverConnection)
{
if (events.Count == 0 || serverConnection == null) return;
@@ -103,7 +103,7 @@ namespace Barotrauma.Networking
eventLastSent[entityEvent.ID] = (float)Lidgren.Network.NetTime.Now;
}
msg.WriteByte((byte)ClientNetObject.ENTITY_STATE);
segmentTable.StartNewSegment(ClientNetSegment.EntityState);
Write(msg, eventsToSync, out _);
}
@@ -112,11 +112,11 @@ namespace Barotrauma.Networking
/// <summary>
/// Read the events from the message, ignoring ones we've already received. Returns false if reading the events fails.
/// </summary>
public bool Read(ServerNetObject type, IReadMessage msg, float sendingTime, List<IServerSerializable> entities)
public bool Read(ServerNetSegment type, IReadMessage msg, float sendingTime, List<IServerSerializable> entities)
{
UInt16 unreceivedEntityEventCount = 0;
if (type == ServerNetObject.ENTITY_EVENT_INITIAL)
if (type == ServerNetSegment.EntityEventInitial)
{
unreceivedEntityEventCount = msg.ReadUInt16();
firstNewID = msg.ReadUInt16();
@@ -218,43 +218,20 @@ namespace Barotrauma.Networking
Microsoft.Xna.Framework.Color.Green);
}
lastReceivedID++;
try
ReadEvent(msg, entity, sendingTime);
msg.ReadPadBits();
if (msg.BitPosition != msgPosition + msgLength * 8)
{
ReadEvent(msg, entity, sendingTime);
msg.ReadPadBits();
var prevEntity = entities.Count >= 2 ? entities[entities.Count - 2] : null;
ushort prevId = prevEntity is Entity p ? p.ID : (ushort)0;
string errorMsg = $"Message byte position incorrect after reading an event for the entity \"{entity}\" (ID {(entity is Entity e ? e.ID : 0)}). "
+$"The previous entity was \"{prevEntity}\" (ID {prevId}) "
+$"Read {msg.BitPosition - msgPosition} bits, expected message length was {msgLength * 8} bits.";
if (msg.BitPosition != msgPosition + msgLength * 8)
{
var prevEntity = entities.Count >= 2 ? entities[entities.Count - 2] : null;
ushort prevId = prevEntity is Entity p ? p.ID : (ushort)0;
string errorMsg = $"Message byte position incorrect after reading an event for the entity \"{entity}\" (ID {(entity is Entity e ? e.ID : 0)}). "
+$"The previous entity was \"{prevEntity}\" (ID {prevId}) "
+$"Read {msg.BitPosition - msgPosition} bits, expected message length was {msgLength * 8} bits.";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("ClientEntityEventManager.Read:BitPosMismatch", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
//TODO: force the BitPosition to correct place? Having some entity in a potentially incorrect state is not as bad as a desync kick
//msg.BitPosition = (int)(msgPosition + msgLength * 8);
}
}
catch (Exception e)
{
string errorMsg = $"Failed to read event {thisEventID} for entity \"{entity}\"" +
$"{(entity is Entity { ID: var entityId } ? $", id {entityId}" : "")} ";
DebugConsole.ThrowError(errorMsg, e);
errorMsg += $"({e.Message})! (MidRoundSyncing: {thisClient.MidRoundSyncing})\n{e.StackTrace.CleanupStackTrace()}";
errorMsg += "\nPrevious entities:";
for (int j = entities.Count - 2; j >= 0; j--)
{
errorMsg += "\n" + (entities[j] == null ? "NULL" : entities[j].ToString());
}
GameAnalyticsManager.AddErrorEventOnce("ClientEntityEventManager.Read:ReadFailed" + entity.ToString(),
GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
msg.BitPosition = (int)(msgPosition + msgLength * 8);
GameAnalyticsManager.AddErrorEventOnce("ClientEntityEventManager.Read:BitPosMismatch", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
throw new Exception(errorMsg);
}
}
}
@@ -277,16 +254,12 @@ namespace Barotrauma.Networking
public void Clear()
{
ID = 0;
lastReceivedID = 0;
firstNewID = null;
events.Clear();
eventLastSent.Clear();
MidRoundSyncingDone = false;
ClearSelf();
}
/// <summary>
@@ -297,6 +270,10 @@ namespace Barotrauma.Networking
{
ID = 0;
events.Clear();
if (thisClient != null)
{
thisClient.LastSentEntityEventID = 0;
}
}
}
}

View File

@@ -4,9 +4,9 @@ namespace Barotrauma.Networking
{
partial class OrderChatMessage : ChatMessage
{
public override void ClientWrite(IWriteMessage msg)
public override void ClientWrite(in SegmentTableWriter<ClientNetSegment> segmentTableWriter, IWriteMessage msg)
{
msg.WriteByte((byte)ClientNetObject.CHAT_MESSAGE);
segmentTableWriter.StartNewSegment(ClientNetSegment.ChatMessage);
msg.WriteUInt16(NetStateID);
msg.WriteRangedInteger((int)ChatMessageType.Order, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1);
msg.WriteRangedInteger((int)ChatMode.None, 0, Enum.GetValues(typeof(ChatMode)).Length - 1);

View File

@@ -91,15 +91,11 @@ namespace Barotrauma.Networking
ToolBox.ThrowIfNull(netClient);
ToolBox.ThrowIfNull(incomingLidgrenMessages);
if (isOwner && !(ChildServerRelay.Process is { HasExited: false }))
if (isOwner && !ChildServerRelay.IsProcessAlive)
{
var gameClient = GameMain.Client;
Close(PeerDisconnectPacket.WithReason(DisconnectReason.ServerCrashed));
var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage);
msgBox.Buttons[0].OnClicked += (btn, obj) =>
{
GameMain.MainMenuScreen.Select();
return false;
};
gameClient?.CreateServerCrashMessage();
return;
}
@@ -111,7 +107,7 @@ namespace Barotrauma.Networking
foreach (NetIncomingMessage inc in incomingLidgrenMessages)
{
if (!inc.SenderConnection.RemoteEndPoint.Equals(lidgrenEndpoint.NetEndpoint))
if (!inc.SenderConnection.RemoteEndPoint.EquivalentTo(lidgrenEndpoint.NetEndpoint))
{
DebugConsole.AddWarning($"Mismatched endpoint: expected {lidgrenEndpoint.NetEndpoint}, got {inc.SenderConnection.RemoteEndPoint}");
continue;

View File

@@ -187,15 +187,11 @@ namespace Barotrauma.Networking
{
if (!isActive) { return; }
if (ChildServerRelay.HasShutDown || !(ChildServerRelay.Process is { HasExited: false }))
if (ChildServerRelay.HasShutDown || !ChildServerRelay.IsProcessAlive)
{
var gameClient = GameMain.Client;
Close(PeerDisconnectPacket.WithReason(DisconnectReason.ServerCrashed));
var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage);
msgBox.Buttons[0].OnClicked += (btn, obj) =>
{
GameMain.MainMenuScreen.Select();
return false;
};
gameClient?.CreateServerCrashMessage();
return;
}
@@ -401,8 +397,6 @@ namespace Barotrauma.Networking
ClosePeerSession(remotePeers[i]);
}
ChildServerRelay.ClosePipes();
callbacks.OnDisconnect.Invoke(peerDisconnectPacket);
SteamManager.LeaveLobby();

View File

@@ -14,7 +14,7 @@ namespace Barotrauma.Networking
{
static class PingUtils
{
private static readonly Dictionary<IPAddress, int> activePings = new Dictionary<IPAddress, int>();
private static readonly Dictionary<IPEndPoint, int> activePings = new Dictionary<IPEndPoint, int>();
private static bool steamPingInfoReady;
@@ -36,9 +36,9 @@ namespace Barotrauma.Networking
switch (serverInfo.Endpoint)
{
case LidgrenEndpoint { NetEndpoint: { Address: var address } }:
case LidgrenEndpoint { NetEndpoint: var endPoint }:
GetIPAddressPing(serverInfo, address, onPingDiscovered);
GetIPAddressPing(serverInfo, endPoint, onPingDiscovered);
break;
case SteamP2PEndpoint steamP2PEndpoint:
TaskPool.Add($"EstimateSteamLobbyPing ({steamP2PEndpoint.StringRepresentation})",
@@ -131,9 +131,9 @@ namespace Barotrauma.Networking
}
}
private static void GetIPAddressPing(ServerInfo serverInfo, IPAddress address, Action<ServerInfo> onPingDiscovered)
private static void GetIPAddressPing(ServerInfo serverInfo, IPEndPoint endPoint, Action<ServerInfo> onPingDiscovered)
{
if (IPAddress.IsLoopback(address))
if (IPAddress.IsLoopback(endPoint.Address))
{
serverInfo.Ping = Option<int>.Some(0);
onPingDiscovered(serverInfo);
@@ -142,24 +142,24 @@ namespace Barotrauma.Networking
{
lock (activePings)
{
if (activePings.ContainsKey(address)) { return; }
activePings.Add(address, activePings.Any() ? activePings.Values.Max() + 1 : 0);
if (activePings.ContainsKey(endPoint)) { return; }
activePings.Add(endPoint, activePings.Any() ? activePings.Values.Max() + 1 : 0);
}
serverInfo.Ping = Option<int>.None();
TaskPool.Add($"PingServerAsync ({address})", PingServerAsync(address, 1000),
TaskPool.Add($"PingServerAsync ({endPoint})", PingServerAsync(endPoint, 1000),
rtt =>
{
if (!rtt.TryGetResult(out serverInfo.Ping)) { serverInfo.Ping = Option<int>.None(); }
onPingDiscovered(serverInfo);
lock (activePings)
{
activePings.Remove(address);
activePings.Remove(endPoint);
}
});
}
}
private static async Task<Option<int>> PingServerAsync(IPAddress ipAddress, int timeOut)
private static async Task<Option<int>> PingServerAsync(IPEndPoint endPoint, int timeOut)
{
await Task.Yield();
bool shouldGo = false;
@@ -167,21 +167,21 @@ namespace Barotrauma.Networking
{
lock (activePings)
{
shouldGo = activePings.Count(kvp => kvp.Value < activePings[ipAddress]) < 25;
shouldGo = activePings.Count(kvp => kvp.Value < activePings[endPoint]) < 25;
}
await Task.Delay(25);
}
if (ipAddress == null) { return Option<int>.None(); }
if (endPoint?.Address == null) { return Option<int>.None(); }
//don't attempt to ping if the address is IPv6 and it's not supported
if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6 && !Socket.OSSupportsIPv6) { return Option<int>.None(); }
if (endPoint.Address.AddressFamily == AddressFamily.InterNetworkV6 && !Socket.OSSupportsIPv6) { return Option<int>.None(); }
Ping ping = new Ping();
byte[] buffer = new byte[32];
try
{
PingReply pingReply = await ping.SendPingAsync(ipAddress, timeOut, buffer, new PingOptions(128, true));
PingReply pingReply = await ping.SendPingAsync(endPoint.Address, timeOut, buffer, new PingOptions(128, true));
return pingReply.Status switch
{
@@ -191,9 +191,9 @@ namespace Barotrauma.Networking
}
catch (Exception ex)
{
GameAnalyticsManager.AddErrorEventOnce("ServerListScreen.PingServer:PingException" + ipAddress, GameAnalyticsManager.ErrorSeverity.Warning, "Failed to ping a server - " + (ex?.InnerException?.Message ?? ex.Message));
GameAnalyticsManager.AddErrorEventOnce("ServerListScreen.PingServer:PingException" + endPoint.Address, GameAnalyticsManager.ErrorSeverity.Warning, "Failed to ping a server - " + (ex?.InnerException?.Message ?? ex.Message));
#if DEBUG
DebugConsole.NewMessage("Failed to ping a server (" + ipAddress + ") - " + (ex?.InnerException?.Message ?? ex.Message), Color.Red);
DebugConsole.NewMessage("Failed to ping a server (" + endPoint.Address + ") - " + (ex?.InnerException?.Message ?? ex.Message), Color.Red);
#endif
return Option<int>.None();

View File

@@ -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<EnemyHealthBarMode>();
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

View File

@@ -17,9 +17,7 @@ namespace Barotrauma.Networking
get;
private set;
}
public static IReadOnlyList<string> CaptureDeviceNames =>
Alc.GetStringList(IntPtr.Zero, OpenAL.Alc.CaptureDeviceSpecifier);
private readonly IntPtr captureDevice;
@@ -169,6 +167,11 @@ namespace Barotrauma.Networking
Create(GameSettings.CurrentConfig.Audio.VoiceCaptureDevice, storedBufferID);
}
public static IReadOnlyList<string> GetCaptureDeviceNames()
{
return Alc.GetStringList(IntPtr.Zero, OpenAL.Alc.CaptureDeviceSpecifier);
}
IntPtr nativeBuffer;
readonly short[] uncompressedBuffer = new short[VoipConfig.BUFFER_SIZE];
readonly short[] prevUncompressedBuffer = new short[VoipConfig.BUFFER_SIZE];
@@ -260,6 +263,13 @@ namespace Barotrauma.Networking
}
}
}
if (Screen.Selected is ModDownloadScreen)
{
allowEnqueue = false;
captureTimer = 0;
}
if (allowEnqueue || captureTimer > 0)
{
LastEnqueueAudio = DateTime.Now;

View File

@@ -1,20 +1,23 @@
using Barotrauma.Sounds;
using Barotrauma.Items.Components;
using Barotrauma.Sounds;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Xna.Framework;
using Barotrauma.Items.Components;
namespace Barotrauma.Networking
{
class VoipClient : IDisposable
{
private GameClient gameClient;
private ClientPeer netClient;
/// <summary>
/// The "near" range of the voice chat (a percentage of either SpeakRange or radio range), further than this the volume starts to diminish
/// </summary>
const float RangeNear = 0.4f;
private readonly GameClient gameClient;
private readonly ClientPeer netClient;
private DateTime lastSendTime;
private List<VoipQueue> queues;
private readonly List<VoipQueue> queues;
private UInt16 storedBufferID = 0;
@@ -32,13 +35,13 @@ namespace Barotrauma.Networking
public void RegisterQueue(VoipQueue queue)
{
if (queue == VoipCapture.Instance) return;
if (!queues.Contains(queue)) queues.Add(queue);
if (queue == VoipCapture.Instance) { return; }
if (!queues.Contains(queue)) { queues.Add(queue); }
}
public void UnregisterQueue(VoipQueue queue)
{
if (queues.Contains(queue)) queues.Remove(queue);
if (queues.Contains(queue)) { queues.Remove(queue); }
}
public void SendToServer()
@@ -85,6 +88,7 @@ namespace Barotrauma.Networking
public void Read(IReadMessage msg)
{
byte queueId = msg.ReadByte();
float distanceFactor = msg.ReadRangedSingle(0.0f, 1.0f, 8);
VoipQueue queue = queues.Find(q => q.QueueID == queueId);
if (queue == null)
@@ -105,9 +109,12 @@ namespace Barotrauma.Networking
client.VoipSound = new VoipSound(client.Name, GameMain.SoundManager, client.VoipQueue);
}
GameMain.SoundManager.ForceStreamUpdate();
client.RadioNoise = 0.0f;
if (client.Character != null && !client.Character.IsDead && !client.Character.Removed && client.Character.SpeechImpediment <= 100.0f)
{
float speechImpedimentMultiplier = 1.0f - client.Character.SpeechImpediment / 100.0f;
bool spectating = Character.Controlled == null;
float rangeMultiplier = spectating ? 2.0f : 1.0f;
WifiComponent radio = null;
var messageType = !client.VoipQueue.ForceLocal && ChatMessage.CanUseRadio(client.Character, out radio) ? ChatMessageType.Radio : ChatMessageType.Default;
client.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]);
@@ -115,11 +122,17 @@ namespace Barotrauma.Networking
client.VoipSound.UseRadioFilter = messageType == ChatMessageType.Radio && !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters;
if (messageType == ChatMessageType.Radio)
{
client.VoipSound.SetRange(radio.Range * 0.8f, radio.Range);
client.VoipSound.SetRange(radio.Range * RangeNear * speechImpedimentMultiplier * rangeMultiplier, radio.Range * speechImpedimentMultiplier * rangeMultiplier);
if (distanceFactor > RangeNear && !spectating)
{
//noise starts increasing exponentially after 40% range
client.RadioNoise = MathF.Pow(MathUtils.InverseLerp(RangeNear, 1.0f, distanceFactor), 2);
}
}
else
{
client.VoipSound.SetRange(ChatMessage.SpeakRange * 0.4f, ChatMessage.SpeakRange);
client.VoipSound.SetRange(ChatMessage.SpeakRange * RangeNear * speechImpedimentMultiplier * rangeMultiplier, ChatMessage.SpeakRange * speechImpedimentMultiplier * rangeMultiplier);
}
client.VoipSound.UseMuffleFilter =
messageType != ChatMessageType.Radio && Character.Controlled != null && !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters &&

View File

@@ -82,6 +82,9 @@ namespace Barotrauma.Particles
[Editable, Serialize(false, IsPropertySaveable.Yes)]
public bool CopyEntityAngle { get; set; }
[Editable, Serialize(true, IsPropertySaveable.Yes, description: "Should the entity heading direction be applied to the particle rotation? Only affects after flipping the texture and when CopyEntityAngle is true.")]
public bool CopyEntityDir { get; set; }
[Editable, Serialize("1,1,1,1", IsPropertySaveable.Yes)]
public Color ColorMultiplier { get; set; }

View File

@@ -59,6 +59,7 @@ namespace Barotrauma
switch (MouseButton)
{
case MouseButton.None:
if (Key == Keys.None) { return false; }
return PlayerInput.KeyDown(Key);
case MouseButton.PrimaryMouse:
return PlayerInput.PrimaryMouseButtonHeld();
@@ -88,6 +89,7 @@ namespace Barotrauma
switch (MouseButton)
{
case MouseButton.None:
if (Key == Keys.None) { return false; }
return PlayerInput.KeyHit(Key);
case MouseButton.PrimaryMouse:
return PlayerInput.PrimaryMouseButtonClicked();

View File

@@ -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
}

View File

@@ -104,6 +104,7 @@ namespace Barotrauma
public struct CampaignSettingElements
{
public SettingValue<bool> TutorialEnabled;
public SettingValue<bool> RadiationEnabled;
public SettingValue<int> MaxMissionCount;
public SettingValue<StartingBalanceAmount> 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<bool> tutorialEnabled = isSinglePlayer ?
CreateTickbox(settingsList.Content, TextManager.Get("CampaignOption.EnableTutorial"), TextManager.Get("campaignoption.enabletutorial.tooltip"), prevSettings.TutorialEnabled, verticalSize) :
new SettingValue<bool>(() => false, b => { });
SettingValue<bool> radiationEnabled = CreateTickbox(settingsList.Content, TextManager.Get("CampaignOption.EnableRadiation"), TextManager.Get("campaignoption.enableradiation.tooltip"), prevSettings.RadiationEnabled, verticalSize);
ImmutableArray<SettingCarouselElement<Identifier>> startingSetOptions = StartItemSet.Sets.OrderBy(s => s.Order).Select(set => new SettingCarouselElement<Identifier>(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,

View File

@@ -35,18 +35,18 @@ namespace Barotrauma
};
// New game
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("SaveName"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft);
saveNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, 20) }, string.Empty)
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, GUI.IntScale(24)) }, TextManager.Get("SaveName"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft);
saveNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform), string.Empty)
{
textFilterFunction = ToolBox.RemoveInvalidFileNameChars
};
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("MapSeed"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft);
seedBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, 20) }, ToolBox.RandomSeed(8));
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, GUI.IntScale(24)) }, TextManager.Get("MapSeed"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft);
seedBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform), ToolBox.RandomSeed(8));
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);

View File

@@ -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;
}

View File

@@ -472,7 +472,12 @@ namespace Barotrauma
{
TextGetter = () =>
{
return TextManager.AddPunctuation(':', TextManager.Get("Missions"), $"{Campaign.NumberOfMissionsAtLocation(destination)}/{Campaign.Settings.TotalMaxMissionCount}");
int missionCount = 0;
if (GameMain.GameSession != null && Campaign.Map?.CurrentLocation?.SelectedMissions != null)
{
missionCount = Campaign.Map.CurrentLocation.SelectedMissions.Count(m => m.Locations.Contains(location) && !GameMain.GameSession.Missions.Contains(m));
}
return TextManager.AddPunctuation(':', TextManager.Get("Missions"), $"{missionCount}/{Campaign.Settings.TotalMaxMissionCount}");
}
};

View File

@@ -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();
@@ -1765,9 +1766,15 @@ namespace Barotrauma.CharacterEditor
var modProject = new ModProject(contentPackage);
var newFile = ModProject.File.FromPath<CharacterFile>(configFilePath);
modProject.AddFile(newFile);
modProject.Save(contentPackage.Path);
contentPackage = ContentPackageManager.ReloadContentPackage(contentPackage);
var reloadResult = ContentPackageManager.ReloadContentPackage(contentPackage);
if (!reloadResult.TryUnwrapSuccess(out var newPackage))
{
throw new Exception($"Failed to reload package",
reloadResult.TryUnwrapFailure(out var exception) ? exception : null);
}
contentPackage = newPackage;
DebugConsole.NewMessage(GetCharacterEditorTranslation("ContentPackageSaved").Replace("[path]", contentPackage.Path));
@@ -3181,10 +3188,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 +3213,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,

View File

@@ -101,7 +101,7 @@ namespace Barotrauma.CharacterEditor
{
bool isSamePackage = contentPackage.GetFiles<CharacterFile>().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<int, string> idToCodeName = new Dictionary<int, string>();
protected void ParseRagdollFromHTML(string path, Action<int, string, LimbType, Rectangle> limbCallback = null, Action<int, int, Vector2, Vector2, string> 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[] { "<div", "</div>", Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)
.Where(s => s.Contains("left") && s.Contains("top") && s.Contains("width") && s.Contains("height"));
int id = 0;
Dictionary<string, int> hierarchyToID = new Dictionary<string, int>();
Dictionary<int, string> idToHierarchy = new Dictionary<int, string>();
Dictionary<int, string> idToPositionCode = new Dictionary<int, string>();
Dictionary<int, string> idToName = new Dictionary<int, string>();
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;

View File

@@ -16,8 +16,8 @@ namespace Barotrauma
GameMain.LightManager.LosEnabled = true;
Hull.EditFire = false;
Hull.EditWater = false;
#endif
HumanAIController.DisableCrewAI = false;
#endif
}
protected virtual void DeselectEditorSpecific() { }

Some files were not shown because too many files have changed in this diff Show More