Unstable 1.2.4.0

This commit is contained in:
Markus Isberg
2023-11-30 13:53:00 +02:00
parent 8a2e2ea0ae
commit fb5ea537bf
210 changed files with 4201 additions and 1283 deletions
@@ -6,7 +6,7 @@ using System;
namespace Barotrauma namespace Barotrauma
{ {
class Camera : IDisposable class Camera
{ {
public static bool FollowSub = true; public static bool FollowSub = true;
@@ -147,21 +147,10 @@ namespace Barotrauma
position = Vector2.Zero; position = Vector2.Zero;
CreateMatrices(); CreateMatrices();
// TODO: this has the potential to cause a resource leak
// by sneakily creating a reference to cameras that we might
// fail to release.
GameMain.Instance.ResolutionChanged += CreateMatrices;
UpdateTransform(false); UpdateTransform(false);
} }
private bool disposed = false;
public void Dispose()
{
if (!disposed) { GameMain.Instance.ResolutionChanged -= CreateMatrices; }
disposed = true;
}
public Vector2 TargetPos { get; set; } public Vector2 TargetPos { get; set; }
public Vector2 GetPosition() public Vector2 GetPosition()
@@ -207,6 +196,12 @@ namespace Barotrauma
public void UpdateTransform(bool interpolate = true, bool updateListener = true) public void UpdateTransform(bool interpolate = true, bool updateListener = true)
{ {
if (GameMain.GraphicsWidth != Resolution.X ||
GameMain.GraphicsHeight != Resolution.Y)
{
CreateMatrices();
}
Vector2 interpolatedPosition = interpolate ? Timing.Interpolate(prevPosition, position) : position; Vector2 interpolatedPosition = interpolate ? Timing.Interpolate(prevPosition, position) : position;
float interpolatedZoom = interpolate ? Timing.Interpolate(prevZoom, zoom) : zoom; float interpolatedZoom = interpolate ? Timing.Interpolate(prevZoom, zoom) : zoom;
@@ -136,6 +136,7 @@ namespace Barotrauma
set set
{ {
if (!MathUtils.IsValid(value)) { return; } if (!MathUtils.IsValid(value)) { return; }
if (this != Controlled) { return; }
if (Screen.Selected?.Cam != null) if (Screen.Selected?.Cam != null)
{ {
Screen.Selected.Cam.Shake = value; Screen.Selected.Cam.Shake = value;
@@ -521,22 +522,25 @@ namespace Barotrauma
if (controlled == this) if (controlled == this)
{ {
controlled = null; controlled = null;
if (!(Screen.Selected?.Cam is null)) if (Screen.Selected?.Cam is not null)
{ {
Screen.Selected.Cam.TargetPos = Vector2.Zero; Screen.Selected.Cam.TargetPos = Vector2.Zero;
Lights.LightManager.ViewTarget = null; Lights.LightManager.ViewTarget = null;
} }
} }
sounds.ForEach(s => s.Sound?.Dispose());
sounds.Clear();
if (GameMain.GameSession?.CrewManager != null && if (GameMain.GameSession?.CrewManager != null &&
GameMain.GameSession.CrewManager.GetCharacters().Contains(this)) GameMain.GameSession.CrewManager.GetCharacters().Contains(this))
{ {
GameMain.GameSession.CrewManager.RemoveCharacter(this); GameMain.GameSession.CrewManager.RemoveCharacter(this);
} }
if (GameMain.Client?.Character == this) GameMain.Client.Character = null; if (GameMain.Client?.Character == this) { GameMain.Client.Character = null; }
if (Lights.LightManager.ViewTarget == this) Lights.LightManager.ViewTarget = null; if (Lights.LightManager.ViewTarget == this) { Lights.LightManager.ViewTarget = null; }
} }
@@ -894,7 +894,7 @@ namespace Barotrauma
if (!orderIndicatorCount.ContainsKey(target)) { orderIndicatorCount.Add(target, 0); } if (!orderIndicatorCount.ContainsKey(target)) { orderIndicatorCount.Add(target, 0); }
Vector2 drawPos = target is Entity ? (target as Entity).DrawPosition : Vector2 drawPos = target is Entity entity ? entity.DrawPosition :
target.Submarine == null ? target.Position : target.Position + target.Submarine.DrawPosition; target.Submarine == null ? target.Position : target.Position + target.Submarine.DrawPosition;
drawPos += Vector2.UnitX * order.SymbolSprite.size.X * 1.5f * orderIndicatorCount[target]; drawPos += Vector2.UnitX * order.SymbolSprite.size.X * 1.5f * orderIndicatorCount[target];
GUI.DrawIndicator(spriteBatch, drawPos, cam, 100.0f, order.SymbolSprite, order.Color * iconAlpha, GUI.DrawIndicator(spriteBatch, drawPos, cam, 100.0f, order.SymbolSprite, order.Color * iconAlpha,
@@ -217,6 +217,7 @@ namespace Barotrauma
if ((int)newLevel > (int)prevLevel) if ((int)newLevel > (int)prevLevel)
{ {
Character.Controlled?.SelectedItem?.OnPlayerSkillsChanged();
int increase = Math.Max((int)newLevel - (int)prevLevel, 1); int increase = Math.Max((int)newLevel - (int)prevLevel, 1);
Character?.AddMessage( Character?.AddMessage(
@@ -518,7 +519,7 @@ namespace Barotrauma
attachment.Sprite.Draw(spriteBatch, drawPos, color ?? Color.White, origin, rotate: 0, scale: scale, depth: depth, spriteEffect: spriteEffects); attachment.Sprite.Draw(spriteBatch, drawPos, color ?? Color.White, origin, rotate: 0, scale: scale, depth: depth, spriteEffect: spriteEffects);
} }
public static CharacterInfo ClientRead(Identifier speciesName, IReadMessage inc) public static CharacterInfo ClientRead(Identifier speciesName, IReadMessage inc, bool requireJobPrefabFound = true)
{ {
ushort infoID = inc.ReadUInt16(); ushort infoID = inc.ReadUInt16();
string newName = inc.ReadString(); string newName = inc.ReadString();
@@ -554,14 +555,19 @@ namespace Barotrauma
if (jobIdentifier > 0) if (jobIdentifier > 0)
{ {
jobPrefab = JobPrefab.Prefabs.Find(jp => jp.UintIdentifier == jobIdentifier); jobPrefab = JobPrefab.Prefabs.Find(jp => jp.UintIdentifier == jobIdentifier);
if (jobPrefab == null) if (jobPrefab == null && requireJobPrefabFound)
{ {
throw new Exception($"Error while reading {nameof(CharacterInfo)} received from the server: could not find a job prefab with the identifier \"{jobIdentifier}\"."); throw new Exception($"Error while reading {nameof(CharacterInfo)} received from the server: could not find a job prefab with the identifier \"{jobIdentifier}\".");
} }
foreach (SkillPrefab skillPrefab in jobPrefab.Skills.OrderBy(s => s.Identifier)) byte skillCount = inc.ReadByte();
List<SkillPrefab> jobSkills = jobPrefab?.Skills.OrderBy(s => s.Identifier).ToList();
for (int i = 0; i < skillCount; i++)
{ {
float skillLevel = inc.ReadSingle(); float skillLevel = inc.ReadSingle();
skillLevels.Add(skillPrefab.Identifier, skillLevel); if (jobSkills != null && i < jobSkills.Count)
{
skillLevels.Add(jobSkills[i].Identifier, skillLevel);
}
} }
} }
@@ -2170,6 +2170,8 @@ namespace Barotrauma
medUIExtra?.Remove(); medUIExtra?.Remove();
medUIExtra = null; medUIExtra = null;
Character.OnAttacked -= OnAttacked;
limbIndicatorOverlay?.Remove(); limbIndicatorOverlay?.Remove();
limbIndicatorOverlay = null; limbIndicatorOverlay = null;
@@ -1216,7 +1216,7 @@ namespace Barotrauma
pos: new Vector2(body.DrawPosition.X, -body.DrawPosition.Y), pos: new Vector2(body.DrawPosition.X, -body.DrawPosition.Y),
srcRect: w.Sprite.SourceRect, srcRect: w.Sprite.SourceRect,
color: Color.White, color: Color.White,
rotation: rotation, rotationRad: rotation,
origin: origin, origin: origin,
scale: new Vector2(scale, scale), scale: new Vector2(scale, scale),
effects: spriteEffect, effects: spriteEffect,
@@ -681,6 +681,7 @@ namespace Barotrauma
AssignRelayToServer("savebinds", false); AssignRelayToServer("savebinds", false);
AssignRelayToServer("spreadsheetexport", false); AssignRelayToServer("spreadsheetexport", false);
#if DEBUG #if DEBUG
AssignRelayToServer("listspamfilters", false);
AssignRelayToServer("crash", false); AssignRelayToServer("crash", false);
AssignRelayToServer("showballastflorasprite", false); AssignRelayToServer("showballastflorasprite", false);
AssignRelayToServer("simulatedlatency", false); AssignRelayToServer("simulatedlatency", false);
@@ -2234,6 +2235,30 @@ namespace Barotrauma
})); }));
#if DEBUG #if DEBUG
commands.Add(new Command("listspamfilters", "Lists filters that are in the global spam filter.", (string[] args) =>
{
if (!SpamServerFilters.GlobalSpamFilter.TryUnwrap(out var filter))
{
ThrowError("Global spam list is not initialized.");
return;
}
if (!filter.Filters.Any())
{
NewMessage("Global spam list is empty.", GUIStyle.Green);
return;
}
StringBuilder sb = new();
foreach (var f in filter.Filters)
{
sb.AppendLine(f.ToString());
}
NewMessage(sb.ToString(), GUIStyle.Green);
}));
commands.Add(new Command("setplanthealth", "setplanthealth [value]: Sets the health of the selected plant in sub editor.", (string[] args) => commands.Add(new Command("setplanthealth", "setplanthealth [value]: Sets the health of the selected plant in sub editor.", (string[] args) =>
{ {
if (1 > args.Length || Screen.Selected != GameMain.SubEditorScreen) { return; } if (1 > args.Length || Screen.Selected != GameMain.SubEditorScreen) { return; }
@@ -3094,7 +3119,7 @@ namespace Barotrauma
int i = 0; int i = 0;
foreach (LocationConnection connection in campaign.Map.CurrentLocation.Connections) foreach (LocationConnection connection in campaign.Map.CurrentLocation.Connections)
{ {
NewMessage(" " + i + ". " + connection.OtherLocation(campaign.Map.CurrentLocation).Name, Color.White); NewMessage(" " + i + ". " + connection.OtherLocation(campaign.Map.CurrentLocation).DisplayName, Color.White);
i++; i++;
} }
ShowQuestionPrompt("Select a destination (0 - " + (campaign.Map.CurrentLocation.Connections.Count - 1) + "):", (string selectedDestination) => ShowQuestionPrompt("Select a destination (0 - " + (campaign.Map.CurrentLocation.Connections.Count - 1) + "):", (string selectedDestination) =>
@@ -16,6 +16,11 @@ partial class EventObjectiveAction : EventAction
int width = 450, int width = 450,
int height = 80) int height = 80)
{ {
if (Type == SegmentActionType.AddIfNotFound)
{
if (ObjectiveManager.IsSegmentActive(Identifier)) { return; }
}
ObjectiveManager.Segment? segment = null; ObjectiveManager.Segment? segment = null;
// Only need to create the segment when it's being triggered (otherwise the tutorial already has the segment instance) // Only need to create the segment when it's being triggered (otherwise the tutorial already has the segment instance)
if (Type == SegmentActionType.Trigger) if (Type == SegmentActionType.Trigger)
@@ -24,7 +29,8 @@ partial class EventObjectiveAction : EventAction
new ObjectiveManager.Segment.Text(TextTag, width, height, Anchor.Center), new ObjectiveManager.Segment.Text(TextTag, width, height, Anchor.Center),
new ObjectiveManager.Segment.Video(videoFile, TextTag, width, height)); new ObjectiveManager.Segment.Video(videoFile, TextTag, width, height));
} }
else if (Type == SegmentActionType.Add) else if (Type == SegmentActionType.Add ||
Type == SegmentActionType.AddIfNotFound)
{ {
segment = ObjectiveManager.Segment.CreateObjectiveSegment(Identifier, !ObjectiveTag.IsEmpty ? ObjectiveTag : Identifier); segment = ObjectiveManager.Segment.CreateObjectiveSegment(Identifier, !ObjectiveTag.IsEmpty ? ObjectiveTag : Identifier);
} }
@@ -33,10 +39,12 @@ partial class EventObjectiveAction : EventAction
segment.CanBeCompleted = CanBeCompleted; segment.CanBeCompleted = CanBeCompleted;
segment.ParentId = ParentObjectiveId; segment.ParentId = ParentObjectiveId;
} }
switch (Type) switch (Type)
{ {
case SegmentActionType.Trigger: case SegmentActionType.Trigger:
case SegmentActionType.Add: case SegmentActionType.Add:
case SegmentActionType.AddIfNotFound:
ObjectiveManager.TriggerSegment(segment); ObjectiveManager.TriggerSegment(segment);
break; break;
case SegmentActionType.Complete: case SegmentActionType.Complete:
@@ -1,22 +1,19 @@
#nullable enable
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma; namespace Barotrauma;
partial class TutorialHighlightAction : EventAction partial class HighlightAction : EventAction
{ {
private static readonly Color highlightColor = Color.Orange; partial void SetHighlightProjSpecific(Entity entity, IEnumerable<Character>? targetCharacters)
partial void UpdateProjSpecific()
{ {
if (GameMain.GameSession?.GameMode is not TutorialMode) { return; } if (targetCharacters != null && !targetCharacters.Contains(Character.Controlled))
foreach (var target in ParentEvent.GetTargets(TargetTag))
{ {
SetHighlight(target); return;
} }
}
private void SetHighlight(Entity entity)
{
if (entity is Item i) if (entity is Item i)
{ {
SetItemHighlight(i); SetItemHighlight(i);
@@ -581,7 +581,14 @@ namespace Barotrauma
StatusEffect effect = StatusEffect.Load(subElement, $"EventManager.ClientRead ({eventIdentifier})"); StatusEffect effect = StatusEffect.Load(subElement, $"EventManager.ClientRead ({eventIdentifier})");
foreach (Entity target in targets) foreach (Entity target in targets)
{ {
effect.Apply(effect.type, 1.0f, target, target as ISerializableEntity); if (target is Item item)
{
effect.Apply(effect.type, 1.0f, item, item.AllPropertyObjects);
}
else
{
effect.Apply(effect.type, 1.0f, target, target as ISerializableEntity);
}
} }
} }
break; break;
@@ -126,7 +126,13 @@ namespace Barotrauma
void GiveMissionExperience(CharacterInfo info) void GiveMissionExperience(CharacterInfo info)
{ {
if (info == null) { return; } if (info == null) { return; }
var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f); var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f, info.Character);
//check if anyone else in the crew has talents that could give a bonus to this one
foreach (var c in crew)
{
if (c == info.Character) { continue; }
c.CheckTalents(AbilityEffectType.OnAllyGainMissionExperience, experienceGainMultiplierIndividual);
}
info.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual); info.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual);
info.GiveExperience((int)(experienceGain * experienceGainMultiplierIndividual.Value)); info.GiveExperience((int)(experienceGain * experienceGainMultiplierIndividual.Value));
} }
@@ -247,30 +247,33 @@ namespace Barotrauma
UpdateCrew(); UpdateCrew();
} }
public void UpdateHireables()
{
UpdateHireables(campaign?.CurrentLocation);
}
private void UpdateHireables(Location location) private void UpdateHireables(Location location)
{ {
if (hireableList != null) if (hireableList == null) { return; }
hireableList.Content.Children.ToList().ForEach(c => hireableList.RemoveChild(c));
var hireableCharacters = location.GetHireableCharacters();
if (hireableCharacters.None())
{ {
hireableList.Content.Children.ToList().ForEach(c => hireableList.RemoveChild(c)); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), hireableList.Content.RectTransform), TextManager.Get("HireUnavailable"), textAlignment: Alignment.Center)
var hireableCharacters = location.GetHireableCharacters();
if (hireableCharacters.None())
{ {
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), hireableList.Content.RectTransform), TextManager.Get("HireUnavailable"), textAlignment: Alignment.Center) CanBeFocused = false
{ };
CanBeFocused = false
};
}
else
{
foreach (CharacterInfo c in hireableCharacters)
{
if (c == null) { continue; }
CreateCharacterFrame(c, hireableList);
}
}
sortingDropDown.SelectItem(SortingMethod.JobAsc);
hireableList.UpdateScrollBarSize();
} }
else
{
foreach (CharacterInfo c in hireableCharacters)
{
if (c == null) { continue; }
CreateCharacterFrame(c, hireableList);
}
}
sortingDropDown.SelectItem(SortingMethod.JobAsc);
hireableList.UpdateScrollBarSize();
} }
public void SetHireables(Location location, List<CharacterInfo> availableHires) public void SetHireables(Location location, List<CharacterInfo> availableHires)
@@ -434,7 +437,7 @@ namespace Barotrauma
if (listBox != crewList) if (listBox != crewList)
{ {
new GUITextBlock(new RectTransform(new Vector2(width, 1.0f), mainGroup.RectTransform), new GUITextBlock(new RectTransform(new Vector2(width, 1.0f), mainGroup.RectTransform),
TextManager.FormatCurrency(characterInfo.Salary), TextManager.FormatCurrency(HireManager.GetSalaryFor(characterInfo)),
textAlignment: Alignment.Center) textAlignment: Alignment.Center)
{ {
CanBeFocused = false CanBeFocused = false
@@ -692,11 +695,8 @@ namespace Barotrauma
private void SetTotalHireCost() private void SetTotalHireCost()
{ {
if (pendingList == null || totalBlock == null || validateHiresButton == null) { return; } if (pendingList == null || totalBlock == null || validateHiresButton == null) { return; }
int total = 0; var infos = pendingList.Content.Children.Select(static c => ((InfoSkill)c.UserData).CharacterInfo).ToArray();
pendingList.Content.Children.ForEach(c => int total = HireManager.GetSalaryFor(infos);
{
total += ((InfoSkill)c.UserData).CharacterInfo.Salary;
});
totalBlock.Text = TextManager.FormatCurrency(total); totalBlock.Text = TextManager.FormatCurrency(total);
bool enoughMoney = campaign == null || campaign.CanAfford(total); bool enoughMoney = campaign == null || campaign.CanAfford(total);
totalBlock.TextColor = enoughMoney ? Color.White : Color.Red; totalBlock.TextColor = enoughMoney ? Color.White : Color.Red;
@@ -718,14 +718,14 @@ namespace Barotrauma
if (nonDuplicateHires.None()) { return false; } if (nonDuplicateHires.None()) { return false; }
int total = nonDuplicateHires.Aggregate(0, (total, info) => total + info.Salary); int total = HireManager.GetSalaryFor(nonDuplicateHires);
if (!campaign.CanAfford(total)) { return false; } if (!campaign.CanAfford(total)) { return false; }
bool atLeastOneHired = false; bool atLeastOneHired = false;
foreach (CharacterInfo ci in nonDuplicateHires) foreach (CharacterInfo ci in nonDuplicateHires)
{ {
if (campaign.TryHireCharacter(campaign.Map.CurrentLocation, ci)) if (campaign.TryHireCharacter(campaign.Map.CurrentLocation, ci, Character.Controlled))
{ {
atLeastOneHired = true; atLeastOneHired = true;
} }
@@ -741,7 +741,7 @@ namespace Barotrauma
SelectCharacter(null, null, null); SelectCharacter(null, null, null);
var dialog = new GUIMessageBox( var dialog = new GUIMessageBox(
TextManager.Get("newcrewmembers"), TextManager.Get("newcrewmembers"),
TextManager.GetWithVariable("crewhiredmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.Name), TextManager.GetWithVariable("crewhiredmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.DisplayName),
new LocalizedString[] { TextManager.Get("Ok") }); new LocalizedString[] { TextManager.Get("Ok") });
dialog.Buttons[0].OnClicked += dialog.Close; dialog.Buttons[0].OnClicked += dialog.Close;
} }
@@ -512,10 +512,18 @@ namespace Barotrauma
soundStr += " (stopped)"; soundStr += " (stopped)";
clr *= 0.5f; clr *= 0.5f;
} }
else if (playingSoundChannel.Muffled) else
{ {
soundStr += " (muffled)"; if (playingSoundChannel.Muffled)
clr = Color.Lerp(clr, Color.LightGray, 0.5f); {
soundStr += " (muffled)";
clr = Color.Lerp(clr, Color.LightGray, 0.5f);
}
if (playingSoundChannel.FadingOutAndDisposing)
{
soundStr += ". Fading out...";
clr = Color.Lerp(clr, Color.Black, 0.15f);
}
} }
} }
@@ -2163,10 +2171,10 @@ namespace Barotrauma
}; };
} }
public static GUIMessageBox AskForConfirmation(LocalizedString header, LocalizedString body, Action onConfirm, Action onDeny = null) public static GUIMessageBox AskForConfirmation(LocalizedString header, LocalizedString body, Action onConfirm, Action onDeny = null, Vector2? relativeSize = null, Point? minSize = null)
{ {
LocalizedString[] buttons = { TextManager.Get("Ok"), TextManager.Get("Cancel") }; LocalizedString[] buttons = { TextManager.Get("Ok"), TextManager.Get("Cancel") };
GUIMessageBox msgBox = new GUIMessageBox(header, body, buttons, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175)); GUIMessageBox msgBox = new GUIMessageBox(header, body, buttons, relativeSize: relativeSize ?? new Vector2(0.2f, 0.175f), minSize: minSize ?? new Point(300, 175));
// Cancel button // Cancel button
msgBox.Buttons[1].OnClicked = delegate msgBox.Buttons[1].OnClicked = delegate
@@ -775,23 +775,30 @@ namespace Barotrauma
toolTipBlock.UserData = toolTip; toolTipBlock.UserData = toolTip;
} }
toolTipBlock.RectTransform.AbsoluteOffset = CalculateOffset();
RectTransform.CalculateAnchorPoint(anchor, targetElement) +
RectTransform.CalculatePivotOffset(pivot, toolTipBlock.RectTransform.NonScaledSize);
if (toolTipBlock.Rect.Right > GameMain.GraphicsWidth - 10) if (toolTipBlock.Rect.Right > GameMain.GraphicsWidth - 10)
{ {
toolTipBlock.RectTransform.AbsoluteOffset -= new Point(toolTipBlock.Rect.Width + targetElement.Width, 0); anchor = RectTransform.MoveAnchorLeft(anchor);
pivot = (Pivot)RectTransform.MoveAnchorRight((Anchor)pivot);
CalculateOffset();
} }
if (toolTipBlock.Rect.Bottom > GameMain.GraphicsHeight - 10) if (toolTipBlock.Rect.Bottom > GameMain.GraphicsHeight - 10)
{ {
toolTipBlock.RectTransform.AbsoluteOffset -= new Point( anchor = RectTransform.MoveAnchorTop(anchor);
0, pivot = (Pivot)RectTransform.MoveAnchorBottom((Anchor)pivot);
toolTipBlock.Rect.Bottom - (GameMain.GraphicsHeight - 10)); CalculateOffset();
} }
toolTipBlock.SetTextPos(); toolTipBlock.SetTextPos();
toolTipBlock.DrawManually(spriteBatch); toolTipBlock.DrawManually(spriteBatch);
void CalculateOffset()
{
toolTipBlock.RectTransform.AbsoluteOffset =
RectTransform.CalculateAnchorPoint(anchor, targetElement) +
RectTransform.CalculatePivotOffset(pivot, toolTipBlock.RectTransform.NonScaledSize);
}
} }
#endregion #endregion
@@ -313,13 +313,18 @@ namespace Barotrauma
public class GUIColor : GUISelector<GUIColorPrefab> public class GUIColor : GUISelector<GUIColorPrefab>
{ {
public GUIColor(string identifier) : base(identifier) { } private readonly Color fallbackColor;
public GUIColor(string identifier, Color fallbackColor) : base(identifier)
{
this.fallbackColor = fallbackColor;
}
public Color Value public Color Value
{ {
get get
{ {
return Prefabs.ActivePrefab.Color; return Prefabs?.ActivePrefab?.Color ?? fallbackColor;
} }
} }
@@ -75,72 +75,72 @@ namespace Barotrauma
/// <summary> /// <summary>
/// General green color used for elements whose colors are set from code /// General green color used for elements whose colors are set from code
/// </summary> /// </summary>
public readonly static GUIColor Green = new GUIColor("Green"); public readonly static GUIColor Green = new GUIColor("Green", new Color(154, 213, 163, 255));
/// <summary> /// <summary>
/// General red color used for elements whose colors are set from code /// General red color used for elements whose colors are set from code
/// </summary> /// </summary>
public readonly static GUIColor Orange = new GUIColor("Orange"); public readonly static GUIColor Orange = new GUIColor("Orange", new Color(243, 162, 50, 255));
/// <summary> /// <summary>
/// General red color used for elements whose colors are set from code /// General red color used for elements whose colors are set from code
/// </summary> /// </summary>
public readonly static GUIColor Red = new GUIColor("Red"); public readonly static GUIColor Red = new GUIColor("Red", new Color(245, 105, 105, 255));
/// <summary> /// <summary>
/// General blue color used for elements whose colors are set from code /// General blue color used for elements whose colors are set from code
/// </summary> /// </summary>
public readonly static GUIColor Blue = new GUIColor("Blue"); public readonly static GUIColor Blue = new GUIColor("Blue", new Color(126, 211, 224, 255));
/// <summary> /// <summary>
/// General yellow color used for elements whose colors are set from code /// General yellow color used for elements whose colors are set from code
/// </summary> /// </summary>
public readonly static GUIColor Yellow = new GUIColor("Yellow"); public readonly static GUIColor Yellow = new GUIColor("Yellow", new Color(255, 255, 0, 255));
/// <summary> /// <summary>
/// Color to display the name of modded servers in the server list. /// Color to display the name of modded servers in the server list.
/// </summary> /// </summary>
public readonly static GUIColor ModdedServerColor = new GUIColor("ModdedServerColor"); public readonly static GUIColor ModdedServerColor = new GUIColor("ModdedServerColor", new Color(154, 185, 160, 255));
public readonly static GUIColor ColorInventoryEmpty = new GUIColor("ColorInventoryEmpty"); public readonly static GUIColor ColorInventoryEmpty = new GUIColor("ColorInventoryEmpty", new Color(245, 105, 105, 255));
public readonly static GUIColor ColorInventoryHalf = new GUIColor("ColorInventoryHalf"); public readonly static GUIColor ColorInventoryHalf = new GUIColor("ColorInventoryHalf", new Color(243, 162, 50, 255));
public readonly static GUIColor ColorInventoryFull = new GUIColor("ColorInventoryFull"); public readonly static GUIColor ColorInventoryFull = new GUIColor("ColorInventoryFull", new Color(96, 222, 146, 255));
public readonly static GUIColor ColorInventoryBackground = new GUIColor("ColorInventoryBackground"); public readonly static GUIColor ColorInventoryBackground = new GUIColor("ColorInventoryBackground", new Color(56, 56, 56, 255));
public readonly static GUIColor ColorInventoryEmptyOverlay = new GUIColor("ColorInventoryEmptyOverlay"); public readonly static GUIColor ColorInventoryEmptyOverlay = new GUIColor("ColorInventoryEmptyOverlay", new Color(125, 125, 125, 255));
public readonly static GUIColor TextColorNormal = new GUIColor("TextColorNormal"); public readonly static GUIColor TextColorNormal = new GUIColor("TextColorNormal", new Color(228, 217, 167, 255));
public readonly static GUIColor TextColorBright = new GUIColor("TextColorBright"); public readonly static GUIColor TextColorBright = new GUIColor("TextColorBright", new Color(255, 255, 255, 255));
public readonly static GUIColor TextColorDark = new GUIColor("TextColorDark"); public readonly static GUIColor TextColorDark = new GUIColor("TextColorDark", new Color(0, 0, 0, 230));
public readonly static GUIColor TextColorDim = new GUIColor("TextColorDim"); public readonly static GUIColor TextColorDim = new GUIColor("TextColorDim", new Color(153, 153, 153, 153));
public readonly static GUIColor ItemQualityColorPoor = new GUIColor("ItemQualityColorPoor"); public readonly static GUIColor ItemQualityColorPoor = new GUIColor("ItemQualityColorPoor", new Color(128, 128, 128, 255));
public readonly static GUIColor ItemQualityColorNormal = new GUIColor("ItemQualityColorNormal"); public readonly static GUIColor ItemQualityColorNormal = new GUIColor("ItemQualityColorNormal", new Color(255, 255, 255, 255));
public readonly static GUIColor ItemQualityColorGood = new GUIColor("ItemQualityColorGood"); public readonly static GUIColor ItemQualityColorGood = new GUIColor("ItemQualityColorGood", new Color(144, 238, 144, 255));
public readonly static GUIColor ItemQualityColorExcellent = new GUIColor("ItemQualityColorExcellent"); public readonly static GUIColor ItemQualityColorExcellent = new GUIColor("ItemQualityColorExcellent", new Color(173, 216, 230, 255));
public readonly static GUIColor ItemQualityColorMasterwork = new GUIColor("ItemQualityColorMasterwork"); public readonly static GUIColor ItemQualityColorMasterwork = new GUIColor("ItemQualityColorMasterwork", new Color(147, 112, 219, 255));
public readonly static GUIColor ColorReputationVeryLow = new GUIColor("ColorReputationVeryLow"); public readonly static GUIColor ColorReputationVeryLow = new GUIColor("ColorReputationVeryLow", new Color(192, 60, 60, 255));
public readonly static GUIColor ColorReputationLow = new GUIColor("ColorReputationLow"); public readonly static GUIColor ColorReputationLow = new GUIColor("ColorReputationLow", new Color(203, 145, 23, 255));
public readonly static GUIColor ColorReputationNeutral = new GUIColor("ColorReputationNeutral"); public readonly static GUIColor ColorReputationNeutral = new GUIColor("ColorReputationNeutral", new Color(228, 217, 167, 255));
public readonly static GUIColor ColorReputationHigh = new GUIColor("ColorReputationHigh"); public readonly static GUIColor ColorReputationHigh = new GUIColor("ColorReputationHigh", new Color(51, 152, 64, 255));
public readonly static GUIColor ColorReputationVeryHigh = new GUIColor("ColorReputationVeryHigh"); public readonly static GUIColor ColorReputationVeryHigh = new GUIColor("ColorReputationVeryHigh", new Color(71, 160, 164, 255));
// Inventory // Inventory
public readonly static GUIColor EquipmentSlotIconColor = new GUIColor("EquipmentSlotIconColor"); public readonly static GUIColor EquipmentSlotIconColor = new GUIColor("EquipmentSlotIconColor", new Color(99, 70, 64, 255));
// Health HUD // Health HUD
public readonly static GUIColor BuffColorLow = new GUIColor("BuffColorLow"); public readonly static GUIColor BuffColorLow = new GUIColor("BuffColorLow", new Color(66, 170, 73, 255));
public readonly static GUIColor BuffColorMedium = new GUIColor("BuffColorMedium"); public readonly static GUIColor BuffColorMedium = new GUIColor("BuffColorMedium", new Color(110, 168, 118, 255));
public readonly static GUIColor BuffColorHigh = new GUIColor("BuffColorHigh"); public readonly static GUIColor BuffColorHigh = new GUIColor("BuffColorHigh", new Color(154, 213, 163, 255));
public readonly static GUIColor DebuffColorLow = new GUIColor("DebuffColorLow"); public readonly static GUIColor DebuffColorLow = new GUIColor("DebuffColorLow", new Color(243, 162, 50, 255));
public readonly static GUIColor DebuffColorMedium = new GUIColor("DebuffColorMedium"); public readonly static GUIColor DebuffColorMedium = new GUIColor("DebuffColorMedium", new Color(155, 55, 55, 255));
public readonly static GUIColor DebuffColorHigh = new GUIColor("DebuffColorHigh"); public readonly static GUIColor DebuffColorHigh = new GUIColor("DebuffColorHigh", new Color(228, 27, 27, 255));
public readonly static GUIColor HealthBarColorLow = new GUIColor("HealthBarColorLow"); public readonly static GUIColor HealthBarColorLow = new GUIColor("HealthBarColorLow", new Color(255, 0, 0, 255));
public readonly static GUIColor HealthBarColorMedium = new GUIColor("HealthBarColorMedium"); public readonly static GUIColor HealthBarColorMedium = new GUIColor("HealthBarColorMedium", new Color(255, 165, 0, 255));
public readonly static GUIColor HealthBarColorHigh = new GUIColor("HealthBarColorHigh"); public readonly static GUIColor HealthBarColorHigh = new GUIColor("HealthBarColorHigh", new Color(78, 114, 88));
public readonly static GUIColor HealthBarColorPoisoned = new GUIColor("HealthBarColorPoisoned"); public readonly static GUIColor HealthBarColorPoisoned = new GUIColor("HealthBarColorPoisoned", new Color(100, 150, 0, 255));
private readonly static Point defaultItemFrameMargin = new Point(50, 56); private readonly static Point defaultItemFrameMargin = new Point(50, 56);
@@ -461,14 +461,16 @@ namespace Barotrauma
} }
private ImmutableArray<Vector2> cachedCaretPositions = ImmutableArray<Vector2>.Empty; private ImmutableArray<Vector2> cachedCaretPositions = ImmutableArray<Vector2>.Empty;
//which text were the cached caret positions calculated for?
private string cachedCaretPositionsText;
public ImmutableArray<Vector2> GetAllCaretPositions() public ImmutableArray<Vector2> GetAllCaretPositions()
{ {
if (cachedCaretPositions.Any()) string textDrawn = Censor ? CensoredText : Text.SanitizedValue;
if (cachedCaretPositions.Any() &&
textDrawn == cachedCaretPositionsText)
{ {
return cachedCaretPositions; return cachedCaretPositions;
} }
string textDrawn = Censor ? CensoredText : Text.SanitizedValue;
float w = Wrap float w = Wrap
? (Rect.Width - Padding.X - Padding.Z) / TextScale ? (Rect.Width - Padding.X - Padding.Z) / TextScale
: float.PositiveInfinity; : float.PositiveInfinity;
@@ -482,6 +484,7 @@ namespace Barotrauma
.Select(p => p - new Vector2(alignmentXDiff, 0)) .Select(p => p - new Vector2(alignmentXDiff, 0))
.Select(p => p * TextScale + TextPos - Origin * TextScale) .Select(p => p * TextScale + TextPos - Origin * TextScale)
.ToImmutableArray(); .ToImmutableArray();
cachedCaretPositionsText = textDrawn;
return cachedCaretPositions; return cachedCaretPositions;
} }
@@ -353,6 +353,10 @@ namespace Barotrauma
{ {
CaretIndex = Math.Clamp(CaretIndex, 0, textBlock.Text.Length); CaretIndex = Math.Clamp(CaretIndex, 0, textBlock.Text.Length);
var caretPositions = textBlock.GetAllCaretPositions(); var caretPositions = textBlock.GetAllCaretPositions();
if (CaretIndex >= caretPositions.Length)
{
throw new Exception($"Caret index was outside the bounds of the calculated caret positions. Index: {CaretIndex}, caret positions: {caretPositions.Length}, text: {textBlock.Text}");
}
caretPos = caretPositions[CaretIndex]; caretPos = caretPositions[CaretIndex];
caretPosDirty = false; caretPosDirty = false;
} }
@@ -784,11 +784,95 @@ namespace Barotrauma
#region Static methods #region Static methods
public static Pivot MatchPivotToAnchor(Anchor anchor) public static Pivot MatchPivotToAnchor(Anchor anchor)
{ {
if (!Enum.TryParse(anchor.ToString(), out Pivot pivot)) return (Pivot)anchor;
}
public static Anchor MatchAnchorToPivot(Pivot pivot)
{
return (Anchor)pivot;
}
/// <summary>
/// Moves the anchor to the left, keeping the vertical position unchanged (e.g. CenterRight -> CenterLeft)
/// </summary>
public static Anchor MoveAnchorLeft(Anchor anchor)
{
switch (anchor)
{ {
throw new Exception($"[RectTransform] Cannot match pivot to anchor {anchor}"); case Anchor.TopCenter:
case Anchor.TopRight:
return Anchor.TopLeft;
case Anchor.Center:
case Anchor.CenterRight:
return Anchor.CenterLeft;
case Anchor.BottomCenter:
case Anchor.BottomRight:
return Anchor.BottomLeft;
default:
return anchor;
}
}
/// <summary>
/// Moves the anchor to the right, keeping the vertical position unchanged (e.g. CenterLeft -> CenterRight)
/// </summary>
public static Anchor MoveAnchorRight(Anchor anchor)
{
switch (anchor)
{
case Anchor.TopCenter:
case Anchor.TopLeft:
return Anchor.TopRight;
case Anchor.Center:
case Anchor.CenterLeft:
return Anchor.CenterRight;
case Anchor.BottomCenter:
case Anchor.BottomLeft:
return Anchor.BottomRight;
default:
return anchor;
}
}
/// <summary>
/// Moves the anchor to the top, keeping the horizontal position unchanged (e.g. BottomCenter -> TopCenter)
/// </summary>
public static Anchor MoveAnchorTop(Anchor anchor)
{
switch (anchor)
{
case Anchor.CenterLeft:
case Anchor.BottomLeft:
return Anchor.TopLeft;
case Anchor.Center:
case Anchor.BottomCenter:
return Anchor.TopCenter;
case Anchor.CenterRight:
case Anchor.BottomRight:
return Anchor.TopRight;
default:
return anchor;
}
}
/// <summary>
/// Moves the anchor to the bottom, keeping the horizontal position unchanged (e.g. TopCenter -> BottomCenter)
/// </summary>
public static Anchor MoveAnchorBottom(Anchor anchor)
{
switch (anchor)
{
case Anchor.CenterLeft:
case Anchor.TopLeft:
return Anchor.BottomLeft;
case Anchor.Center:
case Anchor.TopCenter:
return Anchor.BottomCenter;
case Anchor.CenterRight:
case Anchor.TopRight:
return Anchor.BottomRight;
default:
return anchor;
} }
return pivot;
} }
/// <summary> /// <summary>
@@ -811,11 +895,11 @@ namespace Barotrauma
} }
} }
public static Point CalculatePivotOffset(Pivot pivot, Point size) public static Point CalculatePivotOffset(Pivot anchor, Point size)
{ {
int width = size.X; int width = size.X;
int height = size.Y; int height = size.Y;
switch (pivot) switch (anchor)
{ {
case Pivot.TopLeft: case Pivot.TopLeft:
return Point.Zero; return Point.Zero;
@@ -836,7 +920,7 @@ namespace Barotrauma
case Pivot.BottomRight: case Pivot.BottomRight:
return new Point(-width, -height); return new Point(-width, -height);
default: default:
throw new NotImplementedException(pivot.ToString()); throw new NotImplementedException(anchor.ToString());
} }
} }
@@ -2127,11 +2127,10 @@ namespace Barotrauma
{ {
var dialog = new GUIMessageBox( var dialog = new GUIMessageBox(
TextManager.Get("newsupplies"), TextManager.Get("newsupplies"),
TextManager.GetWithVariable("suppliespurchasedmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.Name)); TextManager.GetWithVariable("suppliespurchasedmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.DisplayName));
dialog.Buttons[0].OnClicked += dialog.Close; dialog.Buttons[0].OnClicked += dialog.Close;
} }
} }
return false; return false;
} }
@@ -130,7 +130,7 @@ namespace Barotrauma
}; };
content = new GUILayoutGroup(new RectTransform(new Point(background.Rect.Width - HUDLayoutSettings.Padding * 4, background.Rect.Height - HUDLayoutSettings.Padding * 4), background.RectTransform, Anchor.Center)) { AbsoluteSpacing = (int)(HUDLayoutSettings.Padding * 1.5f) }; content = new GUILayoutGroup(new RectTransform(new Point(background.Rect.Width - HUDLayoutSettings.Padding * 4, background.Rect.Height - HUDLayoutSettings.Padding * 4), background.RectTransform, Anchor.Center)) { AbsoluteSpacing = (int)(HUDLayoutSettings.Padding * 1.5f) };
GUITextBlock header = new GUITextBlock(new RectTransform(new Vector2(1f, 0.0f), content.RectTransform), transferService ? TextManager.Get("switchsubmarineheader") : TextManager.GetWithVariable("outpostshipyard", "[location]", GameMain.GameSession.Map.CurrentLocation.Name), font: GUIStyle.LargeFont); GUITextBlock header = new GUITextBlock(new RectTransform(new Vector2(1f, 0.0f), content.RectTransform), transferService ? TextManager.Get("switchsubmarineheader") : TextManager.GetWithVariable("outpostshipyard", "[location]", GameMain.GameSession.Map.CurrentLocation.DisplayName), font: GUIStyle.LargeFont);
header.CalculateHeightFromText(0, true); header.CalculateHeightFromText(0, true);
playerBalanceElement = CampaignUI.AddBalanceElement(header, new Vector2(1.0f, 1.5f)); playerBalanceElement = CampaignUI.AddBalanceElement(header, new Vector2(1.0f, 1.5f));
@@ -165,6 +165,11 @@ namespace Barotrauma
public TabMenu() public TabMenu()
{ {
if (!initialized) { Initialize(); } if (!initialized) { Initialize(); }
if (Level.Loaded == null)
{
//make sure we're not trying to view e.g. mission or reputation info if the tab menu is opened in the test mode
SelectedTab = InfoFrameTab.Crew;
}
CreateInfoFrame(SelectedTab); CreateInfoFrame(SelectedTab);
SelectInfoFrameTab(SelectedTab); SelectInfoFrameTab(SelectedTab);
} }
@@ -303,7 +308,7 @@ namespace Barotrauma
{ {
var missionBtn = createTabButton(InfoFrameTab.Mission, "mission"); var missionBtn = createTabButton(InfoFrameTab.Mission, "mission");
eventLogNotification = GameSession.CreateNotificationIcon(missionBtn); eventLogNotification = GameSession.CreateNotificationIcon(missionBtn);
eventLogNotification.Visible = GameMain.GameSession.EventManager?.EventLog?.UnreadEntries ?? false; eventLogNotification.Visible = GameMain.GameSession?.EventManager?.EventLog?.UnreadEntries ?? false;
if (eventLogNotification.Visible) if (eventLogNotification.Visible)
{ {
eventLogNotification.Pulsate(Vector2.One, Vector2.One * 2, 1.0f); eventLogNotification.Pulsate(Vector2.One, Vector2.One * 2, 1.0f);
@@ -1508,7 +1513,7 @@ namespace Barotrauma
portraitImage.RectTransform.NonScaledSize = new Point(Math.Min((int)(portraitImage.Rect.Size.Y * portraitAspectRatio), portraitImage.Rect.Width), portraitImage.Rect.Size.Y); portraitImage.RectTransform.NonScaledSize = new Point(Math.Min((int)(portraitImage.Rect.Size.Y * portraitAspectRatio), portraitImage.Rect.Width), portraitImage.Rect.Size.Y);
} }
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), locationInfoContainer.RectTransform), location.Name, font: GUIStyle.LargeFont); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), locationInfoContainer.RectTransform), location.DisplayName, font: GUIStyle.LargeFont);
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), locationInfoContainer.RectTransform), location.Type.Name, font: GUIStyle.SubHeadingFont); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), locationInfoContainer.RectTransform), location.Type.Name, font: GUIStyle.SubHeadingFont);
if (location.Faction?.Prefab != null) if (location.Faction?.Prefab != null)
@@ -123,6 +123,10 @@ namespace Barotrauma
private Viewport defaultViewport; private Viewport defaultViewport;
/// <summary>
/// NOTE: Use very carefully. You need to ensure that you ALWAYS unsubscribe from this when you no longer need the subscriber!
/// If you're subscribing to this from something else than a singleton or something that there's only ever one instance of, you're probably in dangerous territory.
/// </summary>
public event Action ResolutionChanged; public event Action ResolutionChanged;
private bool exiting; private bool exiting;
@@ -404,7 +408,7 @@ namespace Barotrauma
//do this here because we need it for the loading screen //do this here because we need it for the loading screen
WaterRenderer.Instance = new WaterRenderer(base.GraphicsDevice); WaterRenderer.Instance = new WaterRenderer(base.GraphicsDevice);
Quad.Init(GraphicsDevice); GraphicsQuad.Init(GraphicsDevice);
loadingScreenOpen = true; loadingScreenOpen = true;
TitleScreen = new LoadingScreen(GraphicsDevice) TitleScreen = new LoadingScreen(GraphicsDevice)
@@ -192,12 +192,12 @@ namespace Barotrauma
if (Level.Loaded.EndOutpost == null || !Level.Loaded.EndOutpost.DockedTo.Contains(leavingSub)) if (Level.Loaded.EndOutpost == null || !Level.Loaded.EndOutpost.DockedTo.Contains(leavingSub))
{ {
string textTag = availableTransition == TransitionType.ProgressToNextLocation ? "EnterLocation" : "EnterEmptyLocation"; string textTag = availableTransition == TransitionType.ProgressToNextLocation ? "EnterLocation" : "EnterEmptyLocation";
buttonText = TextManager.GetWithVariable(textTag, "[locationname]", Level.Loaded.EndLocation?.Name ?? "[ERROR]"); buttonText = TextManager.GetWithVariable(textTag, "[locationname]", Level.Loaded.EndLocation?.DisplayName ?? "[ERROR]");
allowEndingRound = !ForceMapUI && !ShowCampaignUI; allowEndingRound = !ForceMapUI && !ShowCampaignUI;
} }
break; break;
case TransitionType.LeaveLocation: case TransitionType.LeaveLocation:
buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.DisplayName ?? "[ERROR]");
allowEndingRound = !ForceMapUI && !ShowCampaignUI; allowEndingRound = !ForceMapUI && !ShowCampaignUI;
break; break;
case TransitionType.ReturnToPreviousLocation: case TransitionType.ReturnToPreviousLocation:
@@ -205,7 +205,7 @@ namespace Barotrauma
if (Level.Loaded.StartOutpost == null || !Level.Loaded.StartOutpost.DockedTo.Contains(leavingSub)) if (Level.Loaded.StartOutpost == null || !Level.Loaded.StartOutpost.DockedTo.Contains(leavingSub))
{ {
string textTag = availableTransition == TransitionType.ReturnToPreviousLocation ? "EnterLocation" : "EnterEmptyLocation"; string textTag = availableTransition == TransitionType.ReturnToPreviousLocation ? "EnterLocation" : "EnterEmptyLocation";
buttonText = TextManager.GetWithVariable(textTag, "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); buttonText = TextManager.GetWithVariable(textTag, "[locationname]", Level.Loaded.StartLocation?.DisplayName ?? "[ERROR]");
allowEndingRound = !ForceMapUI && !ShowCampaignUI; allowEndingRound = !ForceMapUI && !ShowCampaignUI;
} }
break; break;
@@ -221,7 +221,7 @@ namespace Barotrauma
endRoundButton.Color = GUIStyle.Red * 0.7f; endRoundButton.Color = GUIStyle.Red * 0.7f;
endRoundButton.HoverColor = GUIStyle.Red; endRoundButton.HoverColor = GUIStyle.Red;
} }
buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.DisplayName ?? "[ERROR]");
allowEndingRound = !ForceMapUI && !ShowCampaignUI; allowEndingRound = !ForceMapUI && !ShowCampaignUI;
} }
else else
@@ -614,7 +614,7 @@ namespace Barotrauma
{ {
if (availableMission.ConnectionIndex < 0 || availableMission.ConnectionIndex >= campaign.Map.CurrentLocation.Connections.Count) if (availableMission.ConnectionIndex < 0 || availableMission.ConnectionIndex >= campaign.Map.CurrentLocation.Connections.Count)
{ {
DebugConsole.ThrowError($"Error when receiving campaign data from the server: connection index for mission \"{availableMission.Identifier}\" out of range (index: {availableMission.ConnectionIndex}, current location: {campaign.Map.CurrentLocation.Name}, connections: {campaign.Map.CurrentLocation.Connections.Count})."); DebugConsole.ThrowError($"Error when receiving campaign data from the server: connection index for mission \"{availableMission.Identifier}\" out of range (index: {availableMission.ConnectionIndex}, current location: {campaign.Map.CurrentLocation.DisplayName}, connections: {campaign.Map.CurrentLocation.Connections.Count}).");
continue; continue;
} }
LocationConnection connection = campaign.Map.CurrentLocation.Connections[availableMission.ConnectionIndex]; LocationConnection connection = campaign.Map.CurrentLocation.Connections[availableMission.ConnectionIndex];
@@ -647,7 +647,15 @@ namespace Barotrauma
{ {
if (ownedSubIndex >= GameMain.Client.ServerSubmarines.Count) 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))}"; string errorMsg;
if (GameMain.Client.ServerSubmarines.None())
{
errorMsg = $"Error in {nameof(MultiPlayerCampaign.ClientRead)}. Owned submarine index was out of bounds (list of server submarines is empty).";
}
else
{
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); DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce( GameAnalyticsManager.AddErrorEventOnce(
"MultiPlayerCampaign.ClientRead.OwnerSubIndexOutOfBounds" + ownedSubIndex, "MultiPlayerCampaign.ClientRead.OwnerSubIndexOutOfBounds" + ownedSubIndex,
@@ -822,11 +830,12 @@ namespace Barotrauma
UInt16 id = msg.ReadUInt16(); UInt16 id = msg.ReadUInt16();
bool hasCharacterData = msg.ReadBoolean(); bool hasCharacterData = msg.ReadBoolean();
CharacterInfo myCharacterInfo = null; CharacterInfo myCharacterInfo = null;
bool waitForModsDownloaded = Screen.Selected is ModDownloadScreen;
if (hasCharacterData) if (hasCharacterData)
{ {
myCharacterInfo = CharacterInfo.ClientRead(CharacterPrefab.HumanSpeciesName, msg); myCharacterInfo = CharacterInfo.ClientRead(CharacterPrefab.HumanSpeciesName, msg, requireJobPrefabFound: !waitForModsDownloaded);
} }
if (ShouldApply(NetFlags.CharacterInfo, id, requireUpToDateSave: true)) if (!waitForModsDownloaded && ShouldApply(NetFlags.CharacterInfo, id, requireUpToDateSave: true))
{ {
if (myCharacterInfo != null) if (myCharacterInfo != null)
{ {
@@ -548,9 +548,12 @@ namespace Barotrauma
} }
else else
{ {
//wasn't initially docked (sub doesn't have a docking port?) //force the map to open if the sub is somehow not at the start of the outpost level
// -> choose a destination when the sub is far enough from the start outpost //UNLESS the level has specific exit points, in that case the sub needs to get to those
if (!Submarine.MainSub.AtStartExit && !Level.Loaded.StartOutpost.ExitPoints.Any()) if (!Submarine.MainSub.AtStartExit &&
/*there should normally always be a start outpost in outpost levels,
* but that might not always be the case e.g. mods or outdated saves (see #13042)*/
Level.Loaded.StartOutpost is not { ExitPoints.Count: > 0 })
{ {
ForceMapUI = true; ForceMapUI = true;
CampaignUI.SelectTab(InteractionType.Map); CampaignUI.SelectTab(InteractionType.Map);
@@ -48,6 +48,8 @@ namespace Barotrauma
private GUIImage eventLogNotification; private GUIImage eventLogNotification;
private Point prevTopLeftButtonsResolution;
private void CreateTopLeftButtons() private void CreateTopLeftButtons()
{ {
if (topLeftButtonGroup != null) if (topLeftButtonGroup != null)
@@ -61,10 +63,6 @@ namespace Barotrauma
AbsoluteSpacing = HUDLayoutSettings.Padding, AbsoluteSpacing = HUDLayoutSettings.Padding,
CanBeFocused = false CanBeFocused = false
}; };
topLeftButtonGroup.RectTransform.ParentChanged += (_) =>
{
GameMain.Instance.ResolutionChanged -= CreateTopLeftButtons;
};
int buttonHeight = GUI.IntScale(40); int buttonHeight = GUI.IntScale(40);
Vector2 buttonSpriteSize = GUIStyle.GetComponentStyle("CrewListToggleButton").GetDefaultSprite().size; Vector2 buttonSpriteSize = GUIStyle.GetComponentStyle("CrewListToggleButton").GetDefaultSprite().size;
int buttonWidth = (int)((buttonHeight / buttonSpriteSize.Y) * buttonSpriteSize.X); int buttonWidth = (int)((buttonHeight / buttonSpriteSize.Y) * buttonSpriteSize.X);
@@ -98,8 +96,6 @@ namespace Barotrauma
talentPointNotification = CreateNotificationIcon(tabMenuButton); talentPointNotification = CreateNotificationIcon(tabMenuButton);
eventLogNotification = CreateNotificationIcon(tabMenuButton); eventLogNotification = CreateNotificationIcon(tabMenuButton);
GameMain.Instance.ResolutionChanged += CreateTopLeftButtons;
respawnInfoFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 1.0f), parent: topLeftButtonGroup.RectTransform) respawnInfoFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 1.0f), parent: topLeftButtonGroup.RectTransform)
{ MaxSize = new Point(HUDLayoutSettings.ButtonAreaTop.Width / 3, int.MaxValue) }, style: null) { MaxSize = new Point(HUDLayoutSettings.ButtonAreaTop.Width / 3, int.MaxValue) }, style: null)
{ {
@@ -121,6 +117,7 @@ namespace Barotrauma
return true; return true;
} }
}; };
prevTopLeftButtonsResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight);
} }
public void AddToGUIUpdateList() public void AddToGUIUpdateList()
@@ -133,7 +130,8 @@ namespace Barotrauma
if ((GameMode is not CampaignMode campaign || (!campaign.ForceMapUI && !campaign.ShowCampaignUI)) && if ((GameMode is not CampaignMode campaign || (!campaign.ForceMapUI && !campaign.ShowCampaignUI)) &&
!CoroutineManager.IsCoroutineRunning("LevelTransition") && !CoroutineManager.IsCoroutineRunning("SubmarineTransition")) !CoroutineManager.IsCoroutineRunning("LevelTransition") && !CoroutineManager.IsCoroutineRunning("SubmarineTransition"))
{ {
if (topLeftButtonGroup == null) if (topLeftButtonGroup == null ||
prevTopLeftButtonsResolution.X != GameMain.GraphicsWidth || prevTopLeftButtonsResolution.Y != GameMain.GraphicsHeight)
{ {
CreateTopLeftButtons(); CreateTopLeftButtons();
} }
@@ -139,6 +139,11 @@ static class ObjectiveManager
VideoPlayer.AddToGUIUpdateList(order: 100); VideoPlayer.AddToGUIUpdateList(order: 100);
} }
public static bool IsSegmentActive(Identifier segmentId)
{
return activeObjectives.Any(o => o.Id == segmentId);
}
public static void TriggerSegment(Segment segment, bool connectObjective = false) public static void TriggerSegment(Segment segment, bool connectObjective = false)
{ {
if (segment.SegmentType != SegmentType.InfoBox) if (segment.SegmentType != SegmentType.InfoBox)
@@ -361,9 +366,18 @@ static class ObjectiveManager
activeObjectives.IndexOf(parentSegment) + activeObjectives.Count(s => s.ParentId == segment.ParentId); activeObjectives.IndexOf(parentSegment) + activeObjectives.Count(s => s.ParentId == segment.ParentId);
if (objectiveGroup.RectTransform.GetChildIndex(frameRt) != childIndex) if (objectiveGroup.RectTransform.GetChildIndex(frameRt) != childIndex)
{ {
frameRt.RepositionChildInHierarchy(childIndex); if (childIndex < 0 || childIndex >= frameRt.Parent.CountChildren)
activeObjectives.Remove(segment); {
activeObjectives.Insert(childIndex, segment); DebugConsole.ThrowError(
$"Error in {nameof(ObjectiveManager.AddToObjectiveList)}. " +
$"Failed to reposition an objective in the list. Text \"{segment.ObjectiveText}\", parentId: {segment.ParentId}, childIndex: {childIndex}");
}
else
{
frameRt.RepositionChildInHierarchy(childIndex);
activeObjectives.Remove(segment);
activeObjectives.Insert(childIndex, segment);
}
} }
} }
frameRt.AbsoluteOffset = GetObjectiveHiddenPosition(); frameRt.AbsoluteOffset = GetObjectiveHiddenPosition();
@@ -564,7 +564,7 @@ namespace Barotrauma
private LocalizedString GetHeaderText(bool gameOver, CampaignMode.TransitionType transitionType) private LocalizedString GetHeaderText(bool gameOver, CampaignMode.TransitionType transitionType)
{ {
string locationName = Submarine.MainSub is { AtEndExit: true } ? endLocation?.Name : startLocation?.Name; LocalizedString locationName = Submarine.MainSub is { AtEndExit: true } ? endLocation?.DisplayName : startLocation?.DisplayName;
string textTag; string textTag;
if (gameOver) if (gameOver)
@@ -576,23 +576,23 @@ namespace Barotrauma
switch (transitionType) switch (transitionType)
{ {
case CampaignMode.TransitionType.LeaveLocation: case CampaignMode.TransitionType.LeaveLocation:
locationName = startLocation?.Name; locationName = startLocation?.DisplayName;
textTag = "RoundSummaryLeaving"; textTag = "RoundSummaryLeaving";
break; break;
case CampaignMode.TransitionType.ProgressToNextLocation: case CampaignMode.TransitionType.ProgressToNextLocation:
locationName = endLocation?.Name; locationName = endLocation?.DisplayName;
textTag = "RoundSummaryProgress"; textTag = "RoundSummaryProgress";
break; break;
case CampaignMode.TransitionType.ProgressToNextEmptyLocation: case CampaignMode.TransitionType.ProgressToNextEmptyLocation:
locationName = endLocation?.Name; locationName = endLocation?.DisplayName;
textTag = "RoundSummaryProgressToEmptyLocation"; textTag = "RoundSummaryProgressToEmptyLocation";
break; break;
case CampaignMode.TransitionType.ReturnToPreviousLocation: case CampaignMode.TransitionType.ReturnToPreviousLocation:
locationName = startLocation?.Name; locationName = startLocation?.DisplayName;
textTag = "RoundSummaryReturn"; textTag = "RoundSummaryReturn";
break; break;
case CampaignMode.TransitionType.ReturnToPreviousEmptyLocation: case CampaignMode.TransitionType.ReturnToPreviousEmptyLocation:
locationName = startLocation?.Name; locationName = startLocation?.DisplayName;
textTag = "RoundSummaryReturnToEmptyLocation"; textTag = "RoundSummaryReturnToEmptyLocation";
break; break;
default: default:
@@ -603,14 +603,14 @@ namespace Barotrauma
if (startLocation?.Biome != null && startLocation.Biome.IsEndBiome) if (startLocation?.Biome != null && startLocation.Biome.IsEndBiome)
{ {
locationName ??= startLocation.Name; locationName ??= startLocation.DisplayName;
} }
if (textTag == null) { return ""; } if (textTag == null) { return ""; }
if (locationName == null) if (locationName == null)
{ {
DebugConsole.ThrowError($"Error while creating round summary: could not determine destination location. Start location: {startLocation?.Name ?? "null"}, end location: {endLocation?.Name ?? "null"}"); DebugConsole.ThrowError($"Error while creating round summary: could not determine destination location. Start location: {startLocation?.DisplayName ?? "null"}, end location: {endLocation?.DisplayName ?? "null"}");
locationName = "[UNKNOWN]"; locationName = "[UNKNOWN]";
} }
@@ -568,7 +568,7 @@ namespace Barotrauma
itemContainer.KeepOpenWhenEquippedBy(character) && itemContainer.KeepOpenWhenEquippedBy(character) &&
!DraggingItems.Contains(item) && !DraggingItems.Contains(item) &&
character.CanAccessInventory(itemContainer.Inventory) && character.CanAccessInventory(itemContainer.Inventory) &&
!highlightedSubInventorySlots.Any(s => s.Inventory == itemContainer.Inventory)) !highlightedSubInventorySlots.Any(s => s.Inventory == itemContainer.Inventory && s.SlotIndex == i))
{ {
ShowSubInventory(new SlotReference(this, visualSlots[i], i, false, itemContainer.Inventory), deltaTime, cam, hideSubInventories, true); ShowSubInventory(new SlotReference(this, visualSlots[i], i, false, itemContainer.Inventory), deltaTime, cam, hideSubInventories, true);
} }
@@ -709,11 +709,11 @@ namespace Barotrauma
private void ShowSubInventory(SlotReference slotRef, float deltaTime, Camera cam, List<SlotReference> hideSubInventories, bool isEquippedSubInventory) private void ShowSubInventory(SlotReference slotRef, float deltaTime, Camera cam, List<SlotReference> hideSubInventories, bool isEquippedSubInventory)
{ {
Rectangle hoverArea = GetSubInventoryHoverArea(slotRef); Rectangle hoverArea = GetSubInventoryHoverArea(slotRef);
if (isEquippedSubInventory) if (isEquippedSubInventory && slotRef.Inventory is not ItemInventory { Container.MovableFrame: true, Container.KeepOpenWhenEquipped: true })
{ {
foreach (SlotReference highlightedSubInventorySlot in highlightedSubInventorySlots) foreach (SlotReference highlightedSubInventorySlot in highlightedSubInventorySlots)
{ {
if (highlightedSubInventorySlot == slotRef) continue; if (highlightedSubInventorySlot == slotRef) { continue; }
if (hoverArea.Intersects(GetSubInventoryHoverArea(highlightedSubInventorySlot))) if (hoverArea.Intersects(GetSubInventoryHoverArea(highlightedSubInventorySlot)))
{ {
return; // If an equipped one intersects with a currently active hover one, do not open return; // If an equipped one intersects with a currently active hover one, do not open
@@ -819,6 +819,7 @@ namespace Barotrauma
if (selectedContainer != null && if (selectedContainer != null &&
selectedContainer.Inventory != null && selectedContainer.Inventory != null &&
!selectedContainer.Inventory.Locked && !selectedContainer.Inventory.Locked &&
selectedContainer.DrawInventory &&
allowInventorySwap) allowInventorySwap)
{ {
//player has selected the inventory of another item -> attempt to move the item there //player has selected the inventory of another item -> attempt to move the item there
@@ -841,6 +842,7 @@ namespace Barotrauma
} }
else if (character.HeldItems.FirstOrDefault(i => else if (character.HeldItems.FirstOrDefault(i =>
i.OwnInventory != null && i.OwnInventory != null &&
i.OwnInventory.Container.DrawInventory &&
(i.OwnInventory.CanBePut(item) || ((i.OwnInventory.Capacity == 1 || i.OwnInventory.Container.HasSubContainers) && i.OwnInventory.AllowSwappingContainedItems && i.OwnInventory.Container.CanBeContained(item)))) is { } equippedContainer) (i.OwnInventory.CanBePut(item) || ((i.OwnInventory.Capacity == 1 || i.OwnInventory.Container.HasSubContainers) && i.OwnInventory.AllowSwappingContainedItems && i.OwnInventory.Container.CanBeContained(item)))) is { } equippedContainer)
{ {
if (allowEquip) if (allowEquip)
@@ -1027,7 +1029,7 @@ namespace Barotrauma
//order by the condition of the contained item to prefer putting into the item with the emptiest ammo/battery/tank //order by the condition of the contained item to prefer putting into the item with the emptiest ammo/battery/tank
foreach (Item heldItem in character.HeldItems.OrderByDescending(heldItem => GetContainPriority(item, heldItem))) foreach (Item heldItem in character.HeldItems.OrderByDescending(heldItem => GetContainPriority(item, heldItem)))
{ {
if (heldItem.OwnInventory == null) { continue; } if (heldItem.OwnInventory == null || !heldItem.OwnInventory.Container.DrawInventory) { continue; }
//don't allow swapping if we're moving items into an item with 1 slot holding a stack of items //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) //(in that case, the quick action should just fill up the stack)
bool disallowSwapping = bool disallowSwapping =
@@ -260,6 +260,12 @@ namespace Barotrauma.Items.Components
if (!hasSoundsOfType[(int)type]) { return; } if (!hasSoundsOfType[(int)type]) { return; }
if (GameMain.Client?.MidRoundSyncing ?? false) { return; } if (GameMain.Client?.MidRoundSyncing ?? false) { return; }
//above the top boundary of the level (in an inactive respawn shuttle?)
if (item.Submarine != null && Level.Loaded != null && item.Submarine.WorldPosition.Y > Level.Loaded.Size.Y)
{
return;
}
if (loopingSound != null) if (loopingSound != null)
{ {
if (Vector3.DistanceSquared(GameMain.SoundManager.ListenerPosition, new Vector3(item.WorldPosition, 0.0f)) > loopingSound.Range * loopingSound.Range || if (Vector3.DistanceSquared(GameMain.SoundManager.ListenerPosition, new Vector3(item.WorldPosition, 0.0f)) > loopingSound.Range * loopingSound.Range ||
@@ -388,12 +394,9 @@ namespace Barotrauma.Items.Components
} }
} }
public void StopSounds(ActionType type) public void StopLoopingSound()
{ {
if (loopingSound == null) { return; } if (loopingSound == null) { return; }
if (loopingSound.Type != type) { return; }
if (loopingSoundChannel != null) if (loopingSoundChannel != null)
{ {
loopingSoundChannel.FadeOutAndDispose(); loopingSoundChannel.FadeOutAndDispose();
@@ -402,6 +405,12 @@ namespace Barotrauma.Items.Components
} }
} }
public void StopSounds(ActionType type)
{
if (loopingSound == null || loopingSound.Type != type) { return; }
StopLoopingSound();
}
private float GetSoundVolume(ItemSound sound) private float GetSoundVolume(ItemSound sound)
{ {
if (sound == null) { return 0.0f; } if (sound == null) { return 0.0f; }
@@ -761,6 +770,8 @@ namespace Barotrauma.Items.Components
} }
} }
public virtual void OnPlayerSkillsChanged() { }
public virtual void AddTooltipInfo(ref LocalizedString name, ref LocalizedString description) { } public virtual void AddTooltipInfo(ref LocalizedString name, ref LocalizedString description) { }
} }
} }
@@ -346,9 +346,9 @@ namespace Barotrauma.Items.Components
public bool KeepOpenWhenEquippedBy(Character character) public bool KeepOpenWhenEquippedBy(Character character)
{ {
if (!character.CanAccessInventory(Inventory) || if (!KeepOpenWhenEquipped ||
!KeepOpenWhenEquipped || !character.HasEquippedItem(Item) ||
!character.HasEquippedItem(Item)) !character.CanAccessInventory(Inventory))
{ {
return false; return false;
} }
@@ -571,11 +571,13 @@ namespace Barotrauma.Items.Components
{ {
spriteRotation = contained.Rotation; spriteRotation = contained.Rotation;
} }
if ((item.body != null && item.body.Dir == -1) || item.FlippedX) bool flipX = (item.body != null && item.body.Dir == -1) || item.FlippedX;
if (flipX)
{ {
spriteEffects |= MathUtils.NearlyEqual(spriteRotation % 180, 90.0f) ? SpriteEffects.FlipVertically : SpriteEffects.FlipHorizontally; spriteEffects |= MathUtils.NearlyEqual(spriteRotation % 180, 90.0f) ? SpriteEffects.FlipVertically : SpriteEffects.FlipHorizontally;
} }
if (item.FlippedY) bool flipY = item.FlippedY;
if (flipY)
{ {
spriteEffects |= MathUtils.NearlyEqual(spriteRotation % 180, 90.0f) ? SpriteEffects.FlipHorizontally : SpriteEffects.FlipVertically; spriteEffects |= MathUtils.NearlyEqual(spriteRotation % 180, 90.0f) ? SpriteEffects.FlipHorizontally : SpriteEffects.FlipVertically;
} }
@@ -589,6 +591,7 @@ namespace Barotrauma.Items.Components
contained.Item.Scale, contained.Item.Scale,
spriteEffects, spriteEffects,
depth: containedSpriteDepth); depth: containedSpriteDepth);
contained.Item.DrawDecorativeSprites(spriteBatch, itemPos, flipX,flipY, (contained.Item.body == null ? 0.0f : contained.Item.body.DrawRotation), containedSpriteDepth);
foreach (ItemContainer ic in contained.Item.GetComponents<ItemContainer>()) foreach (ItemContainer ic in contained.Item.GetComponents<ItemContainer>())
{ {
@@ -227,7 +227,7 @@ namespace Barotrauma.Items.Components
switch (text) switch (text)
{ {
case "[CurrentLocationName]": case "[CurrentLocationName]":
SetDisplayText(Level.Loaded?.StartLocation?.Name ?? string.Empty); SetDisplayText(Level.Loaded?.StartLocation?.DisplayName.Value ?? string.Empty);
break; break;
case "[CurrentBiomeName]": case "[CurrentBiomeName]":
SetDisplayText(Level.Loaded?.LevelData?.Biome?.DisplayName.Value ?? string.Empty); SetDisplayText(Level.Loaded?.LevelData?.Biome?.DisplayName.Value ?? string.Empty);
@@ -67,7 +67,7 @@ namespace Barotrauma.Items.Components
Light.Position = item.Position; Light.Position = item.Position;
} }
PhysicsBody body = Light.ParentBody; PhysicsBody body = Light.ParentBody;
if (body != null) if (body != null && body.Enabled)
{ {
Light.Rotation = body.Dir > 0.0f ? body.DrawRotation : body.DrawRotation - MathHelper.Pi; Light.Rotation = body.Dir > 0.0f ? body.DrawRotation : body.DrawRotation - MathHelper.Pi;
Light.LightSpriteEffect = (body.Dir > 0.0f) ? SpriteEffects.None : SpriteEffects.FlipVertically; Light.LightSpriteEffect = (body.Dir > 0.0f) ? SpriteEffects.None : SpriteEffects.FlipVertically;
@@ -66,7 +66,11 @@ namespace Barotrauma.Items.Components
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), sliderArea.RectTransform, Anchor.TopCenter), "", textColor: GUIStyle.TextColorNormal, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center) new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), sliderArea.RectTransform, Anchor.TopCenter), "", textColor: GUIStyle.TextColorNormal, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center)
{ {
AutoScaleHorizontal = true, AutoScaleHorizontal = true,
TextGetter = () => { return TextManager.AddPunctuation(':', powerLabel, (int)(targetForce) + " %"); } TextGetter = () =>
{
return TextManager.AddPunctuation(':', powerLabel,
TextManager.GetWithVariable("percentageformat", "[value]", ((int)MathF.Round(targetForce)).ToString()));
}
}; };
forceSlider = new GUIScrollBar(new RectTransform(new Vector2(0.95f, 0.45f), sliderArea.RectTransform, Anchor.Center), barSize: 0.1f, style: "DeviceSlider") forceSlider = new GUIScrollBar(new RectTransform(new Vector2(0.95f, 0.45f), sliderArea.RectTransform, Anchor.Center), barSize: 0.1f, style: "DeviceSlider")
{ {
@@ -4,6 +4,7 @@ using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
namespace Barotrauma.Items.Components namespace Barotrauma.Items.Components
@@ -825,11 +826,22 @@ namespace Barotrauma.Items.Components
return true; return true;
} }
private readonly record struct SelectedRecipe(Character User, FabricationRecipe SelectedItem, Option<float> OverrideRequiredTime);
private Option<SelectedRecipe> LastSelectedRecipe = Option.None;
private bool SelectItem(Character user, FabricationRecipe selectedItem, float? overrideRequiredTime = null) private bool SelectItem(Character user, FabricationRecipe selectedItem, float? overrideRequiredTime = null)
{ {
this.selectedItem = selectedItem; this.selectedItem = selectedItem;
displayingForCharacter = user; displayingForCharacter = user;
var selectedRecipe = new SelectedRecipe(user, selectedItem, overrideRequiredTime is null ? Option.None : Option.Some(overrideRequiredTime.Value));
LastSelectedRecipe = Option.Some(selectedRecipe);
CreateSelectedItemUI(selectedRecipe);
return true;
}
private void CreateSelectedItemUI(SelectedRecipe recipe)
{
var (user, selectedItem, overrideRequiredTime) = recipe;
int max = Math.Max(selectedItem.TargetItem.GetMaxStackSize(outputContainer.Inventory) / selectedItem.Amount, 1); int max = Math.Max(selectedItem.TargetItem.GetMaxStackSize(outputContainer.Inventory) / selectedItem.Amount, 1);
if (amountInput != null) if (amountInput != null)
@@ -853,8 +865,10 @@ namespace Barotrauma.Items.Components
LocalizedString itemName = GetRecipeNameAndAmount(selectedItem); LocalizedString itemName = GetRecipeNameAndAmount(selectedItem);
LocalizedString name = itemName; LocalizedString name = itemName;
float quality = selectedItem.Quality ?? GetFabricatedItemQuality(selectedItem, user); QualityResult result = GetFabricatedItemQuality(selectedItem, user);
if (quality > 0)
float quality = selectedItem.Quality ?? result.Quality;
if (quality > 0 || result.HasRandomQualityRollChance)
{ {
name = TextManager.GetWithVariable("itemname.quality" + (int)quality, "[itemname]", itemName + '\n') name = TextManager.GetWithVariable("itemname.quality" + (int)quality, "[itemname]", itemName + '\n')
.Fallback(TextManager.GetWithVariable("itemname.quality3", "[itemname]", itemName + '\n')); .Fallback(TextManager.GetWithVariable("itemname.quality3", "[itemname]", itemName + '\n'));
@@ -865,6 +879,49 @@ namespace Barotrauma.Items.Components
{ {
AutoScaleHorizontal = true AutoScaleHorizontal = true
}; };
if (result.HasRandomQualityRollChance)
{
var iconLayout = new GUIFrame(new RectTransform(new Vector2(0.4f, 1f), selectedItemFrame.RectTransform, anchor: Anchor.TopRight), style: null);
var icon = GameSession.CreateNotificationIcon(iconLayout, offset: true);
float percentage1 = result.TotalPlusOnePercentage;
float percentage2 = result.TotalPlusTwoPercentage;
string chance1text = percentage1.ToString("F1", CultureInfo.InvariantCulture);
string chance2text = percentage2.ToString("F1", CultureInfo.InvariantCulture);
int quality1 = Math.Clamp(result.Quality + 1, min: 0, max: 3);
int quality2 = Math.Clamp(result.Quality + 2, min: 0, max: 3);
LocalizedString quality1Text = TextManager.Get($"quality{quality1}");
LocalizedString quality2Text = TextManager.Get($"quality{quality2}");
string localizationTag = percentage2 > 0f && percentage1 > 0 && quality1 != quality2 ? "meetsbonusrequirementtwice" : "meetsbonusrequirement";
var variables = new (string Key, LocalizedString Value)[]
{
("[chance]", chance1text), ("[quality]", quality1Text),
("[chance2]", chance2text), ("[quality2]", quality2Text)
};
if (MathUtils.NearlyEqual(percentage1, 0))
{
variables = new[] { ("[chance]", chance2text), ("[quality]", quality2Text) };
}
if (quality1 == quality2)
{
LocalizedString rawPercentage = result.PlusOnePercentage.ToString("F1", CultureInfo.InvariantCulture);
variables = new[] { ("[chance]", rawPercentage), ("[quality]", quality1Text) };
}
LocalizedString qualityTooltip = TextManager.GetWithVariables(localizationTag, variables);
icon.ToolTip = RichString.Rich(qualityTooltip);
icon.Visible = icon.CanBeFocused = true;
}
nameBlock.Padding = new Vector4(0, nameBlock.Padding.Y, GUI.IntScale(5), nameBlock.Padding.W); nameBlock.Padding = new Vector4(0, nameBlock.Padding.Y, GUI.IntScale(5), nameBlock.Padding.W);
if (nameBlock.TextScale < 0.7f) if (nameBlock.TextScale < 0.7f)
{ {
@@ -921,8 +978,9 @@ namespace Barotrauma.Items.Components
float degreeOfSuccess = user == null ? 0.0f : FabricationDegreeOfSuccess(user, selectedItem.RequiredSkills); float degreeOfSuccess = user == null ? 0.0f : FabricationDegreeOfSuccess(user, selectedItem.RequiredSkills);
if (degreeOfSuccess > 0.5f) { degreeOfSuccess = 1.0f; } if (degreeOfSuccess > 0.5f) { degreeOfSuccess = 1.0f; }
float requiredTime = overrideRequiredTime ?? float requiredTime = overrideRequiredTime.TryUnwrap(out var time)
(user == null ? selectedItem.RequiredTime : GetRequiredTime(selectedItem, user)); ? time
: (user == null ? selectedItem.RequiredTime : GetRequiredTime(selectedItem, user));
if ((int)requiredTime > 0) if ((int)requiredTime > 0)
{ {
@@ -946,7 +1004,6 @@ namespace Barotrauma.Items.Components
font: GUIStyle.SmallFont); font: GUIStyle.SmallFont);
} }
return true;
} }
public void HighlightRecipe(string identifier, Color color) public void HighlightRecipe(string identifier, Color color)
@@ -1056,6 +1113,15 @@ namespace Barotrauma.Items.Components
} }
} }
public override void OnPlayerSkillsChanged()
=> RefreshSelectedItem();
public void RefreshSelectedItem()
{
if (!LastSelectedRecipe.TryUnwrap(out var lastSelected)) { return; }
CreateSelectedItemUI(lastSelected);
}
partial void UpdateRequiredTimeProjSpecific() partial void UpdateRequiredTimeProjSpecific()
{ {
if (requiredTimeBlock == null) { return; } if (requiredTimeBlock == null) { return; }
@@ -991,7 +991,7 @@ namespace Barotrauma.Items.Components
if (Level.Loaded.StartLocation?.Type is { ShowSonarMarker: true }) if (Level.Loaded.StartLocation?.Type is { ShowSonarMarker: true })
{ {
DrawMarker(spriteBatch, DrawMarker(spriteBatch,
Level.Loaded.StartLocation.Name, Level.Loaded.StartLocation.DisplayName.Value,
(Level.Loaded.StartOutpost != null ? "outpost" : "location").ToIdentifier(), (Level.Loaded.StartOutpost != null ? "outpost" : "location").ToIdentifier(),
"startlocation", "startlocation",
Level.Loaded.StartExitPosition, transducerCenter, Level.Loaded.StartExitPosition, transducerCenter,
@@ -1001,7 +1001,7 @@ namespace Barotrauma.Items.Components
if (Level.Loaded is { EndLocation.Type.ShowSonarMarker: true, Type: LevelData.LevelType.LocationConnection }) if (Level.Loaded is { EndLocation.Type.ShowSonarMarker: true, Type: LevelData.LevelType.LocationConnection })
{ {
DrawMarker(spriteBatch, DrawMarker(spriteBatch,
Level.Loaded.EndLocation.Name, Level.Loaded.EndLocation.DisplayName.Value,
(Level.Loaded.EndOutpost != null ? "outpost" : "location").ToIdentifier(), (Level.Loaded.EndOutpost != null ? "outpost" : "location").ToIdentifier(),
"endlocation", "endlocation",
Level.Loaded.EndExitPosition, transducerCenter, Level.Loaded.EndExitPosition, transducerCenter,
@@ -216,7 +216,7 @@ namespace Barotrauma.Items.Components
} }
}; };
levelStartTickBox = new GUITickBox(new RectTransform(new Vector2(1, 0.333f), paddedAutoPilotControls.RectTransform, Anchor.Center), levelStartTickBox = new GUITickBox(new RectTransform(new Vector2(1, 0.333f), paddedAutoPilotControls.RectTransform, Anchor.Center),
GameMain.GameSession?.StartLocation == null ? "" : ToolBox.LimitString(GameMain.GameSession.StartLocation.Name, GUIStyle.SmallFont, textLimit), GameMain.GameSession?.StartLocation == null ? "" : ToolBox.LimitString(GameMain.GameSession.StartLocation.DisplayName, GUIStyle.SmallFont, textLimit),
font: GUIStyle.SmallFont, style: "GUIRadioButton") font: GUIStyle.SmallFont, style: "GUIRadioButton")
{ {
Enabled = autoPilot, Enabled = autoPilot,
@@ -243,7 +243,7 @@ namespace Barotrauma.Items.Components
}; };
levelEndTickBox = new GUITickBox(new RectTransform(new Vector2(1, 0.333f), paddedAutoPilotControls.RectTransform, Anchor.BottomCenter), levelEndTickBox = new GUITickBox(new RectTransform(new Vector2(1, 0.333f), paddedAutoPilotControls.RectTransform, Anchor.BottomCenter),
(GameMain.GameSession?.EndLocation == null || Level.IsLoadedOutpost) ? "" : ToolBox.LimitString(GameMain.GameSession.EndLocation.Name, GUIStyle.SmallFont, textLimit), (GameMain.GameSession?.EndLocation == null || Level.IsLoadedOutpost) ? "" : ToolBox.LimitString(GameMain.GameSession.EndLocation.DisplayName, GUIStyle.SmallFont, textLimit),
font: GUIStyle.SmallFont, style: "GUIRadioButton") font: GUIStyle.SmallFont, style: "GUIRadioButton")
{ {
Enabled = autoPilot, Enabled = autoPilot,
@@ -389,7 +389,7 @@ namespace Barotrauma.Items.Components
if (!ObjectiveManager.AllActiveObjectivesCompleted()) if (!ObjectiveManager.AllActiveObjectivesCompleted())
{ {
exitOutpostPrompt = new GUIMessageBox("", exitOutpostPrompt = new GUIMessageBox("",
TextManager.GetWithVariable("CampaignExitTutorialOutpostPrompt", "[locationname]", campaign.Map.CurrentLocation.Name), TextManager.GetWithVariable("CampaignExitTutorialOutpostPrompt", "[locationname]", campaign.Map.CurrentLocation.DisplayName),
new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") });
exitOutpostPrompt.Buttons[0].OnClicked += (_, _) => exitOutpostPrompt.Buttons[0].OnClicked += (_, _) =>
{ {
@@ -509,9 +509,9 @@ namespace Barotrauma.Items.Components
noPowerTip = TextManager.Get("SteeringNoPowerTip"); noPowerTip = TextManager.Get("SteeringNoPowerTip");
autoPilotMaintainPosTip = TextManager.Get("SteeringAutoPilotMaintainPosTip"); autoPilotMaintainPosTip = TextManager.Get("SteeringAutoPilotMaintainPosTip");
autoPilotLevelStartTip = TextManager.GetWithVariable("SteeringAutoPilotLocationTip", "[locationname]", autoPilotLevelStartTip = TextManager.GetWithVariable("SteeringAutoPilotLocationTip", "[locationname]",
GameMain.GameSession?.StartLocation == null ? "Start" : GameMain.GameSession.StartLocation.Name); GameMain.GameSession?.StartLocation == null ? "Start" : GameMain.GameSession.StartLocation.DisplayName);
autoPilotLevelEndTip = TextManager.GetWithVariable("SteeringAutoPilotLocationTip", "[locationname]", autoPilotLevelEndTip = TextManager.GetWithVariable("SteeringAutoPilotLocationTip", "[locationname]",
GameMain.GameSession?.EndLocation == null ? "End" : GameMain.GameSession.EndLocation.Name); GameMain.GameSession?.EndLocation == null ? "End" : GameMain.GameSession.EndLocation.DisplayName);
} }
protected override void OnResolutionChanged() protected override void OnResolutionChanged()
@@ -16,6 +16,8 @@ namespace Barotrauma.Items.Components
private GUIProgressBar powerIndicator; private GUIProgressBar powerIndicator;
private Vector2? debugDrawTargetPos;
public int UIElementHeight public int UIElementHeight
{ {
get get
@@ -422,9 +424,23 @@ namespace Barotrauma.Items.Components
if (GameMain.DebugDraw) if (GameMain.DebugDraw)
{ {
Vector2 firingPos = GetRelativeFiringPosition(); Vector2 firingPos = GetRelativeFiringPosition();
Vector2 endPos = firingPos + 3500 * GetBarrelDir();
firingPos.Y = -firingPos.Y; firingPos.Y = -firingPos.Y;
endPos.Y = -endPos.Y;
GUI.DrawLine(spriteBatch, firingPos - Vector2.UnitX * 5, firingPos + Vector2.UnitX * 5, Color.Red); 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); GUI.DrawLine(spriteBatch, firingPos - Vector2.UnitY * 5, firingPos + Vector2.UnitY * 5, Color.Red);
if (debugDrawTargetPos.HasValue)
{
Vector2 targetPos = debugDrawTargetPos.Value;
targetPos.Y = -targetPos.Y;
GUI.DrawLine(spriteBatch, targetPos - Vector2.UnitX * 5, targetPos + Vector2.UnitX * 5, Color.Magenta, width: 5);
GUI.DrawLine(spriteBatch, targetPos - Vector2.UnitY * 5, targetPos + Vector2.UnitY * 5, Color.Magenta, width: 5);
GUI.DrawLine(spriteBatch, firingPos, targetPos, Color.Magenta, width: 2);
}
GUI.DrawLine(spriteBatch, firingPos, endPos, Color.LightGray, width: 2);
} }
if (!editing || GUI.DisableHUD || !item.IsSelected) { return; } if (!editing || GUI.DisableHUD || !item.IsSelected) { return; }
@@ -730,7 +730,7 @@ namespace Barotrauma
DraggingInventory = null; DraggingInventory = null;
subInventory.savedPosition = PlayerInput.MousePosition.ToPoint(); subInventory.savedPosition = PlayerInput.MousePosition.ToPoint();
} }
else else if (DraggingInventory == subInventory)
{ {
subInventory.savedPosition = PlayerInput.MousePosition.ToPoint(); subInventory.savedPosition = PlayerInput.MousePosition.ToPoint();
} }
@@ -901,7 +901,7 @@ namespace Barotrauma
if (IsOnInventorySlot(Character.Controlled.SelectedCharacter.Inventory)) { return true; } if (IsOnInventorySlot(Character.Controlled.SelectedCharacter.Inventory)) { return true; }
} }
bool IsOnInventorySlot(Inventory inventory) static bool IsOnInventorySlot(Inventory inventory)
{ {
for (var i = 0; i < inventory.visualSlots.Length; i++) for (var i = 0; i < inventory.visualSlots.Length; i++)
{ {
@@ -1107,7 +1107,7 @@ namespace Barotrauma
if (container.MovableFrame && !IsInventoryHoverAvailable(Owner as Character, container)) if (container.MovableFrame && !IsInventoryHoverAvailable(Owner as Character, container))
{ {
if (positionUpdateQueued) // Wait a frame before updating the positioning of the container after a resolution change to have everything working if (container.Inventory.positionUpdateQueued) // Wait a frame before updating the positioning of the container after a resolution change to have everything working
{ {
int height = (int)(movableFrameRectHeight * UIScale); int height = (int)(movableFrameRectHeight * UIScale);
CreateSlots(); CreateSlots();
@@ -1116,7 +1116,7 @@ namespace Barotrauma
draggableIndicatorOffset = DraggableIndicator.size * draggableIndicatorScale / 2f; draggableIndicatorOffset = DraggableIndicator.size * draggableIndicatorScale / 2f;
draggableIndicatorOffset += new Vector2(height / 2f - draggableIndicatorOffset.Y); draggableIndicatorOffset += new Vector2(height / 2f - draggableIndicatorOffset.Y);
container.Inventory.originalPos = container.Inventory.savedPosition = container.Inventory.movableFrameRect.Center; container.Inventory.originalPos = container.Inventory.savedPosition = container.Inventory.movableFrameRect.Center;
positionUpdateQueued = false; container.Inventory.positionUpdateQueued = false;
} }
if (container.Inventory.movableFrameRect.Size == Point.Zero || GUI.HasSizeChanged(prevScreenResolution, prevUIScale, prevHUDScale)) if (container.Inventory.movableFrameRect.Size == Point.Zero || GUI.HasSizeChanged(prevScreenResolution, prevUIScale, prevHUDScale))
@@ -1127,11 +1127,20 @@ namespace Barotrauma
prevScreenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); prevScreenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight);
prevUIScale = UIScale; prevUIScale = UIScale;
prevHUDScale = GUI.Scale; prevHUDScale = GUI.Scale;
positionUpdateQueued = true; container.Inventory.positionUpdateQueued = true;
} }
else else
{ {
GUI.DrawRectangle(spriteBatch, container.Inventory.movableFrameRect, movableFrameRectColor, true); Color color = movableFrameRectColor;
if (DraggingInventory != null && DraggingInventory != container.Inventory)
{
color *= 0.7f;
}
else if (container.Inventory.movableFrameRect.Contains(PlayerInput.MousePosition))
{
color = Color.Lerp(color, PlayerInput.PrimaryMouseButtonHeld() ? Color.Black : Color.White, 0.25f);
}
GUI.DrawRectangle(spriteBatch, container.Inventory.movableFrameRect, color, true);
DraggableIndicator.Draw(spriteBatch, container.Inventory.movableFrameRect.Location.ToVector2() + draggableIndicatorOffset, 0, draggableIndicatorScale); DraggableIndicator.Draw(spriteBatch, container.Inventory.movableFrameRect.Location.ToVector2() + draggableIndicatorOffset, 0, draggableIndicatorScale);
} }
} }
@@ -1274,7 +1283,14 @@ namespace Barotrauma
int itemCount = 0; int itemCount = 0;
foreach (Item item in DraggingItems) foreach (Item item in DraggingItems)
{ {
bool success = selectedInventory.TryPutItem(item, slotIndex, allowSwapping: !anySuccess, allowCombine, Character.Controlled); if (selectedInventory.GetItemAt(slotIndex)?.OwnInventory?.Container is { } container)
{
if (!container.AllowDragAndDrop || !container.DrawInventory)
{
allowCombine = false;
}
}
bool success = selectedInventory.TryPutItem(item, slotIndex, allowSwapping: !anySuccess, allowCombine, Character.Controlled);
if (success) if (success)
{ {
anySuccess = true; anySuccess = true;
@@ -1380,18 +1396,17 @@ namespace Barotrauma
protected static Rectangle GetSubInventoryHoverArea(SlotReference subSlot) protected static Rectangle GetSubInventoryHoverArea(SlotReference subSlot)
{ {
Rectangle hoverArea; if (Character.Controlled == null)
if ((Screen.Selected != GameMain.SubEditorScreen || GameMain.SubEditorScreen.DrawCharacterInventory) &&
(!subSlot.Inventory.Movable() ||
(Character.Controlled?.Inventory == subSlot.ParentInventory && !Character.Controlled.HasEquippedItem(subSlot.Item)) ||
(subSlot.ParentInventory is CharacterInventory characterInventory && characterInventory.CurrentLayout != CharacterInventory.Layout.Default)))
{ {
//slot not visible as a separate, movable panel -> just use the area of the slot directly return Rectangle.Empty;
hoverArea = subSlot.Slot.Rect;
hoverArea.Location += subSlot.Slot.DrawOffset.ToPoint();
hoverArea = Rectangle.Union(hoverArea, subSlot.Slot.EquipButtonRect);
} }
else
Rectangle hoverArea;
bool isMovable = subSlot.Inventory.Movable() && !subSlot.ParentInventory.IsInventoryHoverAvailable(Character.Controlled, subSlot.Item?.GetComponent<ItemContainer>());
bool unEquipped = Character.Controlled.Inventory == subSlot.ParentInventory && !Character.Controlled.HasEquippedItem(subSlot.Item);
bool isDefaultLayout = subSlot.ParentInventory is not CharacterInventory characterInventory || characterInventory.CurrentLayout == CharacterInventory.Layout.Default;
bool subEditorCharacterInventoryHidden = Screen.Selected == GameMain.SubEditorScreen && !GameMain.SubEditorScreen.DrawCharacterInventory;
if (subEditorCharacterInventoryHidden || (isMovable && !unEquipped && isDefaultLayout))
{ {
hoverArea = subSlot.Inventory.BackgroundFrame; hoverArea = subSlot.Inventory.BackgroundFrame;
hoverArea.Location += subSlot.Slot.DrawOffset.ToPoint(); hoverArea.Location += subSlot.Slot.DrawOffset.ToPoint();
@@ -1400,6 +1415,13 @@ namespace Barotrauma
hoverArea = Rectangle.Union(hoverArea, subSlot.Inventory.movableFrameRect); hoverArea = Rectangle.Union(hoverArea, subSlot.Inventory.movableFrameRect);
} }
} }
else
{
//slot not visible as a separate, movable panel -> just use the area of the slot directly
hoverArea = subSlot.Slot.Rect;
hoverArea.Location += subSlot.Slot.DrawOffset.ToPoint();
hoverArea = Rectangle.Union(hoverArea, subSlot.Slot.EquipButtonRect);
}
if (subSlot.Inventory?.visualSlots != null) if (subSlot.Inventory?.visualSlots != null)
{ {
@@ -1584,11 +1606,16 @@ namespace Barotrauma
if (DraggingItems.Any() && inventory != null && slotIndex > -1 && slotIndex < inventory.visualSlots.Length) if (DraggingItems.Any() && inventory != null && slotIndex > -1 && slotIndex < inventory.visualSlots.Length)
{ {
var itemInSlot = inventory.slots[slotIndex].FirstOrDefault();
if (inventory.CanBePutInSlot(DraggingItems.First(), slotIndex)) if (inventory.CanBePutInSlot(DraggingItems.First(), slotIndex))
{ {
canBePut = true; canBePut = true;
} }
else if (inventory.slots[slotIndex].FirstOrDefault()?.OwnInventory?.CanBePut(DraggingItems.First()) ?? false) else if
(itemInSlot?.OwnInventory != null &&
itemInSlot.OwnInventory.CanBePut(DraggingItems.First()) &&
itemInSlot.OwnInventory.Container.AllowDragAndDrop &&
itemInSlot.OwnInventory.Container.DrawInventory)
{ {
canBePut = true; canBePut = true;
} }
@@ -286,7 +286,8 @@ namespace Barotrauma
{ {
int padding = 100; int padding = 100;
Vector2 min = new Vector2(-rect.Width / 2 - padding, -rect.Height / 2 - padding); RectangleF boundingBox = GetTransformedQuad().BoundingAxisAlignedRectangle;
Vector2 min = new Vector2(-boundingBox.Width / 2 - padding, -boundingBox.Height / 2 - padding);
Vector2 max = -min; Vector2 max = -min;
foreach (IDrawableComponent drawable in drawableComponents) foreach (IDrawableComponent drawable in drawableComponents)
@@ -386,9 +387,9 @@ namespace Barotrauma
{ {
if (Prefab.ResizeHorizontal || Prefab.ResizeVertical) if (Prefab.ResizeHorizontal || Prefab.ResizeVertical)
{ {
Vector2 size = new Vector2(rect.Width, rect.Height);
if (color.A > 0) if (color.A > 0)
{ {
Vector2 size = new Vector2(rect.Width, rect.Height);
activeSprite.DrawTiled(spriteBatch, new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)) + drawOffset, activeSprite.DrawTiled(spriteBatch, new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)) + drawOffset,
size, color: color, size, color: color,
textureScale: Vector2.One * Scale, textureScale: Vector2.One * Scale,
@@ -401,20 +402,7 @@ namespace Barotrauma
textureScale: Vector2.One * Scale, textureScale: Vector2.One * Scale,
depth: d); depth: d);
} }
foreach (var decorativeSprite in Prefab.DecorativeSprites) DrawDecorativeSprites(spriteBatch, DrawPosition, flippedX && Prefab.CanSpriteFlipX, flippedY && Prefab.CanSpriteFlipY, rotation: 0, depth);
{
if (!spriteAnimState[decorativeSprite].IsActive) { continue; }
Color decorativeSpriteColor = GetSpriteColor(decorativeSprite.Color);
Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, flippedX && Prefab.CanSpriteFlipX ? RotationRad : -RotationRad) * Scale;
if (flippedX && Prefab.CanSpriteFlipX) { offset.X = -offset.X; }
if (flippedY && Prefab.CanSpriteFlipY) { offset.Y = -offset.Y; }
decorativeSprite.Sprite.DrawTiled(spriteBatch,
new Vector2(DrawPosition.X + offset.X - rect.Width / 2, -(DrawPosition.Y + offset.Y + rect.Height / 2)),
size, color: decorativeSpriteColor,
textureScale: Vector2.One * Scale,
depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth), 0.999f));
}
} }
} }
else else
@@ -434,21 +422,8 @@ namespace Barotrauma
Prefab.InfectedSprite?.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, color, Prefab.InfectedSprite.Origin, RotationRad, Scale, activeSprite.effects, depth - 0.001f); Prefab.InfectedSprite?.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, color, Prefab.InfectedSprite.Origin, RotationRad, Scale, activeSprite.effects, depth - 0.001f);
Prefab.DamagedInfectedSprite?.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, Infector.HealthColor, Prefab.DamagedInfectedSprite.Origin, RotationRad, Scale, activeSprite.effects, depth - 0.002f); Prefab.DamagedInfectedSprite?.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, Infector.HealthColor, Prefab.DamagedInfectedSprite.Origin, RotationRad, Scale, activeSprite.effects, depth - 0.002f);
} }
foreach (var decorativeSprite in Prefab.DecorativeSprites)
{
if (!spriteAnimState[decorativeSprite].IsActive) { continue; }
Color decorativeSpriteColor = GetSpriteColor(decorativeSprite.Color); DrawDecorativeSprites(spriteBatch, DrawPosition, flippedX && Prefab.CanSpriteFlipX, flippedY && Prefab.CanSpriteFlipY, -RotationRad, depth);
float rot = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor);
bool flipX = flippedX && Prefab.CanSpriteFlipX;
bool flipY = flippedY && Prefab.CanSpriteFlipY;
Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, flipX ^ flipY ? RotationRad : -RotationRad) * Scale;
if (flipX) { offset.X = -offset.X; }
if (flipY) { offset.Y = -offset.Y; }
decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), decorativeSpriteColor,
RotationRad + rot, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects,
depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth), 0.999f));
}
} }
} }
else if (body.Enabled) else if (body.Enabled)
@@ -492,21 +467,7 @@ namespace Barotrauma
float d = Math.Min(depth + (fadeInBrokenSprite.Sprite.Depth - activeSprite.Depth - 0.000001f), 0.999f); float d = Math.Min(depth + (fadeInBrokenSprite.Sprite.Depth - activeSprite.Depth - 0.000001f), 0.999f);
body.Draw(spriteBatch, fadeInBrokenSprite.Sprite, color * fadeInBrokenSpriteAlpha, d, Scale); body.Draw(spriteBatch, fadeInBrokenSprite.Sprite, color * fadeInBrokenSpriteAlpha, d, Scale);
} }
foreach (var decorativeSprite in Prefab.DecorativeSprites) DrawDecorativeSprites(spriteBatch, body.DrawPosition, flipX: body.Dir < 0, flipY: false, rotation: body.Rotation, depth: depth);
{
if (!spriteAnimState[decorativeSprite].IsActive) { continue; }
float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor);
Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, -RotationRad) * Scale;
if (flippedX && Prefab.CanSpriteFlipX) { offset.X = -offset.X; }
if (flippedY && Prefab.CanSpriteFlipY) { offset.Y = -offset.Y; }
var ca = MathF.Cos(-body.DrawRotation);
var sa = MathF.Sin(-body.DrawRotation);
Vector2 transformedOffset = new Vector2(ca * offset.X + sa * offset.Y, -sa * offset.X + ca * offset.Y);
decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(body.DrawPosition.X + transformedOffset.X, -(body.DrawPosition.Y + transformedOffset.Y)), color,
-body.DrawRotation + rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects,
depth: depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth));
}
} }
foreach (var upgrade in Upgrades) foreach (var upgrade in Upgrades)
@@ -524,7 +485,6 @@ namespace Barotrauma
rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects, rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects,
depth: depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth)); depth: depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth));
} }
} }
activeSprite.effects = oldEffects; activeSprite.effects = oldEffects;
@@ -569,8 +529,14 @@ namespace Barotrauma
Vector2 drawPos = new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)); Vector2 drawPos = new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2));
Vector2 drawSize = new Vector2(MathF.Ceiling(rect.Width + Math.Abs(drawPos.X - (int)drawPos.X)), MathF.Ceiling(rect.Height + Math.Abs(drawPos.Y - (int)drawPos.Y))); Vector2 drawSize = new Vector2(MathF.Ceiling(rect.Width + Math.Abs(drawPos.X - (int)drawPos.X)), MathF.Ceiling(rect.Height + Math.Abs(drawPos.Y - (int)drawPos.Y)));
drawPos = new Vector2(MathF.Floor(drawPos.X), MathF.Floor(drawPos.Y)); drawPos = new Vector2(MathF.Floor(drawPos.X), MathF.Floor(drawPos.Y));
GUI.DrawRectangle(spriteBatch, drawPos, drawSize, GUI.DrawRectangle(sb: spriteBatch,
Color.White, false, 0, thickness: Math.Max(1, (int)(2 / Screen.Selected.Cam.Zoom))); center: drawPos + drawSize * 0.5f,
width: drawSize.X,
height: drawSize.Y,
rotation: RotationRad,
clr: Color.White,
depth: 0,
thickness: 2f / Screen.Selected.Cam.Zoom);
foreach (Rectangle t in Prefab.Triggers) foreach (Rectangle t in Prefab.Triggers)
{ {
@@ -629,6 +595,55 @@ namespace Barotrauma
} }
} }
public void DrawDecorativeSprites(SpriteBatch spriteBatch, Vector2 drawPos, bool flipX, bool flipY, float rotation, float depth)
{
foreach (var decorativeSprite in Prefab.DecorativeSprites)
{
Color decorativeSpriteColor = GetSpriteColor(decorativeSprite.Color).Multiply(GetSpriteColor(spriteColor));
if (!spriteAnimState[decorativeSprite].IsActive) { continue; }
Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier,
flipX ^ flipY ? -rotation : rotation) * Scale;
if (ResizeHorizontal || ResizeVertical)
{
decorativeSprite.Sprite.DrawTiled(spriteBatch,
new Vector2(DrawPosition.X + offset.X - rect.Width / 2, -(DrawPosition.Y + offset.Y + rect.Height / 2)),
new Vector2(rect.Width, rect.Height), color: decorativeSpriteColor,
textureScale: Vector2.One * Scale,
depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth), 0.999f));
}
else
{
float spriteRotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor);
Vector2 origin = decorativeSprite.Sprite.Origin;
SpriteEffects spriteEffects = SpriteEffects.None;
if (flipX && Prefab.CanSpriteFlipX)
{
offset.X = -offset.X;
origin.X = -origin.X + decorativeSprite.Sprite.size.X;
spriteEffects = SpriteEffects.FlipHorizontally;
}
if (flipY && Prefab.CanSpriteFlipY)
{
offset.Y = -offset.Y;
origin.Y = -origin.Y + decorativeSprite.Sprite.size.Y;
spriteEffects |= SpriteEffects.FlipVertically;
}
if (body != null)
{
var ca = MathF.Cos(-body.DrawRotation);
var sa = MathF.Sin(-body.DrawRotation);
offset = new Vector2(ca * offset.X + sa * offset.Y, -sa * offset.X + ca * offset.Y);
}
decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(drawPos.X + offset.X, -(drawPos.Y + offset.Y)), decorativeSpriteColor, origin,
-rotation + spriteRotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, spriteEffects,
depth: depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth));
}
}
}
partial void OnCollisionProjSpecific(float impact) partial void OnCollisionProjSpecific(float impact)
{ {
if (impact > 1.0f && if (impact > 1.0f &&
@@ -804,6 +819,19 @@ namespace Barotrauma
} }
} }
public override bool IsMouseOn(Vector2 position)
{
Vector2 rectSize = rect.Size.ToVector2();
Vector2 bodyPos = WorldPosition;
Vector2 transformedMousePos = MathUtils.RotatePointAroundTarget(position, bodyPos, RotationRad);
return
Math.Abs(transformedMousePos.X - bodyPos.X) < rectSize.X / 2.0f &&
Math.Abs(transformedMousePos.Y - bodyPos.Y) < rectSize.Y / 2.0f;
}
public GUIComponent CreateEditingHUD(bool inGame = false) public GUIComponent CreateEditingHUD(bool inGame = false)
{ {
activeEditors.Clear(); activeEditors.Clear();
@@ -861,6 +889,11 @@ namespace Barotrauma
CanBeFocused = true CanBeFocused = true
}; };
GUINumberInput rotationField =
itemEditor.Fields.TryGetValue("Rotation".ToIdentifier(), out var rotationFieldComponents)
? rotationFieldComponents.OfType<GUINumberInput>().FirstOrDefault()
: null;
var mirrorX = new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityX"), style: "GUIButtonSmall") var mirrorX = new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityX"), style: "GUIButtonSmall")
{ {
ToolTip = TextManager.Get("MirrorEntityXToolTip"), ToolTip = TextManager.Get("MirrorEntityXToolTip"),
@@ -873,6 +906,7 @@ namespace Barotrauma
} }
if (!SelectedList.Contains(this)) { FlipX(relativeToSub: false); } if (!SelectedList.Contains(this)) { FlipX(relativeToSub: false); }
ColorFlipButton(button, FlippedX); ColorFlipButton(button, FlippedX);
if (rotationField != null) { rotationField.FloatValue = Rotation; }
return true; return true;
} }
}; };
@@ -889,6 +923,7 @@ namespace Barotrauma
} }
if (!SelectedList.Contains(this)) { FlipY(relativeToSub: false); } if (!SelectedList.Contains(this)) { FlipY(relativeToSub: false); }
ColorFlipButton(button, FlippedY); ColorFlipButton(button, FlippedY);
if (rotationField != null) { rotationField.FloatValue = Rotation; }
return true; return true;
} }
}; };
@@ -1553,6 +1588,23 @@ namespace Barotrauma
RemoveFromDroppedStack(allowClientExecute: true); RemoveFromDroppedStack(allowClientExecute: true);
} }
break; break;
case EventType.SetHighlight:
bool isTargetedForThisClient = msg.ReadBoolean();
if (isTargetedForThisClient)
{
bool highlight = msg.ReadBoolean();
ExternalHighlight = highlight;
if (highlight)
{
Color highlightColor = msg.ReadColorR8G8B8A8();
HighlightColor = highlightColor;
}
else
{
HighlightColor = null;
}
}
break;
default: default:
throw new Exception($"Malformed incoming item event: unsupported event type {eventType}"); throw new Exception($"Malformed incoming item event: unsupported event type {eventType}");
} }
@@ -1953,5 +2005,13 @@ namespace Barotrauma
Inventory.DraggingSlot = null; Inventory.DraggingSlot = null;
} }
} }
public void OnPlayerSkillsChanged()
{
foreach (ItemComponent ic in components)
{
ic.OnPlayerSkillsChanged();
}
}
} }
} }
@@ -371,19 +371,27 @@ namespace Barotrauma
{ {
if (!ResizeHorizontal && !ResizeVertical) if (!ResizeHorizontal && !ResizeVertical)
{ {
sprite.Draw(spriteBatch, new Vector2(placeRect.Center.X, -(placeRect.Y - placeRect.Height / 2)), SpriteColor * 0.8f, scale: scale, rotate: rotation); sprite.Draw(
spriteBatch: spriteBatch,
pos: new Vector2(placeRect.Center.X,
-(placeRect.Y - placeRect.Height / 2)),
color: SpriteColor * 0.8f,
scale: scale,
rotate: rotation,
spriteEffect: spriteEffects ^ sprite.effects);
} }
else else
{ {
Vector2 position = placeRect.Location.ToVector2(); Vector2 position = placeRect.Location.ToVector2();
Vector2 placeSize = placeRect.Size.ToVector2(); Vector2 placeSize = placeRect.Size.ToVector2();
sprite?.DrawTiled( sprite?.DrawTiled(
spriteBatch, spriteBatch: spriteBatch,
new Vector2(position.X, -position.Y), position: new Vector2(position.X, -position.Y),
placeSize, targetSize: placeSize,
rotation: rotation, rotation: rotation,
textureScale: Vector2.One * scale, textureScale: Vector2.One * scale,
color: SpriteColor * 0.8f); color: SpriteColor * 0.8f,
spriteEffects: spriteEffects ^ sprite.effects);
} }
} }
@@ -86,7 +86,7 @@ namespace Barotrauma
MathUtils.RoundTowardsClosest(center.Y, Submarine.GridSize.Y) - center.Y - Submarine.GridSize.Y / 2); MathUtils.RoundTowardsClosest(center.Y, Submarine.GridSize.Y) - center.Y - Submarine.GridSize.Y / 2);
MapEntity.SelectedList.Clear(); MapEntity.SelectedList.Clear();
assemblyEntities.ForEach(e => MapEntity.AddSelection(e)); entities.ForEach(e => MapEntity.AddSelection(e));
foreach (MapEntity mapEntity in assemblyEntities) foreach (MapEntity mapEntity in assemblyEntities)
{ {
@@ -1,10 +1,9 @@
using Barotrauma.Networking; using Barotrauma.Networking;
using FarseerPhysics;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
using System; using System;
using System.Linq;
using System.Collections.Generic; using System.Collections.Generic;
using FarseerPhysics;
namespace Barotrauma namespace Barotrauma
{ {
@@ -162,6 +162,8 @@ namespace Barotrauma
CanBeVisible = CanBeVisible =
Sprite != null || Sprite != null ||
Prefab.DeformableSprite != null || Prefab.DeformableSprite != null ||
ParticleEmitters is { Length: > 0 } ||
(GameMain.DebugDraw && Triggers is { Count: > 0 }) ||
Prefab.OverrideProperties.Any(p => p != null && (p.Sprites.Any() || p.DeformableSprite != null)); Prefab.OverrideProperties.Any(p => p != null && (p.Sprites.Any() || p.DeformableSprite != null));
} }
@@ -24,6 +24,13 @@ namespace Barotrauma
public bool ForceRefreshVisibleObjects; public bool ForceRefreshVisibleObjects;
partial void RemoveProjSpecific()
{
visibleObjectsBack.Clear();
visibleObjectsMid.Clear();
visibleObjectsFront.Clear();
}
partial void UpdateProjSpecific(float deltaTime) partial void UpdateProjSpecific(float deltaTime)
{ {
foreach (LevelObject obj in visibleObjectsBack) foreach (LevelObject obj in visibleObjectsBack)
@@ -133,6 +133,8 @@ namespace Barotrauma.Lights
public Rectangle BoundingBox { get; private set; } public Rectangle BoundingBox { get; private set; }
public bool IsInvalid { get; private set; }
public ConvexHull(Rectangle rect, bool isHorizontal, MapEntity parent) public ConvexHull(Rectangle rect, bool isHorizontal, MapEntity parent)
{ {
shadowEffect ??= new BasicEffect(GameMain.Instance.GraphicsDevice) shadowEffect ??= new BasicEffect(GameMain.Instance.GraphicsDevice)
@@ -481,15 +483,34 @@ namespace Barotrauma.Lights
for (int i = 0; i < 4; i++) for (int i = 0; i < 4; i++)
{ {
vertices[i].WorldPos = vertices[i].Pos; vertices[i].WorldPos = vertices[i].Pos;
ValidateVertex(vertices[i].WorldPos, "vertices[i].Pos");
segments[i].Start.WorldPos = segments[i].Start.Pos; segments[i].Start.WorldPos = segments[i].Start.Pos;
ValidateVertex(segments[i].Start.WorldPos, "segments[i].Start.Pos");
segments[i].End.WorldPos = segments[i].End.Pos; segments[i].End.WorldPos = segments[i].End.Pos;
ValidateVertex(segments[i].End.WorldPos, "segments[i].End.Pos");
} }
if (ParentEntity == null || ParentEntity.Submarine == null) { return; } if (ParentEntity == null || ParentEntity.Submarine == null) { return; }
for (int i = 0; i < 4; i++) for (int i = 0; i < 4; i++)
{ {
vertices[i].WorldPos += ParentEntity.Submarine.DrawPosition; vertices[i].WorldPos += ParentEntity.Submarine.DrawPosition;
ValidateVertex(vertices[i].WorldPos, "vertices[i].WorldPos");
segments[i].Start.WorldPos += ParentEntity.Submarine.DrawPosition; segments[i].Start.WorldPos += ParentEntity.Submarine.DrawPosition;
ValidateVertex(segments[i].Start.WorldPos, "segments[i].Start.WorldPos");
segments[i].End.WorldPos += ParentEntity.Submarine.DrawPosition; segments[i].End.WorldPos += ParentEntity.Submarine.DrawPosition;
ValidateVertex(segments[i].End.WorldPos, "segments[i].End.WorldPos");
}
void ValidateVertex(Vector2 vertex, string debugName)
{
if (!MathUtils.IsValid(vertex))
{
IsInvalid = true;
string errorMsg = $"Invalid vertex on convex hull ({debugName}: {vertex}, parent entity: {ParentEntity?.ToString() ?? "null"}).";
#if DEBUG
DebugConsole.ThrowError(errorMsg);
#endif
GameAnalyticsManager.AddErrorEventOnce("ConvexHull.RefreshWorldPositions:InvalidVertex", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
}
} }
} }
@@ -234,6 +234,15 @@ namespace Barotrauma.Lights
} }
} }
public void DebugDrawVertices(SpriteBatch spriteBatch)
{
foreach (LightSource light in lights)
{
if (!light.Enabled) { continue; }
light.DebugDrawVertices(spriteBatch);
}
}
public void RenderLightMap(GraphicsDevice graphics, SpriteBatch spriteBatch, Camera cam, RenderTarget2D backgroundObstructor = null) public void RenderLightMap(GraphicsDevice graphics, SpriteBatch spriteBatch, Camera cam, RenderTarget2D backgroundObstructor = null)
{ {
if (!LightingEnabled) { return; } if (!LightingEnabled) { return; }
@@ -259,7 +268,8 @@ namespace Barotrauma.Lights
{ {
if (!light.Enabled) { continue; } if (!light.Enabled) { continue; }
if ((light.Color.A < 1 || light.Range < 1.0f) && !light.LightSourceParams.OverrideLightSpriteAlpha.HasValue) { continue; } if ((light.Color.A < 1 || light.Range < 1.0f) && !light.LightSourceParams.OverrideLightSpriteAlpha.HasValue) { continue; }
//above the top boundary of the level (in an inactive respawn shuttle?)
if (Level.Loaded != null && light.WorldPosition.Y > Level.Loaded.Size.Y) { continue; }
if (light.ParentBody != null) if (light.ParentBody != null)
{ {
light.ParentBody.UpdateDrawPosition(); light.ParentBody.UpdateDrawPosition();
@@ -801,6 +811,8 @@ namespace Barotrauma.Lights
public void ClearLights() public void ClearLights()
{ {
activeLights.Clear();
activeLightsWithLightVolume.Clear();
lights.Clear(); lights.Clear();
} }
} }
@@ -37,9 +37,9 @@ namespace Barotrauma.Lights
TextureRange = range; TextureRange = range;
if (OverrideLightTexture != null) if (OverrideLightTexture != null)
{ {
TextureRange += Math.Max( TextureRange *= 1.0f + Math.Max(
Math.Abs(OverrideLightTexture.RelativeOrigin.X - 0.5f) * OverrideLightTexture.size.X, Math.Abs(OverrideLightTexture.RelativeOrigin.X - 0.5f),
Math.Abs(OverrideLightTexture.RelativeOrigin.Y - 0.5f) * OverrideLightTexture.size.Y); Math.Abs(OverrideLightTexture.RelativeOrigin.Y - 0.5f));
} }
} }
} }
@@ -238,7 +238,11 @@ namespace Barotrauma.Lights
private bool needsRecalculationWhenUpToDate; private bool needsRecalculationWhenUpToDate;
public bool NeedsRecalculation public bool NeedsRecalculation
{ {
get { return needsRecalculation; } get
{
if (ParentBody?.UserData is Item it && it.Prefab.Identifier == "flashlight") { return true; }
return needsRecalculation;
}
set set
{ {
if (!needsRecalculation && value) if (!needsRecalculation && value)
@@ -708,6 +712,7 @@ namespace Barotrauma.Lights
{ {
foreach (ConvexHull hull in chList.List) foreach (ConvexHull hull in chList.List)
{ {
if (hull.IsInvalid) { continue; }
if (!chList.IsHidden.Contains(hull)) if (!chList.IsHidden.Contains(hull))
{ {
//find convexhull segments that are close enough and facing towards the light source //find convexhull segments that are close enough and facing towards the light source
@@ -735,6 +740,7 @@ namespace Barotrauma.Lights
GameMain.LightManager.AddRayCastTask(this, drawPos, rotation); GameMain.LightManager.AddRayCastTask(this, drawPos, rotation);
} }
const float MinPointDistance = 6;
public void RayCastTask(Vector2 drawPos, float rotation) public void RayCastTask(Vector2 drawPos, float rotation)
{ {
@@ -877,12 +883,11 @@ namespace Barotrauma.Lights
} }
} }
const float MinPointDistance = 6;
//remove points that are very close to each other //remove points that are very close to each other
for (int i = 0; i < points.Count; i++) //+= 2 because the points are added in pairs above, i.e. 0 and 1 belong to the same segment
for (int i = 0; i < points.Count; i += 2)
{ {
for (int j = Math.Min(i + 4, points.Count - 1); j > i; j--) for (int j = Math.Min(i + 2, points.Count - 1); j > i; j--)
{ {
if (Math.Abs(points[i].WorldPos.X - points[j].WorldPos.X) < MinPointDistance && if (Math.Abs(points[i].WorldPos.X - points[j].WorldPos.X) < MinPointDistance &&
Math.Abs(points[i].WorldPos.Y - points[j].WorldPos.Y) < MinPointDistance) Math.Abs(points[i].WorldPos.Y - points[j].WorldPos.Y) < MinPointDistance)
@@ -892,14 +897,14 @@ namespace Barotrauma.Lights
} }
} }
var compareCCW = new CompareSegmentPointCW(drawPos);
try try
{ {
points.Sort(compareCCW); var compareCW = new CompareSegmentPointCW(drawPos);
points.Sort(compareCW);
} }
catch (Exception e) catch (Exception e)
{ {
StringBuilder sb = new StringBuilder("Constructing light volumes failed! Light pos: " + drawPos + ", Hull verts:\n"); StringBuilder sb = new StringBuilder($"Constructing light volumes failed ({nameof(CompareSegmentPointCW)})! Light pos: {drawPos}, Hull verts:\n");
foreach (SegmentPoint sp in points) foreach (SegmentPoint sp in points)
{ {
sb.AppendLine(sp.Pos.ToString()); sb.AppendLine(sp.Pos.ToString());
@@ -914,7 +919,11 @@ namespace Barotrauma.Lights
verts.Clear(); verts.Clear();
foreach (SegmentPoint p in points) foreach (SegmentPoint p in points)
{ {
Vector2 dir = Vector2.Normalize(p.WorldPos - drawPos); Vector2 diff = p.WorldPos - drawPos;
float dist = diff.Length();
//light source exactly at the segment point, don't cast a shadow (normalizing the vector would lead to NaN)
if (dist <= 0.0001f) { continue; }
Vector2 dir = diff / dist;
Vector2 dirNormal = new Vector2(-dir.Y, dir.X) * MinPointDistance; Vector2 dirNormal = new Vector2(-dir.Y, dir.X) * MinPointDistance;
//do two slightly offset raycasts to hit the segment itself and whatever's behind it //do two slightly offset raycasts to hit the segment itself and whatever's behind it
@@ -940,9 +949,36 @@ namespace Barotrauma.Lights
{ {
//the raycasts landed on different segments //the raycasts landed on different segments
//we definitely want to generate new geometry here //we definitely want to generate new geometry here
verts.Add(isPoint1 ? p.WorldPos : intersection1.pos); if (isPoint1)
verts.Add(isPoint2 ? p.WorldPos : intersection2.pos); {
markAsVisible = true; TryAddPoints(intersection2.pos, p.WorldPos, drawPos, verts);
markAsVisible = true;
}
else if (isPoint2)
{
TryAddPoints(intersection1.pos, p.WorldPos, drawPos, verts);
markAsVisible = true;
}
else
{
//didn't hit either point, completely obstructed
verts.Add(intersection1.pos);
verts.Add(intersection2.pos);
}
static void TryAddPoints(Vector2 intersection, Vector2 point, Vector2 refPos, List<Vector2> verts)
{
//* 0.8f because we don't care about obstacles that are very close (intersecting walls),
//only about obstacles that are clearly between the point and the refPos
bool intersectionCloserThanPoint = Vector2.DistanceSquared(intersection, refPos) < Vector2.DistanceSquared(point, refPos) * 0.8f;
//if the raycast hit a segment that's closer than the point we're aiming towards,
//it means we didn't hit a segment behind the point, but something that's obstructing it
//= we don't want to add vertex at that obstructed point, it could make the light go through obstacles
if (!intersectionCloserThanPoint)
{
verts.Add(point);
}
verts.Add(intersection);
}
} }
if (markAsVisible) if (markAsVisible)
{ {
@@ -959,15 +995,32 @@ namespace Barotrauma.Lights
//remove points that are very close to each other //remove points that are very close to each other
for (int i = 0; i < verts.Count - 1; i++) for (int i = 0; i < verts.Count - 1; i++)
{ {
for (int j = Math.Min(i + 4, verts.Count - 1); j > i; j--) for (int j = verts.Count - 1; j > i; j--)
{ {
if (Math.Abs(verts[i].X - verts[j].X) < 6 && if (Math.Abs(verts[i].X - verts[j].X) < MinPointDistance &&
Math.Abs(verts[i].Y - verts[j].Y) < 6) Math.Abs(verts[i].Y - verts[j].Y) < MinPointDistance)
{ {
verts.RemoveAt(j); verts.RemoveAt(j);
} }
} }
} }
try
{
var compareCW = new CompareCW(drawPos);
verts.Sort(compareCW);
}
catch (Exception e)
{
StringBuilder sb = new StringBuilder($"Constructing light volumes failed ({nameof(CompareSegmentPointCW)})! Light pos: {drawPos}, verts:\n");
foreach (Vector2 v in verts)
{
sb.AppendLine(v.ToString());
}
DebugConsole.ThrowError(sb.ToString(), e);
}
calculatedDrawPos = drawPos; calculatedDrawPos = drawPos;
state = LightVertexState.PendingVertexRecalculation; state = LightVertexState.PendingVertexRecalculation;
} }
@@ -1114,7 +1167,7 @@ namespace Barotrauma.Lights
//add the normals together and use some magic numbers to create //add the normals together and use some magic numbers to create
//a somewhat useful/good-looking blur //a somewhat useful/good-looking blur
float blurDistance = 40.0f; float blurDistance = 25.0f;
Vector2 nDiff = nDiff1 * blurDistance; Vector2 nDiff = nDiff1 * blurDistance;
if (MathUtils.GetLineIntersection(vertex + (nDiff1 * blurDistance), nextVertex + (nDiff1 * blurDistance), vertex + (nDiff2 * blurDistance), prevVertex + (nDiff2 * blurDistance), true, out Vector2 intersection)) if (MathUtils.GetLineIntersection(vertex + (nDiff1 * blurDistance), nextVertex + (nDiff1 * blurDistance), vertex + (nDiff2 * blurDistance), prevVertex + (nDiff2 * blurDistance), true, out Vector2 intersection))
{ {
@@ -1230,7 +1283,8 @@ namespace Barotrauma.Lights
/// <param name="spriteBatch"></param> /// <param name="spriteBatch"></param>
public void DrawSprite(SpriteBatch spriteBatch, Camera cam) public void DrawSprite(SpriteBatch spriteBatch, Camera cam)
{ {
if (GameMain.DebugDraw) //uncomment if you want to visualize the bounds of the light volume
/*if (GameMain.DebugDraw)
{ {
Vector2 drawPos = position; Vector2 drawPos = position;
if (ParentSub != null) if (ParentSub != null)
@@ -1269,7 +1323,7 @@ namespace Barotrauma.Lights
{ {
GUI.DrawLine(spriteBatch, boundaryCorners[i].Pos, boundaryCorners[(i + 1) % 4].Pos, Color.White, 0, 3); GUI.DrawLine(spriteBatch, boundaryCorners[i].Pos, boundaryCorners[(i + 1) % 4].Pos, Color.White, 0, 3);
} }
} }*/
if (DeformableLightSprite != null) if (DeformableLightSprite != null)
{ {
@@ -1367,6 +1421,38 @@ namespace Barotrauma.Lights
} }
} }
public void DebugDrawVertices(SpriteBatch spriteBatch)
{
if (Range < 1.0f || Color.A < 1 || CurrentBrightness <= 0.0f) { return; }
//commented out because this is mostly just useful in very specific situations, otherwise it just makes debugdraw very messy
//(you may also need to add a condition here that only draws this for the specific light you're interested in)
if (GameMain.DebugDraw && vertices != null)
{
if (ParentBody?.UserData is Item it && it.Prefab.Identifier == "flashlight")
for (int i = 1; i < vertices.Length - 1; i += 2)
{
Vector2 vert1 = new Vector2(vertices[i].Position.X, vertices[i].Position.Y);
int nextIndex = (i + 2) % vertices.Length;
//the first vertex is the one at the position of the light source, skip that one
//(we just want to draw lines between the vertices at the circumference of the light volume)
if (nextIndex == 0) { nextIndex++; }
Vector2 vert2 = new Vector2(vertices[nextIndex].Position.X, vertices[nextIndex].Position.Y);
if (ParentSub != null)
{
vert1 += ParentSub.DrawPosition;
vert2 += ParentSub.DrawPosition;
}
vert1.Y = -vert1.Y;
vert2.Y = -vert2.Y;
var randomColor = ToolBox.GradientLerp(i / (float)vertices.Length, Color.Magenta, Color.Blue, Color.Yellow, Color.Green, Color.Cyan, Color.Red, Color.Purple, Color.Yellow);
GUI.DrawLine(spriteBatch, vert1, vert2, randomColor * 0.8f, width: 2);
}
}
}
public void DrawLightVolume(SpriteBatch spriteBatch, BasicEffect lightEffect, Matrix transform, bool allowRecalculation, ref int recalculationCount) public void DrawLightVolume(SpriteBatch spriteBatch, BasicEffect lightEffect, Matrix transform, bool allowRecalculation, ref int recalculationCount)
{ {
if (Range < 1.0f || Color.A < 1 || CurrentBrightness <= 0.0f) { return; } if (Range < 1.0f || Color.A < 1 || CurrentBrightness <= 0.0f) { return; }
@@ -291,14 +291,14 @@ namespace Barotrauma
private readonly List<MapNotification> mapNotifications = new List<MapNotification>(); private readonly List<MapNotification> mapNotifications = new List<MapNotification>();
partial void ChangeLocationTypeProjSpecific(Location location, string prevName, LocationTypeChange change) partial void ChangeLocationTypeProjSpecific(Location location, LocalizedString prevName, LocationTypeChange change)
{ {
var messages = change.GetMessages(location.Faction); var messages = change.GetMessages(location.Faction);
if (!messages.Any()) { return; } if (!messages.Any()) { return; }
string msg = messages.GetRandom(Rand.RandSync.Unsynced) string msg = messages.GetRandom(Rand.RandSync.Unsynced)
.Replace("[previousname]", $"‖color:gui.yellow‖{prevName}‖end‖") .Replace("[previousname]", $"‖color:gui.yellow‖{prevName}‖end‖")
.Replace("[name]", $"‖color:gui.yellow‖{location.Name}‖end‖"); .Replace("[name]", $"‖color:gui.yellow‖{location.DisplayName}‖end‖");
location.LastTypeChangeMessage = msg; location.LastTypeChangeMessage = msg;
mapNotifications.Add(new MapNotification(msg, GUIStyle.SubHeadingFont, mapNotifications, location)); mapNotifications.Add(new MapNotification(msg, GUIStyle.SubHeadingFont, mapNotifications, location));
@@ -377,7 +377,7 @@ namespace Barotrauma
bool showReputation = hudVisibility > 0.0f && location.Type.HasOutpost && location.Reputation != null; bool showReputation = hudVisibility > 0.0f && location.Type.HasOutpost && location.Reputation != null;
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), location.Name, font: GUIStyle.LargeFont) { Padding = Vector4.Zero }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), location.DisplayName, font: GUIStyle.LargeFont) { Padding = Vector4.Zero };
if (!location.Type.Name.IsNullOrEmpty()) if (!location.Type.Name.IsNullOrEmpty())
{ {
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), location.Type.Name, font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), location.Type.Name, font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero };
@@ -1080,6 +1080,7 @@ namespace Barotrauma
} }
float dist = Vector2.Distance(start, end); float dist = Vector2.Distance(start, end);
var connectionSprite = connection.Passed ? generationParams.PassedConnectionSprite : generationParams.ConnectionSprite; var connectionSprite = connection.Passed ? generationParams.PassedConnectionSprite : generationParams.ConnectionSprite;
if (connectionSprite?.Texture == null) { continue; }
Color segmentColor = connectionColor; Color segmentColor = connectionColor;
int segmentWidth = width; int segmentWidth = width;
@@ -1092,9 +1093,6 @@ namespace Barotrauma
segmentWidth /= 2; segmentWidth /= 2;
segmentColor = connection.Passed ? generationParams.ConnectionColor : generationParams.UnvisitedConnectionColor; segmentColor = connection.Passed ? generationParams.ConnectionColor : generationParams.UnvisitedConnectionColor;
} }
else
{
}
} }
spriteBatch.Draw(connectionSprite.Texture, spriteBatch.Draw(connectionSprite.Texture,
@@ -162,21 +162,9 @@ namespace Barotrauma
{ {
if (SelectedAny) if (SelectedAny)
{ {
if (SelectedList.Any(static t => t is Item it && it.GetComponent<CircuitBox>() is not null)) SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List<MapEntity>(SelectedList), true));
{ SelectedList.ForEachMod(static e => { if (!e.Removed) { e.Remove(); } });
GUI.AskForConfirmation(SubEditorScreen.CircuitBoxDeletionWarningHeader, SubEditorScreen.CircuitBoxDeletionWarningBody, onConfirm: Delete); SelectedList.Clear();
}
else
{
Delete();
}
void Delete()
{
SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List<MapEntity>(SelectedList), true));
SelectedList.ForEach(static e => { if (!e.Removed) { e.Remove(); } });
SelectedList.Clear();
}
} }
} }
@@ -1332,12 +1320,15 @@ namespace Barotrauma
HashSet<MapEntity> foundEntities = new HashSet<MapEntity>(); HashSet<MapEntity> foundEntities = new HashSet<MapEntity>();
Rectangle selectionRect = Submarine.AbsRect(pos, size); Rectangle selectionRect = Submarine.AbsRect(pos, size);
Quad2D selectionQuad = Quad2D.FromSubmarineRectangle(selectionRect);
foreach (MapEntity entity in MapEntityList) foreach (MapEntity entity in MapEntityList)
{ {
if (!entity.SelectableInEditor) { continue; } if (!entity.SelectableInEditor) { continue; }
if (Submarine.RectsOverlap(selectionRect, entity.rect)) Quad2D entityQuad = entity.GetTransformedQuad();
if (selectionQuad.Intersects(entityQuad))
{ {
foundEntities.Add(entity); foundEntities.Add(entity);
entity.IsIncludedInSelection = true; entity.IsIncludedInSelection = true;
@@ -236,11 +236,14 @@ namespace Barotrauma
public override bool IsVisible(Rectangle worldView) public override bool IsVisible(Rectangle worldView)
{ {
Rectangle worldRect = WorldRect; RectangleF worldRect = Quad2D.FromSubmarineRectangle(WorldRect).Rotated(
FlippedX != FlippedY
? rotationRad
: -rotationRad).BoundingAxisAlignedRectangle;
Vector2 worldPos = WorldPosition; Vector2 worldPos = WorldPosition;
Vector2 min = new Vector2(worldRect.X, worldRect.Y - worldRect.Height); Vector2 min = new Vector2(worldRect.X, worldRect.Y);
Vector2 max = new Vector2(worldRect.Right, worldRect.Y); Vector2 max = new Vector2(worldRect.Right, worldRect.Y + worldRect.Height);
foreach (DecorativeSprite decorativeSprite in Prefab.DecorativeSprites) foreach (DecorativeSprite decorativeSprite in Prefab.DecorativeSprites)
{ {
float scale = decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale; float scale = decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale;
@@ -312,7 +315,12 @@ namespace Barotrauma
Vector2 bodyPos = WorldPosition + BodyOffset * Scale; Vector2 bodyPos = WorldPosition + BodyOffset * Scale;
GUI.DrawRectangle(spriteBatch, new Vector2(bodyPos.X, -bodyPos.Y), rectSize.X, rectSize.Y, BodyRotation, Color.White, GUI.DrawRectangle(sb: spriteBatch,
center: new Vector2(bodyPos.X, -bodyPos.Y),
width: rectSize.X,
height: rectSize.Y,
rotation: BodyRotation,
clr: Color.White,
thickness: Math.Max(1, (int)(2 / Screen.Selected.Cam.Zoom))); thickness: Math.Max(1, (int)(2 / Screen.Selected.Cam.Zoom)));
} }
@@ -96,9 +96,6 @@ namespace Barotrauma
public override void DrawPlacing(SpriteBatch spriteBatch, Rectangle placeRect, float scale = 1.0f, float rotation = 0.0f, SpriteEffects spriteEffects = SpriteEffects.None) public override void DrawPlacing(SpriteBatch spriteBatch, Rectangle placeRect, float scale = 1.0f, float rotation = 0.0f, SpriteEffects spriteEffects = SpriteEffects.None)
{ {
SpriteEffects oldEffects = Sprite.effects;
Sprite.effects ^= spriteEffects;
var position = placeRect.Location.ToVector2().FlipY(); var position = placeRect.Location.ToVector2().FlipY();
position += placeRect.Size.ToVector2() * 0.5f; position += placeRect.Size.ToVector2() * 0.5f;
@@ -109,9 +106,8 @@ namespace Barotrauma
color: Color.White * 0.8f, color: Color.White * 0.8f,
origin: placeRect.Size.ToVector2() * 0.5f, origin: placeRect.Size.ToVector2() * 0.5f,
rotation: rotation, rotation: rotation,
textureScale: TextureScale * scale); textureScale: TextureScale * scale,
spriteEffects: spriteEffects ^ Sprite.effects);
Sprite.effects = oldEffects;
} }
} }
} }
@@ -421,7 +421,7 @@ namespace Barotrauma
float scale = element.GetAttributeFloat("scale", 1f); float scale = element.GetAttributeFloat("scale", 1f);
Color color = element.GetAttributeColor("spritecolor", Color.White); Color color = element.GetAttributeColor("spritecolor", Color.White);
float rotation = element.GetAttributeFloat("rotation", 0f); float rotationRad = MathHelper.ToRadians(element.GetAttributeFloat("rotation", 0f));
MapEntityPrefab prefab; MapEntityPrefab prefab;
if (element.NameAsIdentifier() == "item" if (element.NameAsIdentifier() == "item"
@@ -455,7 +455,7 @@ namespace Barotrauma
ItemPrefab itemPrefab = prefab as ItemPrefab; ItemPrefab itemPrefab = prefab as ItemPrefab;
if (itemPrefab != null) if (itemPrefab != null)
{ {
BakeItemComponents(itemPrefab, rect, color, scale, rotation, depth, out overrideSprite); BakeItemComponents(itemPrefab, rect, color, scale, rotationRad, depth, out overrideSprite);
} }
if (!overrideSprite) if (!overrideSprite)
@@ -485,13 +485,15 @@ namespace Barotrauma
MathUtils.PositiveModulo((int)-textureOffset.Y, prefab.Sprite.SourceRect.Height)); MathUtils.PositiveModulo((int)-textureOffset.Y, prefab.Sprite.SourceRect.Height));
prefab.Sprite.DrawTiled( prefab.Sprite.DrawTiled(
spriteRecorder, spriteBatch: spriteRecorder,
rect.Location.ToVector2() * new Vector2(1f, -1f), position: new Vector2(rect.X + rect.Width / 2, -(rect.Y - rect.Height / 2)),
rect.Size.ToVector2(), targetSize: rect.Size.ToVector2(),
origin: rect.Size.ToVector2() * new Vector2(0.5f, 0.5f),
color: color, color: color,
startOffset: backGroundOffset, startOffset: backGroundOffset,
textureScale: textureScale * scale, textureScale: textureScale * scale,
depth: depth); depth: depth,
rotation: rotationRad);
} }
else if (itemPrefab != null) else if (itemPrefab != null)
{ {
@@ -552,7 +554,7 @@ namespace Barotrauma
spritePos * new Vector2(1f, -1f), spritePos * new Vector2(1f, -1f),
color, color,
prefab.Sprite.Origin, prefab.Sprite.Origin,
rotation, rotationRad,
scale, scale,
prefab.Sprite.effects, depth); prefab.Sprite.effects, depth);
@@ -564,7 +566,7 @@ namespace Barotrauma
if (flippedX) { offset.X = -offset.X; } if (flippedX) { offset.X = -offset.X; }
if (flippedY) { offset.Y = -offset.Y; } if (flippedY) { offset.Y = -offset.Y; }
decorativeSprite.Sprite.Draw(spriteRecorder, new Vector2(spritePos.X + offset.X, -(spritePos.Y + offset.Y)), color, decorativeSprite.Sprite.Draw(spriteRecorder, new Vector2(spritePos.X + offset.X, -(spritePos.Y + offset.Y)), color,
MathHelper.ToRadians(rotation) + rot, decorativeSprite.GetScale(0f) * scale, prefab.Sprite.effects, rotationRad + rot, decorativeSprite.GetScale(0f) * scale, prefab.Sprite.effects,
depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - prefab.Sprite.Depth), 0.999f)); depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - prefab.Sprite.Depth), 0.999f));
} }
} }
@@ -577,7 +579,7 @@ namespace Barotrauma
private void BakeItemComponents( private void BakeItemComponents(
ItemPrefab prefab, ItemPrefab prefab,
Rectangle rect, Color color, Rectangle rect, Color color,
float scale, float rotation, float depth, float scale, float rotationRad, float depth,
out bool overrideSprite) out bool overrideSprite)
{ {
overrideSprite = false; overrideSprite = false;
@@ -607,7 +609,7 @@ namespace Barotrauma
Vector2 relativeBarrelPos = barrelPos * prefab.Scale - new Vector2(rect.Width / 2, rect.Height / 2); Vector2 relativeBarrelPos = barrelPos * prefab.Scale - new Vector2(rect.Width / 2, rect.Height / 2);
var transformedBarrelPos = MathUtils.RotatePoint( var transformedBarrelPos = MathUtils.RotatePoint(
relativeBarrelPos, relativeBarrelPos,
MathHelper.ToRadians(rotation)); rotationRad);
Vector2 drawPos = new Vector2(rect.X + rect.Width * relativeScale / 2 + transformedBarrelPos.X * relativeScale, rect.Y - rect.Height * relativeScale / 2 - transformedBarrelPos.Y * relativeScale); Vector2 drawPos = new Vector2(rect.X + rect.Width * relativeScale / 2 + transformedBarrelPos.X * relativeScale, rect.Y - rect.Height * relativeScale / 2 - transformedBarrelPos.Y * relativeScale);
drawPos.Y = -drawPos.Y; drawPos.Y = -drawPos.Y;
@@ -615,13 +617,13 @@ namespace Barotrauma
railSprite?.Draw(spriteRecorder, railSprite?.Draw(spriteRecorder,
drawPos, drawPos,
color, color,
rotation + MathHelper.PiOver2, scale, rotationRad, scale,
SpriteEffects.None, depth + (railSprite.Depth - prefab.Sprite.Depth)); SpriteEffects.None, depth + (railSprite.Depth - prefab.Sprite.Depth));
barrelSprite?.Draw(spriteRecorder, barrelSprite?.Draw(spriteRecorder,
drawPos, drawPos,
color, color,
rotation + MathHelper.PiOver2, scale, rotationRad, scale,
SpriteEffects.None, depth + (barrelSprite.Depth - prefab.Sprite.Depth)); SpriteEffects.None, depth + (barrelSprite.Depth - prefab.Sprite.Depth));
break; break;
@@ -781,7 +783,7 @@ namespace Barotrauma
previewFrame = null; previewFrame = null;
} }
spriteRecorder?.Dispose(); spriteRecorder = null; spriteRecorder?.Dispose(); spriteRecorder = null;
camera?.Dispose(); camera = null; camera = null;
isDisposed = true; isDisposed = true;
} }
} }
@@ -2222,7 +2222,7 @@ namespace Barotrauma.Networking
} }
outmsg.WriteByte((byte)MultiplayerPreferences.Instance.TeamPreference); outmsg.WriteByte((byte)MultiplayerPreferences.Instance.TeamPreference);
if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0) if (GameMain.GameSession?.GameMode is not MultiPlayerCampaign campaign || campaign.LastSaveID == 0)
{ {
outmsg.WriteUInt16((UInt16)0); outmsg.WriteUInt16((UInt16)0);
} }
@@ -181,33 +181,6 @@ namespace Barotrauma.Networking
}; };
title.Text = ToolBox.LimitString(title.Text, title.Font, (int)(title.Rect.Width * 0.85f)); title.Text = ToolBox.LimitString(title.Text, title.Font, (int)(title.Rect.Width * 0.85f));
bool isFavorite = serverListScreen.IsFavorite(this);
static LocalizedString favoriteTickBoxToolTip(bool isFavorite)
=> TextManager.Get(isFavorite ? "RemoveFromFavorites" : "AddToFavorites");
GUITickBox favoriteTickBox = new GUITickBox(new RectTransform(new Vector2(0.15f, 0.8f), title.RectTransform, Anchor.CenterRight),
"", null, "GUIServerListFavoriteTickBox")
{
UserData = this,
Selected = isFavorite,
ToolTip = favoriteTickBoxToolTip(isFavorite),
OnSelected = tickbox =>
{
ServerInfo info = (ServerInfo)tickbox.UserData;
if (tickbox.Selected)
{
GameMain.ServerListScreen.AddToFavoriteServers(info);
}
else
{
GameMain.ServerListScreen.RemoveFromFavoriteServers(info);
}
tickbox.ToolTip = favoriteTickBoxToolTip(tickbox.Selected);
return true;
}
};
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform), new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform),
TextManager.AddPunctuation(':', TextManager.Get("ServerListVersion"), TextManager.AddPunctuation(':', TextManager.Get("ServerListVersion"),
GameVersion == new Version(0, 0, 0, 0) ? TextManager.Get("Unknown") : GameVersion.ToString())) GameVersion == new Version(0, 0, 0, 0) ? TextManager.Get("Unknown") : GameVersion.ToString()))
@@ -263,6 +236,59 @@ namespace Barotrauma.Networking
{ {
Stretch = true Stretch = true
}; };
var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 0.25f), playStyleBanner.RectTransform, Anchor.BottomRight),
isHorizontal: true, childAnchor: Anchor.BottomRight);
//shadow behind the buttons
new GUIFrame(new RectTransform(new Vector2(3.15f, 1.05f), buttonContainer.RectTransform, Anchor.BottomRight, scaleBasis: ScaleBasis.Smallest), style: null)
{
Color = Color.Black * 0.7f,
IgnoreLayoutGroups = true
};
bool isFavorite = serverListScreen.IsFavorite(this);
static LocalizedString favoriteTickBoxToolTip(bool isFavorite)
=> TextManager.Get(isFavorite ? "RemoveFromFavorites" : "AddToFavorites");
GUITickBox favoriteTickBox = new GUITickBox(new RectTransform(Vector2.One, buttonContainer.RectTransform, scaleBasis: ScaleBasis.Smallest),
"", null, "GUIServerListFavoriteTickBox")
{
UserData = this,
Selected = isFavorite,
ToolTip = favoriteTickBoxToolTip(isFavorite),
OnSelected = tickbox =>
{
ServerInfo info = (ServerInfo)tickbox.UserData;
if (tickbox.Selected)
{
GameMain.ServerListScreen.AddToFavoriteServers(info);
}
else
{
GameMain.ServerListScreen.RemoveFromFavoriteServers(info);
}
tickbox.ToolTip = favoriteTickBoxToolTip(tickbox.Selected);
return true;
}
};
new GUIButton(new RectTransform(Vector2.One, buttonContainer.RectTransform, scaleBasis: ScaleBasis.Smallest), style: "GUIServerListReportServer")
{
ToolTip = TextManager.Get("reportserver"),
OnClicked = (_, _) => {ServerListScreen.CreateReportPrompt(this); return true; }
};
new GUIButton(new RectTransform(Vector2.One, buttonContainer.RectTransform, scaleBasis: ScaleBasis.Smallest), style: "GUIServerListHideServer")
{
ToolTip = TextManager.Get("filterserver"),
OnClicked = (_, _) =>
{
ServerListScreen.CreateFilterServerPrompt(this);
return true;
}
};
// playstyle tags ----------------------------------------------------------------------------- // playstyle tags -----------------------------------------------------------------------------
var playStyleContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), content.RectTransform), isHorizontal: true) var playStyleContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), content.RectTransform), isHorizontal: true)
@@ -254,7 +254,7 @@ namespace Barotrauma
RelativeSpacing = 0.02f, RelativeSpacing = 0.02f,
}; };
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), location.Name, font: GUIStyle.LargeFont) new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), location.DisplayName, font: GUIStyle.LargeFont)
{ {
AutoScaleHorizontal = true AutoScaleHorizontal = true
}; };
@@ -598,9 +598,10 @@ namespace Barotrauma
break; break;
case CampaignMode.InteractionType.Crew: case CampaignMode.InteractionType.Crew:
CrewManagement.UpdateCrew(); CrewManagement.UpdateCrew();
CrewManagement.UpdateHireables();
break; break;
case CampaignMode.InteractionType.PurchaseSub: case CampaignMode.InteractionType.PurchaseSub:
if (submarineSelection == null) submarineSelection = new SubmarineSelection(false, () => Campaign.ShowCampaignUI = false, tabs[(int)CampaignMode.InteractionType.PurchaseSub].RectTransform); submarineSelection ??= new SubmarineSelection(false, () => Campaign.ShowCampaignUI = false, tabs[(int)CampaignMode.InteractionType.PurchaseSub].RectTransform);
submarineSelection.RefreshSubmarineDisplay(true, setTransferOptionToTrue: true); submarineSelection.RefreshSubmarineDisplay(true, setTransferOptionToTrue: true);
break; break;
case CampaignMode.InteractionType.Map: case CampaignMode.InteractionType.Map:
@@ -258,8 +258,8 @@ namespace Barotrauma
graphics.BlendState = BlendState.NonPremultiplied; graphics.BlendState = BlendState.NonPremultiplied;
graphics.SamplerStates[0] = SamplerState.LinearWrap; graphics.SamplerStates[0] = SamplerState.LinearWrap;
Quad.UseBasicEffect(renderTargetBackground); GraphicsQuad.UseBasicEffect(renderTargetBackground);
Quad.Render(); GraphicsQuad.Render();
//Draw the rest of the structures, characters and front structures //Draw the rest of the structures, characters and front structures
spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, cam.Transform); spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, cam.Transform);
@@ -312,8 +312,8 @@ namespace Barotrauma
graphics.BlendState = BlendState.Opaque; graphics.BlendState = BlendState.Opaque;
graphics.SamplerStates[0] = SamplerState.LinearWrap; graphics.SamplerStates[0] = SamplerState.LinearWrap;
Quad.UseBasicEffect(renderTarget); GraphicsQuad.UseBasicEffect(renderTarget);
Quad.Render(); GraphicsQuad.Render();
//draw alpha blended particles that are inside a sub //draw alpha blended particles that are inside a sub
spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, null, DepthStencilState.DepthRead, null, null, cam.Transform); spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, null, DepthStencilState.DepthRead, null, null, cam.Transform);
@@ -379,8 +379,8 @@ namespace Barotrauma
graphics.DepthStencilState = DepthStencilState.None; graphics.DepthStencilState = DepthStencilState.None;
graphics.SamplerStates[0] = SamplerState.LinearWrap; graphics.SamplerStates[0] = SamplerState.LinearWrap;
graphics.BlendState = CustomBlendStates.Multiplicative; graphics.BlendState = CustomBlendStates.Multiplicative;
Quad.UseBasicEffect(GameMain.LightManager.LightMap); GraphicsQuad.UseBasicEffect(GameMain.LightManager.LightMap);
Quad.Render(); GraphicsQuad.Render();
} }
spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.LinearWrap, DepthStencilState.None, null, null, cam.Transform); spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.LinearWrap, DepthStencilState.None, null, null, cam.Transform);
@@ -389,6 +389,8 @@ namespace Barotrauma
c.DrawFront(spriteBatch, cam); c.DrawFront(spriteBatch, cam);
} }
GameMain.LightManager.DebugDrawVertices(spriteBatch);
Level.Loaded?.DrawDebugOverlay(spriteBatch, cam); Level.Loaded?.DrawDebugOverlay(spriteBatch, cam);
if (GameMain.DebugDraw) if (GameMain.DebugDraw)
{ {
@@ -437,7 +439,7 @@ namespace Barotrauma
graphics.SamplerStates[0] = SamplerState.PointClamp; graphics.SamplerStates[0] = SamplerState.PointClamp;
graphics.SamplerStates[1] = SamplerState.PointClamp; graphics.SamplerStates[1] = SamplerState.PointClamp;
GameMain.LightManager.LosEffect.CurrentTechnique.Passes[0].Apply(); GameMain.LightManager.LosEffect.CurrentTechnique.Passes[0].Apply();
Quad.Render(); GraphicsQuad.Render();
graphics.SamplerStates[0] = SamplerState.LinearWrap; graphics.SamplerStates[0] = SamplerState.LinearWrap;
graphics.SamplerStates[1] = SamplerState.LinearWrap; graphics.SamplerStates[1] = SamplerState.LinearWrap;
} }
@@ -505,7 +507,7 @@ namespace Barotrauma
graphics.DepthStencilState = DepthStencilState.None; graphics.DepthStencilState = DepthStencilState.None;
if (string.IsNullOrEmpty(postProcessTechnique)) if (string.IsNullOrEmpty(postProcessTechnique))
{ {
Quad.UseBasicEffect(renderTargetFinal); GraphicsQuad.UseBasicEffect(renderTargetFinal);
} }
else else
{ {
@@ -514,7 +516,7 @@ namespace Barotrauma
PostProcessEffect.CurrentTechnique = PostProcessEffect.Techniques[postProcessTechnique]; PostProcessEffect.CurrentTechnique = PostProcessEffect.Techniques[postProcessTechnique];
PostProcessEffect.CurrentTechnique.Passes[0].Apply(); PostProcessEffect.CurrentTechnique.Passes[0].Apply();
} }
Quad.Render(); GraphicsQuad.Render();
if (fadeToBlackState > 0.0f) if (fadeToBlackState > 0.0f)
{ {
@@ -221,7 +221,17 @@ namespace Barotrauma
currentLevelData.AllowInvalidOutpost = allowInvalidOutpost.Selected; currentLevelData.AllowInvalidOutpost = allowInvalidOutpost.Selected;
var dummyLocations = GameSession.CreateDummyLocations(currentLevelData); var dummyLocations = GameSession.CreateDummyLocations(currentLevelData);
Level.Generate(currentLevelData, mirror: mirrorLevel.Selected, startLocation: dummyLocations[0], endLocation: dummyLocations[1]); Level.Generate(currentLevelData, mirror: mirrorLevel.Selected, startLocation: dummyLocations[0], endLocation: dummyLocations[1]);
Submarine.MainSub?.SetPosition(Level.Loaded.StartPosition);
if (Submarine.MainSub != null)
{
Vector2 startPos = Level.Loaded.StartPosition;
if (Level.Loaded.StartOutpost != null)
{
startPos.Y -= Level.Loaded.StartOutpost.Borders.Height / 2 + Submarine.MainSub.Borders.Height / 2;
}
Submarine.MainSub?.SetPosition(startPos);
}
GameMain.LightManager.AddLight(pointerLightSource); GameMain.LightManager.AddLight(pointerLightSource);
if (!wasLevelLoaded || Cam.Position.X < 0 || Cam.Position.Y < 0 || Cam.Position.Y > Level.Loaded.Size.X || Cam.Position.Y > Level.Loaded.Size.Y) if (!wasLevelLoaded || Cam.Position.X < 0 || Cam.Position.Y < 0 || Cam.Position.Y > Level.Loaded.Size.X || Cam.Position.Y > Level.Loaded.Size.Y)
{ {
@@ -165,6 +165,7 @@ namespace Barotrauma
} }
} }
#else #else
SpamServerFilters.RequestGlobalSpamFilter();
FetchRemoteContent(); FetchRemoteContent();
#endif #endif
@@ -1520,6 +1520,7 @@ namespace Barotrauma
}; };
bool nameChangePending = isGameRunning && GameMain.Client.PendingName != string.Empty && GameMain.Client?.Character?.Name != GameMain.Client.PendingName; bool nameChangePending = isGameRunning && GameMain.Client.PendingName != string.Empty && GameMain.Client?.Character?.Name != GameMain.Client.PendingName;
changesPendingText?.Parent?.RemoveChild(changesPendingText);
changesPendingText = null; changesPendingText = null;
if (TabMenu.PendingChanges) if (TabMenu.PendingChanges)
@@ -655,6 +655,7 @@ namespace Barotrauma
ScrollBarVisible = true, ScrollBarVisible = true,
OnSelected = (btn, obj) => OnSelected = (btn, obj) =>
{ {
if (GUI.MouseOn is GUIButton) { return false; }
if (obj is not ServerInfo serverInfo) { return false; } if (obj is not ServerInfo serverInfo) { return false; }
joinButton.Enabled = true; joinButton.Enabled = true;
@@ -852,6 +853,13 @@ namespace Barotrauma
}); });
} }
public void HideServerPreview()
{
serverPreviewContainer.Visible = false;
panelAnimator.RightEnabled = false;
panelAnimator.RightVisible = false;
}
private void InsertServer(ServerInfo serverInfo, GUIComponent component) private void InsertServer(ServerInfo serverInfo, GUIComponent component)
{ {
var children = serverList.Content.RectTransform.Children.Reverse().ToList(); var children = serverList.Content.RectTransform.Children.Reverse().ToList();
@@ -973,7 +981,7 @@ namespace Barotrauma
} }
} }
private void FilterServers() public void FilterServers()
{ {
RemoveMsgFromServerList(MsgUserData.NoMatchingServers); RemoveMsgFromServerList(MsgUserData.NoMatchingServers);
foreach (GUIComponent child in serverList.Content.Children) foreach (GUIComponent child in serverList.Content.Children)
@@ -1013,6 +1021,7 @@ namespace Barotrauma
return false; return false;
} }
#endif #endif
if (SpamServerFilters.IsFiltered(serverInfo)) { return false; }
if (!string.IsNullOrEmpty(searchBox.Text) && !serverInfo.ServerName.Contains(searchBox.Text, StringComparison.OrdinalIgnoreCase)) { return false; } if (!string.IsNullOrEmpty(searchBox.Text) && !serverInfo.ServerName.Contains(searchBox.Text, StringComparison.OrdinalIgnoreCase)) { return false; }
@@ -1553,15 +1562,169 @@ namespace Barotrauma
var serverFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.06f), serverList.Content.RectTransform) { MinSize = new Point(0, 35) }, var serverFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.06f), serverList.Content.RectTransform) { MinSize = new Point(0, 35) },
style: "ListBoxElement") style: "ListBoxElement")
{ {
UserData = serverInfo UserData = serverInfo,
}; };
serverFrame.OnSecondaryClicked += (_, data) =>
{
if (data is not ServerInfo info) { return false; }
CreateContextMenu(info);
return true;
};
new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), serverFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), serverFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft)
{ {
Stretch = false Stretch = false
}; };
UpdateServerInfoUI(serverInfo); UpdateServerInfoUI(serverInfo);
if (!skipPing) { PingUtils.GetServerPing(serverInfo, UpdateServerInfoUI); } if (!skipPing) { PingUtils.GetServerPing(serverInfo, UpdateServerInfoUI); }
}
private static readonly Vector2 confirmPopupSize = new Vector2(0.2f, 0.2625f);
private static readonly Point confirmPopupMinSize = new Point(300, 300);
private void CreateContextMenu(ServerInfo info)
{
var favoriteOption = new ContextMenuOption(IsFavorite(info) ? "removefromfavorites" : "addtofavorites", isEnabled: true, () =>
{
if (IsFavorite(info))
{
RemoveFromFavoriteServers(info);
}
else
{
AddToFavoriteServers(info);
}
FilterServers();
});
var reportOption = new ContextMenuOption("reportserver", isEnabled: true, () => { CreateReportPrompt(info); });
var filterOption = new ContextMenuOption("filterserver", isEnabled: true, () =>
{
CreateFilterServerPrompt(info);
})
{
Tooltip = TextManager.Get("filterservertooltip")
};
GUIContextMenu.CreateContextMenu(favoriteOption, filterOption, reportOption);
}
public static void CreateFilterServerPrompt(ServerInfo info)
{
GUI.AskForConfirmation(
header: TextManager.Get("filterserver"),
body: TextManager.GetWithVariables("filterserverconfirm", ("[server]", info.ServerName), ("[filepath]", SpamServerFilter.SavePath)),
onConfirm: () =>
{
SpamServerFilters.AddServerToLocalSpamList(info);
if (GameMain.ServerListScreen is not { } serverListScreen) { return; }
if (serverListScreen.selectedServer.TryUnwrap(out var selectedServer) && selectedServer.Equals(info))
{
serverListScreen.HideServerPreview();
}
serverListScreen.FilterServers();
}, relativeSize: confirmPopupSize, minSize: confirmPopupMinSize);
}
private enum ReportReason
{
Spam,
Advertising,
Inappropriate
}
public static void CreateReportPrompt(ServerInfo info)
{
if (!GameAnalyticsManager.SendUserStatistics)
{
GUI.NotifyPrompt(TextManager.Get("reportserver"), TextManager.Get("reportserverdisabled"));
return;
}
var msgBox = new GUIMessageBox(
headerText: TextManager.Get("reportserver"),
text: string.Empty,
relativeSize: new Vector2(0.2f, 0.4f),
minSize: new Point(380, 430),
buttons: Array.Empty<LocalizedString>());
var layout = new GUILayoutGroup(new RectTransform(Vector2.One, msgBox.Content.RectTransform, Anchor.Center));
new GUITextBlock(new RectTransform(new Vector2(1f, 0.3f), layout.RectTransform), TextManager.GetWithVariable("reportserverexplanation", "[server]", info.ServerName), wrap: true)
{
ToolTip = TextManager.Get("reportserverprompttooltip")
};
var listBox = new GUIListBox(new RectTransform(new Vector2(1f, 0.3f), layout.RectTransform));
var enums = Enum.GetValues<ReportReason>();
foreach (ReportReason reason in enums)
{
new GUITickBox(new RectTransform(new Vector2(1f, 1f / enums.Length), listBox.Content.RectTransform), TextManager.Get($"reportreason.{reason}"))
{
UserData = reason
};
}
// padding
new GUIFrame(new RectTransform(new Vector2(1f, 0.05f), layout.RectTransform), style: null);
var buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.3f), layout.RectTransform))
{
Stretch = true
};
var reportAndHideButton = new GUIButton(new RectTransform(new Vector2(1f, 0.333f), buttonLayout.RectTransform), TextManager.Get("reportoption.reportandhide"))
{
Enabled = false,
OnClicked = (_, _) =>
{
CreateFilterServerPrompt(info);
msgBox.Close();
return true;
}
};
var reportButton = new GUIButton(new RectTransform(new Vector2(1f, 0.333f), buttonLayout.RectTransform), TextManager.Get("reportoption.report"))
{
Enabled = false,
OnClicked = (_, _) =>
{
ReportServer(info, GetUserSelectedReasons());
msgBox.Close();
return true;
}
};
new GUIButton(new RectTransform(new Vector2(1f, 0.333f), buttonLayout.RectTransform), TextManager.Get("cancel"))
{
OnClicked = (_, _) =>
{
msgBox.Close();
return true;
}
};
foreach (var child in listBox.Content.GetAllChildren<GUITickBox>())
{
child.OnSelected += _ =>
{
reportAndHideButton.Enabled = reportButton.Enabled = GetUserSelectedReasons().Any();
return true;
};
}
IEnumerable<ReportReason> GetUserSelectedReasons()
=> listBox.Content.Children
.Where(static c => c.UserData is ReportReason && c.Selected)
.Select(static c => (ReportReason)c.UserData).ToArray();
}
private static void ReportServer(ServerInfo info, IEnumerable<ReportReason> reasons)
{
if (!reasons.Any()) { return; }
GameAnalyticsManager.AddErrorEvent(GameAnalyticsManager.ErrorSeverity.Info, $"[Spam] Reported server: Name: \"{info.ServerName}\", Message: \"{info.ServerMessage}\", Endpoint: \"{info.Endpoint.StringRepresentation}\". Reason: \"{string.Join(", ", reasons)}\".");
} }
private void UpdateServerInfoUI(ServerInfo serverInfo) private void UpdateServerInfoUI(ServerInfo serverInfo)
@@ -1571,7 +1734,6 @@ namespace Barotrauma
serverFrame.UserData = serverInfo; serverFrame.UserData = serverInfo;
serverFrame.ToolTip = "";
var serverContent = serverFrame.Children.First() as GUILayoutGroup; var serverContent = serverFrame.Children.First() as GUILayoutGroup;
serverContent.ClearChildren(); serverContent.ClearChildren();
@@ -1584,14 +1746,13 @@ namespace Barotrauma
style: null); style: null);
} }
void errorTooltip(RichString toolTip) void disableElementFocus()
{ {
sections.Values.ForEach(c => sections.Values.ForEach(c =>
{ {
c.CanBeFocused = false; c.CanBeFocused = false;
c.Children.First().CanBeFocused = false; c.Children.First().CanBeFocused = false;
}); });
serverFrame.ToolTip = toolTip;
} }
RectTransform columnRT(ColumnLabel label, float scale = 0.95f) RectTransform columnRT(ColumnLabel label, float scale = 0.95f)
@@ -1664,9 +1825,10 @@ namespace Barotrauma
serverPingText.TextColor = Color.DarkRed; serverPingText.TextColor = Color.DarkRed;
} }
LocalizedString toolTip = "";
if (!serverInfo.Checked) if (!serverInfo.Checked)
{ {
errorTooltip(TextManager.Get("ServerOffline")); toolTip = TextManager.Get("ServerOffline");
serverName.TextColor *= 0.8f; serverName.TextColor *= 0.8f;
serverPlayers.TextColor *= 0.8f; serverPlayers.TextColor *= 0.8f;
} }
@@ -1681,7 +1843,6 @@ namespace Barotrauma
} }
else if (!compatibleBox.Selected) else if (!compatibleBox.Selected)
{ {
LocalizedString toolTip = "";
if (serverInfo.GameVersion != GameMain.Version) if (serverInfo.GameVersion != GameMain.Version)
{ {
toolTip = TextManager.GetWithVariable("ServerListIncompatibleVersion", "[version]", serverInfo.GameVersion.ToString()); toolTip = TextManager.GetWithVariable("ServerListIncompatibleVersion", "[version]", serverInfo.GameVersion.ToString());
@@ -1707,14 +1868,12 @@ namespace Barotrauma
toolTip += '\n' + TextManager.GetWithVariable("workshopitemdownloadprompttruncated", "[number]", (incompatibleModNames.Count - maxIncompatibleToList).ToString()); toolTip += '\n' + TextManager.GetWithVariable("workshopitemdownloadprompttruncated", "[number]", (incompatibleModNames.Count - maxIncompatibleToList).ToString());
} }
} }
errorTooltip(toolTip);
serverName.TextColor *= 0.5f; serverName.TextColor *= 0.5f;
serverPlayers.TextColor *= 0.5f; serverPlayers.TextColor *= 0.5f;
} }
else else
{ {
LocalizedString toolTip = "";
foreach (var contentPackage in serverInfo.ContentPackages) foreach (var contentPackage in serverInfo.ContentPackages)
{ {
if (ContentPackageManager.EnabledPackages.All.None(cp => cp.Hash.StringRepresentation == contentPackage.Hash)) if (ContentPackageManager.EnabledPackages.All.None(cp => cp.Hash.StringRepresentation == contentPackage.Hash))
@@ -1724,8 +1883,11 @@ namespace Barotrauma
break; break;
} }
} }
errorTooltip(toolTip);
} }
disableElementFocus();
string separator = toolTip.IsNullOrWhiteSpace() ? "" : "\n\n";
serverFrame.ToolTip = RichString.Rich(toolTip + separator + $"‖color:gui.blue‖{TextManager.GetWithVariable("serverlisttooltip", "[button]", PlayerInput.SecondaryMouseLabel)}‖end‖");
foreach (var section in sections.Values) foreach (var section in sections.Values)
{ {
@@ -16,9 +16,6 @@ namespace Barotrauma
{ {
class SubEditorScreen : EditorScreen class SubEditorScreen : EditorScreen
{ {
public const string CircuitBoxDeletionWarningHeader = "Selection contains circuit boxes",
CircuitBoxDeletionWarningBody = "Are you sure you want to delete the selection? Any wiring inside circuit boxes will be lost and cannot be recovered.";
public const int MaxStructures = 2000; public const int MaxStructures = 2000;
public const int MaxWalls = 500; public const int MaxWalls = 500;
public const int MaxItems = 5000; public const int MaxItems = 5000;
@@ -1560,8 +1557,17 @@ namespace Barotrauma
if (editorSelectedTime.TryUnwrap(out DateTime selectedTime)) if (editorSelectedTime.TryUnwrap(out DateTime selectedTime))
{ {
TimeSpan timeInEditor = DateTime.Now - selectedTime; TimeSpan timeInEditor = DateTime.Now - selectedTime;
SteamAchievementManager.IncrementStat("hoursineditor".ToIdentifier(), (float)timeInEditor.TotalHours); if (timeInEditor.TotalSeconds > Timing.TotalTime)
editorSelectedTime = Option<DateTime>.None(); {
DebugConsole.ThrowErrorAndLogToGA(
"SubEditorScreen.DeselectEditorSpecific:InvalidTimeInEditor",
$"Error in sub editor screen. Calculated time in editor {timeInEditor} was larger than the time the game has run ({Timing.TotalTime} s).");
}
else
{
SteamAchievementManager.IncrementStat("hoursineditor".ToIdentifier(), (float)timeInEditor.TotalHours);
editorSelectedTime = Option<DateTime>.None();
}
} }
#endif #endif
@@ -3933,28 +3939,15 @@ namespace Barotrauma
new ContextMenuOption("editor.cut", isEnabled: hasTargets, onSelected: () => MapEntity.Cut(targets)), new ContextMenuOption("editor.cut", isEnabled: hasTargets, onSelected: () => MapEntity.Cut(targets)),
new ContextMenuOption("editor.copytoclipboard", isEnabled: hasTargets, onSelected: () => MapEntity.Copy(targets)), new ContextMenuOption("editor.copytoclipboard", isEnabled: hasTargets, onSelected: () => MapEntity.Copy(targets)),
new ContextMenuOption("editor.paste", isEnabled: MapEntity.CopiedList.Any(), onSelected: () => MapEntity.Paste(cam.ScreenToWorld(PlayerInput.MousePosition))), new ContextMenuOption("editor.paste", isEnabled: MapEntity.CopiedList.Any(), onSelected: () => MapEntity.Paste(cam.ScreenToWorld(PlayerInput.MousePosition))),
new ContextMenuOption("delete", isEnabled: hasTargets, onSelected: () => RemoveEntitiesWithPossibleWarning(targets)), new ContextMenuOption("delete", isEnabled: hasTargets, onSelected: () =>
new ContextMenuOption(TextManager.Get("editortip.shiftforextraoptions") + '\n' + TextManager.Get("editortip.altforruler"), isEnabled: false, onSelected: null)); {
} StoreCommand(new AddOrDeleteCommand(targets, true));
} foreach (var me in targets)
{
public static void RemoveEntitiesWithPossibleWarning(List<MapEntity> targets) if (!me.Removed) { me.Remove(); }
{ }
if (targets.Any(static t => t is Item it && it.GetComponent<CircuitBox>() is not null)) }),
{ new ContextMenuOption(TextManager.GetWithVariable("editortip.shiftforextraoptions", "[button]", PlayerInput.SecondaryMouseLabel) + '\n' + TextManager.Get("editortip.altforruler"), isEnabled: false, onSelected: null));
GUI.AskForConfirmation(CircuitBoxDeletionWarningHeader, CircuitBoxDeletionWarningBody, onConfirm: Delete);
return;
}
Delete();
void Delete()
{
StoreCommand(new AddOrDeleteCommand(targets, true));
foreach (var me in targets)
{
if (!me.Removed) { me.Remove(); }
}
} }
} }
@@ -5485,9 +5478,11 @@ namespace Barotrauma
{ {
foreach (LightComponent lightComponent in item.GetComponents<LightComponent>()) foreach (LightComponent lightComponent in item.GetComponents<LightComponent>())
{ {
lightComponent.Light.Color = item.Container != null || (item.body != null && !item.body.Enabled) ? lightComponent.Light.Color =
Color.Transparent : item.body == null || item.body.Enabled ||
lightComponent.LightColor; (item.ParentInventory is ItemInventory itemInventory && !itemInventory.Container.HideItems) ?
lightComponent.LightColor :
Color.Transparent;
lightComponent.Light.LightSpriteEffect = lightComponent.Item.SpriteEffects; lightComponent.Light.LightSpriteEffect = lightComponent.Item.SpriteEffects;
} }
} }
@@ -727,7 +727,21 @@ namespace Barotrauma
Slider(layout, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.InventoryScale, v => unsavedConfig.Graphics.InventoryScale = v); Slider(layout, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.InventoryScale, v => unsavedConfig.Graphics.InventoryScale = v);
Label(layout, TextManager.Get("TextScale"), GUIStyle.SubHeadingFont); Label(layout, TextManager.Get("TextScale"), GUIStyle.SubHeadingFont);
Slider(layout, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.TextScale, v => unsavedConfig.Graphics.TextScale = v); Slider(layout, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.TextScale, v => unsavedConfig.Graphics.TextScale = v);
Spacer(layout);
var resetSpamListFilter =
new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), layout.RectTransform),
TextManager.Get("clearserverlistfilters"), style: "GUIButtonSmall")
{
OnClicked = static (_, _) =>
{
GUI.AskForConfirmation(
header: TextManager.Get("clearserverlistfilters"),
body: TextManager.Get("clearserverlistfiltersconfirmation"),
onConfirm: SpamServerFilters.ClearLocalSpamFilter);
return true;
}
};
Spacer(layout);
#if !OSX #if !OSX
Spacer(layout); Spacer(layout);
var statisticsTickBox = new GUITickBox(NewItemRectT(layout), TextManager.Get("statisticsconsenttickbox")) var statisticsTickBox = new GUITickBox(NewItemRectT(layout), TextManager.Get("statisticsconsenttickbox"))
@@ -1,15 +1,16 @@
using System; using NVorbis;
using OpenAL; using OpenAL;
using NVorbis; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Xml.Linq;
namespace Barotrauma.Sounds namespace Barotrauma.Sounds
{ {
sealed class OggSound : Sound sealed class OggSound : Sound
{ {
private VorbisReader streamReader; private readonly VorbisReader streamReader;
public long MaxStreamSamplePos => streamReader == null ? 0 : streamReader.TotalSamples * streamReader.Channels * 2;
private List<float> playbackAmplitude; private List<float> playbackAmplitude;
private const int AMPLITUDE_SAMPLE_COUNT = 4410; //100ms in a 44100hz file private const int AMPLITUDE_SAMPLE_COUNT = 4410; //100ms in a 44100hz file
@@ -101,7 +102,7 @@ namespace Barotrauma.Sounds
if (!Stream) { throw new Exception("Called FillStreamBuffer on a non-streamed sound!"); } if (!Stream) { throw new Exception("Called FillStreamBuffer on a non-streamed sound!"); }
if (streamReader == null) { throw new Exception("Called FillStreamBuffer when the reader is null!"); } if (streamReader == null) { throw new Exception("Called FillStreamBuffer when the reader is null!"); }
if (samplePos >= streamReader.TotalSamples * streamReader.Channels * 2) return 0; if (samplePos >= MaxStreamSamplePos) { return 0; }
samplePos /= streamReader.Channels * 2; samplePos /= streamReader.Channels * 2;
streamReader.DecodedPosition = samplePos; streamReader.DecodedPosition = samplePos;
@@ -444,6 +444,18 @@ namespace Barotrauma.Sounds
} }
} }
public long MaxStreamSeekPos
{
get
{
if (!IsStream || Sound is not OggSound oggSound)
{
return 0;
}
return oggSound.MaxStreamSamplePos;
}
}
private readonly object mutex; private readonly object mutex;
public bool IsPlaying public bool IsPlaying
@@ -564,7 +576,7 @@ namespace Barotrauma.Sounds
throw new Exception("Generated streamBuffer[" + i.ToString() + "] is invalid! " + debugName); throw new Exception("Generated streamBuffer[" + i.ToString() + "] is invalid! " + debugName);
} }
} }
Sound.Owner.InitStreamThread(); Sound.Owner.InitUpdateChannelThread();
SetProperties(); SetProperties();
} }
} }
@@ -609,6 +621,7 @@ namespace Barotrauma.Sounds
public void FadeOutAndDispose() public void FadeOutAndDispose()
{ {
FadingOutAndDisposing = true; FadingOutAndDisposing = true;
Sound.Owner.InitUpdateChannelThread();
} }
public void Dispose() public void Dispose()
@@ -39,7 +39,7 @@ namespace Barotrauma.Sounds
public bool Disconnected { get; private set; } public bool Disconnected { get; private set; }
private Thread streamingThread; private Thread updateChannelsThread;
private Vector3 listenerPosition; private Vector3 listenerPosition;
public Vector3 ListenerPosition public Vector3 ListenerPosition
@@ -201,7 +201,7 @@ namespace Barotrauma.Sounds
public SoundManager() public SoundManager()
{ {
loadedSounds = new List<Sound>(); loadedSounds = new List<Sound>();
streamingThread = null; updateChannelsThread = null;
sourcePools = new SoundSourcePool[2]; sourcePools = new SoundSourcePool[2];
playingChannels[(int)SourcePoolIndex.Default] = new SoundChannel[SOURCE_COUNT]; playingChannels[(int)SourcePoolIndex.Default] = new SoundChannel[SOURCE_COUNT];
@@ -696,7 +696,7 @@ namespace Barotrauma.Sounds
CompressionDynamicRangeGain = 1.0f; CompressionDynamicRangeGain = 1.0f;
} }
if (streamingThread == null || streamingThread.ThreadState.HasFlag(ThreadState.Stopped)) if (updateChannelsThread == null || updateChannelsThread.ThreadState.HasFlag(ThreadState.Stopped))
{ {
bool startedStreamThread = false; bool startedStreamThread = false;
for (int i = 0; i < playingChannels.Length; i++) for (int i = 0; i < playingChannels.Length; i++)
@@ -708,7 +708,7 @@ namespace Barotrauma.Sounds
if (playingChannels[i][j] == null) { continue; } if (playingChannels[i][j] == null) { continue; }
if (playingChannels[i][j].IsStream && playingChannels[i][j].IsPlaying) if (playingChannels[i][j].IsStream && playingChannels[i][j].IsPlaying)
{ {
InitStreamThread(); InitUpdateChannelThread();
startedStreamThread = true; startedStreamThread = true;
} }
if (startedStreamThread) { break; } if (startedStreamThread) { break; }
@@ -728,36 +728,42 @@ namespace Barotrauma.Sounds
SetCategoryGainMultiplier("voip", Math.Min(GameSettings.CurrentConfig.Audio.VoiceChatVolume, 1.0f), 0); SetCategoryGainMultiplier("voip", Math.Min(GameSettings.CurrentConfig.Audio.VoiceChatVolume, 1.0f), 0);
} }
public void InitStreamThread() /// <summary>
/// Initializes the thread that handles streaming audio and fading out and disposing channels that are no longer needed.
/// </summary>
public void InitUpdateChannelThread()
{ {
if (Disabled) { return; } if (Disabled) { return; }
bool isStreamThreadDying; bool isUpdateChannelsThreadDying;
lock (threadDeathMutex) lock (threadDeathMutex)
{ {
isStreamThreadDying = !areStreamsPlaying; isUpdateChannelsThreadDying = !needsUpdateChannels;
} }
if (streamingThread == null || streamingThread.ThreadState.HasFlag(ThreadState.Stopped) || isStreamThreadDying) if (updateChannelsThread == null || updateChannelsThread.ThreadState.HasFlag(ThreadState.Stopped) || isUpdateChannelsThreadDying)
{ {
if (streamingThread != null && !streamingThread.Join(1000)) if (updateChannelsThread != null && !updateChannelsThread.Join(1000))
{ {
DebugConsole.ThrowError("Sound stream thread join timed out!"); DebugConsole.ThrowError("SoundManager.UpdateChannels thread join timed out!");
} }
areStreamsPlaying = true; needsUpdateChannels = true;
streamingThread = new Thread(UpdateStreaming) updateChannelsThread = new Thread(UpdateChannels)
{ {
Name = "SoundManager Streaming Thread", Name = "SoundManager.UpdateChannels Thread",
IsBackground = true //this should kill the thread if the game crashes IsBackground = true //this should kill the thread if the game crashes
}; };
streamingThread.Start(); updateChannelsThread.Start();
} }
} }
bool areStreamsPlaying = false; private bool needsUpdateChannels = false;
ManualResetEvent streamMre = null; private ManualResetEvent updateChannelsMre = null;
void UpdateStreaming() /// <summary>
/// Handles streaming audio and fading out and disposing channels that are no longer needed.
/// </summary>
private void UpdateChannels()
{ {
streamMre = new ManualResetEvent(false); updateChannelsMre = new ManualResetEvent(false);
bool killThread = false; bool killThread = false;
while (!killThread) while (!killThread)
{ {
@@ -784,6 +790,7 @@ namespace Barotrauma.Sounds
} }
else if (playingChannels[i][j].FadingOutAndDisposing) else if (playingChannels[i][j].FadingOutAndDisposing)
{ {
killThread = false;
playingChannels[i][j].Gain -= 0.1f; playingChannels[i][j].Gain -= 0.1f;
if (playingChannels[i][j].Gain <= 0.0f) if (playingChannels[i][j].Gain <= 0.0f)
{ {
@@ -794,18 +801,18 @@ namespace Barotrauma.Sounds
} }
} }
} }
streamMre.WaitOne(10); updateChannelsMre.WaitOne(10);
streamMre.Reset(); updateChannelsMre.Reset();
lock (threadDeathMutex) lock (threadDeathMutex)
{ {
areStreamsPlaying = !killThread; needsUpdateChannels = !killThread;
} }
} }
} }
public void ForceStreamUpdate() public void ForceStreamUpdate()
{ {
streamMre?.Set(); updateChannelsMre?.Set();
} }
private void ReloadSounds() private void ReloadSounds()
@@ -824,12 +831,12 @@ namespace Barotrauma.Sounds
{ {
for (int j = 0; j < playingChannels[i].Length; j++) for (int j = 0; j < playingChannels[i].Length; j++)
{ {
if (playingChannels[i][j] != null) playingChannels[i][j].Dispose(); playingChannels[i][j]?.Dispose();
} }
} }
} }
streamingThread?.Join(); updateChannelsThread?.Join();
for (int i = loadedSounds.Count - 1; i >= 0; i--) for (int i = loadedSounds.Count - 1; i >= 0; i--)
{ {
if (keepSounds) if (keepSounds)
@@ -709,6 +709,11 @@ namespace Barotrauma
{ {
musicChannel[i].StreamSeekPos = targetMusic[i].PreviousTime; musicChannel[i].StreamSeekPos = targetMusic[i].PreviousTime;
} }
else if (targetMusic[i].StartFromRandomTime)
{
musicChannel[i].StreamSeekPos =
(int)(musicChannel[i].MaxStreamSeekPos * Rand.Range(0.0f, 1.0f, Rand.RandSync.Unsynced));
}
musicChannel[i].Looping = true; musicChannel[i].Looping = true;
} }
} }
@@ -241,6 +241,7 @@ namespace Barotrauma
public readonly bool MuteIntensityTracks; public readonly bool MuteIntensityTracks;
public readonly float? ForceIntensityTrack; public readonly float? ForceIntensityTrack;
public readonly bool StartFromRandomTime;
public readonly bool ContinueFromPreviousTime; public readonly bool ContinueFromPreviousTime;
public int PreviousTime; public int PreviousTime;
@@ -255,6 +256,7 @@ namespace Barotrauma
ForceIntensityTrack = element.GetAttributeFloat(nameof(ForceIntensityTrack), 0.0f); ForceIntensityTrack = element.GetAttributeFloat(nameof(ForceIntensityTrack), 0.0f);
} }
Volume = element.GetAttributeFloat(nameof(Volume), 1.0f); Volume = element.GetAttributeFloat(nameof(Volume), 1.0f);
StartFromRandomTime = element.GetAttributeBool(nameof(StartFromRandomTime), false);
ContinueFromPreviousTime = element.GetAttributeBool(nameof(ContinueFromPreviousTime), false); ContinueFromPreviousTime = element.GetAttributeBool(nameof(ContinueFromPreviousTime), false);
} }
} }
@@ -0,0 +1,330 @@
#nullable enable
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Net;
using System.Net.Cache;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using Barotrauma.IO;
using Barotrauma.Networking;
using RestSharp;
using XmlWriter = Barotrauma.IO.XmlWriter;
namespace Barotrauma
{
public enum SpamServerFilterType
{
Invalid,
NameEquals,
NameContains,
MessageEquals,
MessageContains,
PlayerCountLarger,
PlayerCountExact,
MaxPlayersLarger,
MaxPlayersExact,
GameModeEquals,
PlayStyleEquals,
Endpoint,
LanguageEquals
}
internal readonly record struct SpamFilter(ImmutableHashSet<(SpamServerFilterType Type, string Value)> Filters)
{
public bool IsFiltered(ServerInfo info)
{
if (!Filters.Any()) { return false; }
foreach (var (type, value) in Filters)
{
if (!IsFiltered(info, type, value)) { return false; }
}
return true;
}
private static bool IsFiltered(ServerInfo info, SpamServerFilterType type, string value)
{
string desc = info.ServerMessage,
name = info.ServerName;
int.TryParse(value, out int parsedInt);
return type switch
{
SpamServerFilterType.NameEquals => CompareEquals(name, value),
SpamServerFilterType.NameContains => CompareContains(name, value),
SpamServerFilterType.MessageEquals => CompareEquals(desc, value),
SpamServerFilterType.MessageContains => CompareContains(desc, value),
SpamServerFilterType.Endpoint => info.Endpoint.StringRepresentation.Equals(value, StringComparison.OrdinalIgnoreCase),
SpamServerFilterType.PlayerCountLarger => info.PlayerCount > parsedInt,
SpamServerFilterType.PlayerCountExact => info.PlayerCount == parsedInt,
SpamServerFilterType.MaxPlayersLarger => info.MaxPlayers > parsedInt,
SpamServerFilterType.MaxPlayersExact => info.MaxPlayers == parsedInt,
SpamServerFilterType.GameModeEquals => info.GameMode == value,
SpamServerFilterType.PlayStyleEquals => info.PlayStyle.ToIdentifier() == value,
SpamServerFilterType.LanguageEquals => info.Language.Value == value,
_ => false
};
static bool CompareEquals(string a, string b)
=> a.Equals(b, StringComparison.OrdinalIgnoreCase) || Homoglyphs.Compare(a, b);
static bool CompareContains(string a, string b)
=> a.Contains(b, StringComparison.OrdinalIgnoreCase);
}
public XElement Serialize()
{
var element = new XElement("Filter");
foreach (var (type, value) in Filters)
{
element.Add(new XAttribute(type.ToString().ToLowerInvariant(), value));
}
return element;
}
public static bool TryParse(XElement element, out SpamFilter filter)
{
var builder = ImmutableHashSet.CreateBuilder<(SpamServerFilterType Type, string Value)>();
foreach (var attribute in element.Attributes())
{
if (!Enum.TryParse(attribute.Name.ToString(), ignoreCase: true, out SpamServerFilterType e))
{
DebugConsole.ThrowError($"Failed to parse spam filter attribute \"{attribute.Name}\"");
continue;
}
if (e is SpamServerFilterType.Invalid) { continue; }
builder.Add((e, attribute.Value));
}
if (builder.Any())
{
filter = new SpamFilter(builder.ToImmutable());
return true;
}
filter = default;
return false;
}
public override string ToString()
{
return !Filters.Any() ? "Invalid Filter" : string.Join(", ", Filters.Select(static f => $"{f.Type}: {f.Value}"));
}
}
internal sealed class SpamServerFilter
{
public readonly ImmutableArray<SpamFilter> Filters;
public bool IsFiltered(ServerInfo info)
{
foreach (var f in Filters)
{
if (f.IsFiltered(info)) { return true; }
}
return false;
}
public SpamServerFilter(XElement element)
{
var builder = ImmutableArray.CreateBuilder<SpamFilter>();
foreach (var subElement in element.Elements())
{
if (SpamFilter.TryParse(subElement, out var filter))
{
builder.Add(filter);
}
}
Filters = builder.ToImmutable();
}
public SpamServerFilter(ImmutableArray<SpamFilter> filters)
=> Filters = filters;
public readonly static string SavePath = Path.Combine("Data", "serverblacklist.xml");
public void Save(string path)
{
var comment = new XComment(SpamServerFilters.LocalFilterComment);
var doc = new XDocument(comment, new XElement("Filters"));
foreach (var filter in Filters)
{
doc.Root?.Add(filter.Serialize());
}
try
{
using var writer = XmlWriter.Create(path, new XmlWriterSettings { Indent = true });
doc.SaveSafe(writer);
}
catch (Exception e)
{
DebugConsole.ThrowError("Saving spam filter failed.", e);
}
}
}
internal static class SpamServerFilters
{
public static Option<SpamServerFilter> LocalSpamFilter;
public static Option<SpamServerFilter> GlobalSpamFilter;
public const string LocalFilterComment = @"
This file contains a list of filters that can be used to hide servers from the server list.
You can add filters by right-clicking a server in the server list and selecting ""Hide server"" or by reporting the server and choosing ""Report and hide server"".
The filters are saved in this file, which you can edit manually if you want to.
The available filter types are:
- NameEquals: The server name must equal the specified value. Homoglyphs are also checked.
- NameContains: The server name must contain the specified value.
- MessageEquals: The server description must equal the specified value. Homoglyphs are also checked.
- MessageContains: The server description must contain the specified value.
- PlayerCountLarger: The player count must be larger than the specified value.
- PlayerCountExact: The player count must match the specified value exactly.
- MaxPlayersLarger: The max player count must be larger than the specified value.
- MaxPlayersExact: The max player count must match the specified value exactly.
- GameModeEquals: The game mode identifier must match the specified value exactly.
- PlayStyleEquals: The play style must match the specified value exactly.
- Endpoint: The server endpoint, which is a Steam ID or an IP address, must match the specified value exactly. Steam ID is in the format of STEAM_X:Y:Z.
- LanguageEquals: The server language must match the specified value exactly.
The filter values are case-insensitive and adding multiple conditions on one filter will require all of them to be met.
Homoglyph comparison is used for NameEquals and MessageEquals filters, which means that it checks whether the words look the same, meaning you can't abuse identical-looking but different symbols to work around the filter. For example ""lmaobox"" and ""lmаobox"" (with a cyrillic a) are considered equal.
Examples:
<Filters>
<Filter namecontains=""discord.gg"" />
<Filter messagecontains=""discord.gg"" />
<Filter nameequals=""get good get lmaobox"" maxplayersexact=""999"" />
</Filters>
These will hide all servers that have a discord.gg link in their name or description and servers with the name ""get good get lmaobox"" that have 999 max players.
";
static SpamServerFilters()
{
XDocument? doc;
if (!File.Exists(SpamServerFilter.SavePath))
{
var comment = new XComment(LocalFilterComment);
doc = new XDocument(comment, new XElement("Filters"));
try
{
using var writer = XmlWriter.Create(SpamServerFilter.SavePath, new XmlWriterSettings { Indent = true });
doc.SaveSafe(writer);
}
catch (Exception e)
{
DebugConsole.ThrowError("Saving spam filter failed.", e);
}
}
else
{
doc = XMLExtensions.TryLoadXml(SpamServerFilter.SavePath);
}
if (doc?.Root is { } root)
{
LocalSpamFilter = Option.Some(new SpamServerFilter(root));
}
}
public static bool IsFiltered(ServerInfo info)
{
if (LocalSpamFilter.TryUnwrap(out var localFilter) && localFilter.IsFiltered(info)) { return true; }
if (GlobalSpamFilter.TryUnwrap(out var globalFilter) && globalFilter.IsFiltered(info)) { return true; }
return false;
}
public static void AddServerToLocalSpamList(ServerInfo info)
{
if (!LocalSpamFilter.TryUnwrap(out var localFilter)) { return; }
if (localFilter.IsFiltered(info)) { return; }
var filters = localFilter.Filters.Add(new SpamFilter(ImmutableHashSet.Create((NameExact: SpamServerFilterType.NameEquals, info.ServerName))));
var newFilter = new SpamServerFilter(filters);
newFilter.Save(SpamServerFilter.SavePath);
LocalSpamFilter = Option.Some(newFilter);
}
public static void ClearLocalSpamFilter()
{
var newFilter = new SpamServerFilter(ImmutableArray<SpamFilter>.Empty);
newFilter.Save(SpamServerFilter.SavePath);
LocalSpamFilter = Option.Some(newFilter);
}
public static void RequestGlobalSpamFilter()
{
if (GameSettings.CurrentConfig.DisableGlobalSpamList) { return; }
string remoteContentUrl = GameSettings.CurrentConfig.RemoteMainMenuContentUrl;
if (string.IsNullOrEmpty(remoteContentUrl)) { return; }
try
{
var client = new RestClient($"{remoteContentUrl}spamfilter")
{
CachePolicy = new HttpRequestCachePolicy(HttpRequestCacheLevel.NoCacheNoStore)
};
client.AddDefaultHeader("Cache-Control", "no-cache");
client.AddDefaultHeader("Pragma", "no-cache");
var request = new RestRequest("serve_spamlist.php", Method.GET);
TaskPool.Add("RequestGlobalSpamFilter", client.ExecuteAsync(request), RemoteContentReceived);
}
catch (Exception e)
{
#if DEBUG
DebugConsole.ThrowError("Fetching global spam list failed.", e);
#endif
GameAnalyticsManager.AddErrorEventOnce("SpamServerFilters.RequestGlobalSpamFilter:Exception", GameAnalyticsManager.ErrorSeverity.Error,
"Fetching global spam list failed. " + e.Message);
}
static void RemoteContentReceived(Task t)
{
try
{
if (!t.TryGetResult(out IRestResponse remoteContentResponse)) { throw new Exception("Task did not return a valid result"); }
if (remoteContentResponse.StatusCode != HttpStatusCode.OK)
{
DebugConsole.AddWarning(
"Failed to receive global spam filter." +
"There may be an issue with your internet connection, or the master server might be temporarily unavailable " +
$"(error code: {remoteContentResponse.StatusCode})");
return;
}
string data = remoteContentResponse.Content;
if (string.IsNullOrWhiteSpace(data)) { return; }
if (XDocument.Parse(data).Root is { } root)
{
GlobalSpamFilter = Option.Some(new SpamServerFilter(root));
}
}
catch (Exception e)
{
#if DEBUG
DebugConsole.ThrowError("Reading received global spam filter failed.", e);
#endif
GameAnalyticsManager.AddErrorEventOnce("SpamServerFilters.RemoteContentReceived:Exception", GameAnalyticsManager.ErrorSeverity.Error,
"Reading received global spam filter failed. " + e.Message);
}
}
}
}
}
@@ -284,13 +284,22 @@ namespace Barotrauma
} }
} }
public void DrawTiled(ISpriteBatch spriteBatch, Vector2 position, Vector2 targetSize, float rotation = 0f, Vector2? origin = null, public void DrawTiled(ISpriteBatch spriteBatch,
Color? color = null, Vector2? startOffset = null, Vector2? textureScale = null, float? depth = null) Vector2 position,
Vector2 targetSize,
float rotation = 0f,
Vector2? origin = null,
Color? color = null,
Vector2? startOffset = null,
Vector2? textureScale = null,
float? depth = null,
SpriteEffects? spriteEffects = null)
{ {
if (Texture == null) { return; } if (Texture == null) { return; }
bool flipHorizontal = (effects & SpriteEffects.FlipHorizontally) != 0; spriteEffects ??= effects;
bool flipVertical = (effects & SpriteEffects.FlipVertically) != 0; bool flipHorizontal = (spriteEffects.Value & SpriteEffects.FlipHorizontally) != 0;
bool flipVertical = (spriteEffects.Value & SpriteEffects.FlipVertically) != 0;
float addedRotation = rotation + this.rotation; float addedRotation = rotation + this.rotation;
if (flipHorizontal != flipVertical) { addedRotation = -addedRotation; } if (flipHorizontal != flipVertical) { addedRotation = -addedRotation; }
@@ -311,7 +320,7 @@ namespace Barotrauma
Vector2 transformedPos = slicePos - position; Vector2 transformedPos = slicePos - position;
transformedPos = advanceX * transformedPos.X + advanceY * transformedPos.Y; transformedPos = advanceX * transformedPos.X + advanceY * transformedPos.Y;
transformedPos += position - transformedOrigin; transformedPos += position - transformedOrigin;
spriteBatch.Draw(texture, transformedPos, sliceRect, drawColor, addedRotation, Vector2.Zero, scale, effects, depth ?? this.depth); spriteBatch.Draw(texture, transformedPos, sliceRect, drawColor, addedRotation, Vector2.Zero, scale, spriteEffects.Value, depth ?? this.depth);
} }
//wrap the drawOffset inside the sourceRect //wrap the drawOffset inside the sourceRect
@@ -210,7 +210,7 @@ namespace Barotrauma
statusEffect.soundChannel.FadeOutAndDispose(); statusEffect.soundChannel.FadeOutAndDispose();
statusEffect.soundChannel = null; statusEffect.soundChannel = null;
} }
else else if (statusEffect.soundEmitter is { Removed: false })
{ {
statusEffect.soundChannel.Position = new Vector3(statusEffect.soundEmitter.WorldPosition, 0.0f); statusEffect.soundChannel.Position = new Vector3(statusEffect.soundEmitter.WorldPosition, 0.0f);
if (doMuffleCheck && !statusEffect.ignoreMuffling) if (doMuffleCheck && !statusEffect.ignoreMuffling)
@@ -101,13 +101,36 @@ namespace Barotrauma.Steam
{ {
Color = GUIStyle.Green Color = GUIStyle.Green
}; };
var textShadow = new GUITextBlock(new RectTransform(Vector2.One, itemDownloadProgress.RectTransform) { AbsoluteOffset = new Point(GUI.IntScale(3)) }, "",
textColor: Color.Black, textAlignment: Alignment.Center);
var text = new GUITextBlock(new RectTransform(Vector2.One, itemDownloadProgress.RectTransform), "",
textAlignment: Alignment.Center);
var itemDownloadProgressUpdater = new GUICustomComponent( var itemDownloadProgressUpdater = new GUICustomComponent(
new RectTransform(Vector2.Zero, msgBox.Content.RectTransform), new RectTransform(Vector2.Zero, msgBox.Content.RectTransform),
onUpdate: (f, component) => onUpdate: (f, component) =>
{ {
float progress = 0.0f; float progress = 0.0f;
if (item.IsDownloading) { progress = item.DownloadAmount; } if (item.IsDownloading)
else if (itemDownloadProgress.BarSize > 0.0f) { progress = 1.0f; } {
progress = item.DownloadAmount;
text.Text = textShadow.Text = TextManager.GetWithVariable(
"PublishPopupDownload",
"[percentage]",
((int)MathF.Round(item.DownloadAmount * 100)).ToString());
}
else if (itemDownloadProgress.BarSize > 0.0f)
{
if (!item.IsInstalled && !SteamManager.Workshop.CanBeInstalled(item.Id))
{
itemDownloadProgress.Color = GUIStyle.Red;
text.Text = textShadow.Text = TextManager.Get("workshopiteminstallfailed");
}
else
{
text.Text = textShadow.Text = TextManager.Get(item.IsInstalled ? "workshopiteminstalled" : "PublishPopupInstall");
}
progress = 1.0f;
}
itemDownloadProgress.BarSize = Math.Max(itemDownloadProgress.BarSize, itemDownloadProgress.BarSize = Math.Max(itemDownloadProgress.BarSize,
MathHelper.Lerp(itemDownloadProgress.BarSize, progress, 0.1f)); MathHelper.Lerp(itemDownloadProgress.BarSize, progress, 0.1f));
@@ -134,9 +157,16 @@ namespace Barotrauma.Steam
{ {
foreach (var item in itemsToDownload) foreach (var item in itemsToDownload)
{ {
DebugConsole.Log($"Reinstalling {item.Title}...");
await SteamManager.Workshop.Reinstall(item); await SteamManager.Workshop.Reinstall(item);
if (!GUIMessageBox.MessageBoxes.Contains(msgBox)) { break; } DebugConsole.Log($"Finished installing {item.Title}...");
if (!GUIMessageBox.MessageBoxes.Contains(msgBox))
{
DebugConsole.Log($"Download prompt closed, interrupting {nameof(DownloadItems)}.");
break;
}
} }
DebugConsole.Log($"{nameof(DownloadItems)} finished.");
} }
} }
} }
@@ -298,7 +298,7 @@ namespace Barotrauma.Steam
public static void OnItemDownloadComplete(ulong id, bool forceInstall = false) public static void OnItemDownloadComplete(ulong id, bool forceInstall = false)
{ {
if (!(Screen.Selected is MainMenuScreen) && !forceInstall) if (Screen.Selected is not MainMenuScreen && !forceInstall)
{ {
if (!MainMenuScreen.WorkshopItemsToUpdate.Contains(id)) if (!MainMenuScreen.WorkshopItemsToUpdate.Contains(id))
{ {
@@ -306,13 +306,26 @@ namespace Barotrauma.Steam
} }
return; return;
} }
else if (CanBeInstalled(id) else if (!CanBeInstalled(id))
&& !ContentPackageManager.WorkshopPackages.Any(p => {
DebugConsole.Log($"Cannot install {id}");
InstallWaiter.StopWaiting(id);
}
else if (ContentPackageManager.WorkshopPackages.Any(p =>
p.UgcId.TryUnwrap(out var ugcId) p.UgcId.TryUnwrap(out var ugcId)
&& ugcId is SteamWorkshopId workshopId && ugcId is SteamWorkshopId workshopId
&& workshopId.Value == id) && workshopId.Value == id))
&& !InstallTaskCounter.IsInstalling(id))
{ {
DebugConsole.Log($"Already installed {id}.");
InstallWaiter.StopWaiting(id);
}
else if (InstallTaskCounter.IsInstalling(id))
{
DebugConsole.Log($"Already installing {id}.");
}
else
{
DebugConsole.Log($"Finished downloading {id}, installing...");
TaskPool.Add($"InstallItem{id}", InstallMod(id), t => InstallWaiter.StopWaiting(id)); TaskPool.Add($"InstallItem{id}", InstallMod(id), t => InstallWaiter.StopWaiting(id));
} }
} }
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Xml.Linq;
using Barotrauma.Items.Components; using Barotrauma.Items.Components;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
@@ -116,6 +117,9 @@ namespace Barotrauma
private readonly bool WasDeleted; private readonly bool WasDeleted;
private readonly List<AddOrDeleteCommand> ContainedItemsCommand = new List<AddOrDeleteCommand>(); private readonly List<AddOrDeleteCommand> ContainedItemsCommand = new List<AddOrDeleteCommand>();
// We need to 'snapshot' the state of the circuit box and the best way to do that is to save it to XML.
private readonly List<XElement> CircuitBoxData = new List<XElement>();
/// <summary> /// <summary>
/// Creates a command where all entities share the same state. /// Creates a command where all entities share the same state.
/// </summary> /// </summary>
@@ -143,13 +147,17 @@ namespace Barotrauma
List<MapEntity> itemsToDelete = new List<MapEntity>(); List<MapEntity> itemsToDelete = new List<MapEntity>();
foreach (MapEntity receiver in Receivers) foreach (MapEntity receiver in Receivers)
{ {
if (receiver is Item it) if (receiver is not Item it) { continue; }
foreach (var cb in it.GetComponents<CircuitBox>())
{ {
foreach (ItemContainer component in it.GetComponents<ItemContainer>()) CircuitBoxData.Add(cb.Save(new XElement("root")));
{ }
if (component.Inventory == null) { continue; }
itemsToDelete.AddRange(component.Inventory.AllItems.Where(item => !item.Removed)); foreach (ItemContainer component in it.GetComponents<ItemContainer>())
} {
if (component.Inventory == null) { continue; }
itemsToDelete.AddRange(component.Inventory.AllItems.Where(static item => !item.Removed));
} }
} }
@@ -192,34 +200,50 @@ namespace Barotrauma
} }
public override void Execute() public override void Execute()
{ => Process(true);
var items = DeleteUndelete(true);
ContainedItemsCommand?.ForEach(static cmd => cmd.Execute());
CircuitBoxWorkaround(items);
}
public override void UnExecute() public override void UnExecute()
=> Process(false);
private void Process(bool redo)
{ {
var items = DeleteUndelete(false); var items = DeleteUndelete(redo);
ContainedItemsCommand?.ForEach(static cmd => cmd.UnExecute()); foreach (var cmd in ContainedItemsCommand)
CircuitBoxWorkaround(items); {
cmd.Process(redo);
}
ApplyCircuitBoxDataIfAny(items);
} }
// FIXME Temporary workaround for circuit boxes throwing console errors and breaking completely when undoing a deletion /// <summary>
private static void CircuitBoxWorkaround(Option<ImmutableArray<MapEntity>> entitiesOption) /// We need to manually copy over the circuit box data because of how the undo handles inventory items.
/// The undo system recursively deletes inventory items and creates a separate command for each one.
/// This causes the circuit box to lose its internal inventory when it's cloned and then restored and make it
/// unable to load the state from XML.
///
/// The workaround to this is to ignore the XML that is being loaded when the item is created and instead
/// save the XML into the command and then load it back after the undo system has restored the items which
/// is what this function does.
/// </summary>
private void ApplyCircuitBoxDataIfAny(ImmutableArray<Item> items)
{ {
if (!entitiesOption.TryUnwrap(out var entities)) { return; } int cbIndex = 0;
foreach (var newItem in items)
foreach (var entity in entities)
{ {
if (entity is not Item it) { continue; } foreach (ItemComponent component in newItem.Components)
if (it.GetComponent<CircuitBox>() is not null)
{ {
foreach (var container in it.GetComponents<ItemContainer>()) if (component is not CircuitBox cb) { continue; }
if (cbIndex < 0 || cbIndex >= CircuitBoxData.Count)
{ {
container.Inventory.DeleteAllItems(); DebugConsole.ThrowError("Unable to restore wiring in circuit box, index out of range.");
continue;
} }
var cbData = CircuitBoxData[cbIndex];
cbIndex++;
cb.LoadFromXML(new ContentXElement(null, cbData));
} }
} }
} }
@@ -238,15 +262,19 @@ namespace Barotrauma
Receivers.Clear(); Receivers.Clear();
PreviousInventories?.Clear(); PreviousInventories?.Clear();
ContainedItemsCommand?.ForEach(static cmd => cmd.Cleanup()); ContainedItemsCommand?.ForEach(static cmd => cmd.Cleanup());
CircuitBoxData.Clear();
} }
private Option<ImmutableArray<MapEntity>> DeleteUndelete(bool redo) private ImmutableArray<Item> DeleteUndelete(bool redo)
{ {
bool wasDeleted = WasDeleted; bool wasDeleted = WasDeleted;
// We are redoing instead of undoing, flip the behavior // We are redoing instead of undoing, flip the behavior
if (redo) { wasDeleted = !wasDeleted; } if (redo) { wasDeleted = !wasDeleted; }
// collect newly created items so we can update their circuit boxes if any
var builder = ImmutableArray.CreateBuilder<Item>();
if (wasDeleted) if (wasDeleted)
{ {
Debug.Assert(Receivers.All(static entity => entity.GetReplacementOrThis().Removed), "Tried to redo a deletion but some items were not deleted"); Debug.Assert(Receivers.All(static entity => entity.GetReplacementOrThis().Removed), "Tried to redo a deletion but some items were not deleted");
@@ -260,6 +288,7 @@ namespace Barotrauma
if (receiver.GetReplacementOrThis() is Item item && clone is Item cloneItem) if (receiver.GetReplacementOrThis() is Item item && clone is Item cloneItem)
{ {
builder.Add(cloneItem);
foreach (ItemComponent ic in item.Components) foreach (ItemComponent ic in item.Components)
{ {
int index = item.GetComponentIndex(ic); int index = item.GetComponentIndex(ic);
@@ -303,7 +332,7 @@ namespace Barotrauma
clone.Submarine = Submarine.MainSub; clone.Submarine = Submarine.MainSub;
} }
return Option.Some(clones.ToImmutableArray()); return builder.ToImmutable();
} }
else else
{ {
@@ -316,7 +345,7 @@ namespace Barotrauma
} }
} }
return Option.None; return builder.ToImmutable();
} }
} }
@@ -6,7 +6,7 @@ using System.Text;
namespace Barotrauma namespace Barotrauma
{ {
static class Quad static class GraphicsQuad
{ {
private static VertexBuffer vertexBuffer = null; private static VertexBuffer vertexBuffer = null;
private static IndexBuffer indexBuffer = null; private static IndexBuffer indexBuffer = null;
@@ -35,7 +35,7 @@ namespace Barotrauma
Vector2 pos, Vector2 pos,
Rectangle srcRect, Rectangle srcRect,
Color color, Color color,
float rotation, float rotationRad,
Vector2 origin, Vector2 origin,
Vector2 scale, Vector2 scale,
SpriteEffects effects, SpriteEffects effects,
@@ -55,9 +55,8 @@ namespace Barotrauma
(srcRectBottom, srcRectTop) = (srcRectTop, srcRectBottom); (srcRectBottom, srcRectTop) = (srcRectTop, srcRectBottom);
} }
rotation = MathHelper.ToRadians(rotation); float sin = (float)Math.Sin(rotationRad);
float sin = (float)Math.Sin(rotation); float cos = (float)Math.Cos(rotationRad);
float cos = (float)Math.Cos(rotation);
var size = srcRect.Size.ToVector2() * scale; var size = srcRect.Size.ToVector2() * scale;
@@ -183,11 +182,11 @@ namespace Barotrauma
commandList.Add(command); commandList.Add(command);
} }
public void Draw(Texture2D texture, Vector2 pos, Rectangle? srcRect, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float depth) public void Draw(Texture2D texture, Vector2 pos, Rectangle? srcRect, Color color, float rotationRad, Vector2 origin, Vector2 scale, SpriteEffects effects, float depth)
{ {
if (isDisposed) { return; } if (isDisposed) { return; }
var command = Command.FromTransform(texture, pos, srcRect ?? texture.Bounds, color, rotation, origin, scale, effects, depth, commandList.Count); var command = Command.FromTransform(texture, pos, srcRect ?? texture.Bounds, color, rotationRad, origin, scale, effects, depth, commandList.Count);
AppendCommand(command); AppendCommand(command);
} }
@@ -76,7 +76,7 @@ namespace Barotrauma
float zoom = (float)texWidth / (float)boundingBox.Width; float zoom = (float)texWidth / (float)boundingBox.Width;
int texHeight = (int)(zoom * boundingBox.Height); int texHeight = (int)(zoom * boundingBox.Height);
using Camera cam = new Camera(); Camera cam = new Camera();
cam.SetResolution(new Point(texWidth, texHeight)); cam.SetResolution(new Point(texWidth, texHeight));
cam.MaxZoom = zoom; cam.MaxZoom = zoom;
cam.MinZoom = zoom * 0.5f; cam.MinZoom = zoom * 0.5f;
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>1.2.1.0</Version> <Version>1.2.4.0</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright> <Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
+1 -1
View File
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>1.2.1.0</Version> <Version>1.2.4.0</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright> <Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>1.2.1.0</Version> <Version>1.2.4.0</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright> <Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>1.2.1.0</Version> <Version>1.2.4.0</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright> <Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
+1 -1
View File
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>1.2.1.0</Version> <Version>1.2.4.0</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright> <Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
@@ -80,7 +80,9 @@ namespace Barotrauma
{ {
msg.WriteUInt32(Job.Prefab.UintIdentifier); msg.WriteUInt32(Job.Prefab.UintIdentifier);
msg.WriteByte((byte)Job.Variant); msg.WriteByte((byte)Job.Variant);
foreach (SkillPrefab skillPrefab in Job.Prefab.Skills.OrderBy(s => s.Identifier)) var skills = Job.Prefab.Skills.OrderBy(s => s.Identifier);
msg.WriteByte((byte)skills.Count());
foreach (SkillPrefab skillPrefab in skills)
{ {
msg.WriteSingle(Job.GetSkill(skillPrefab.Identifier)?.Level ?? 0.0f); msg.WriteSingle(Job.GetSkill(skillPrefab.Identifier)?.Level ?? 0.0f);
} }
@@ -1134,7 +1134,12 @@ namespace Barotrauma
createMessage("Traitors:"); createMessage("Traitors:");
foreach (var ev in traitorManager.ActiveEvents) foreach (var ev in traitorManager.ActiveEvents)
{ {
createMessage($" - {ev.Traitor.Name}: {ev.TraitorEvent.Prefab.Identifier} ({ev.TraitorEvent.CurrentState})"); string msg = $" - {ev.TraitorEvent.Prefab.Identifier} ({ev.TraitorEvent.CurrentState}): {ev.Traitor.Name}";
if (ev.TraitorEvent.SecondaryTraitors.Any())
{
msg += $" secondary traitors: {string.Join(", ", ev.TraitorEvent.SecondaryTraitors.Select(t => t.Name))}";
}
createMessage(msg);
} }
} }
@@ -1416,7 +1421,7 @@ namespace Barotrauma
if (GameMain.GameSession?.GameMode is MultiPlayerCampaign mpCampaign && if (GameMain.GameSession?.GameMode is MultiPlayerCampaign mpCampaign &&
GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign) GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign)
{ {
MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath); MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath, client: null);
} }
else else
{ {
@@ -1461,7 +1466,7 @@ namespace Barotrauma
return; return;
} }
var location = GameMain.GameSession.Campaign.Map.Locations.FirstOrDefault(l => l.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase)); var location = GameMain.GameSession.Campaign.Map.Locations.FirstOrDefault(l => l.DisplayName.Equals(args[0], StringComparison.OrdinalIgnoreCase));
if (location == null) if (location == null)
{ {
ThrowError($"Could not find a location with the name {args[0]}."); ThrowError($"Could not find a location with the name {args[0]}.");
@@ -1484,7 +1489,7 @@ namespace Barotrauma
return new string[][] return new string[][]
{ {
GameMain.GameSession.Campaign.Map.Locations.Select(l => l.Name).ToArray(), GameMain.GameSession.Campaign.Map.Locations.Select(l => l.DisplayName.Value).ToArray(),
LocationType.Prefabs.Select(lt => lt.Name.Value).ToArray() LocationType.Prefabs.Select(lt => lt.Name.Value).ToArray()
}; };
})); }));
@@ -2457,7 +2462,7 @@ namespace Barotrauma
} }
Location location = campaign.Map.CurrentLocation.Connections[destinationIndex].OtherLocation(campaign.Map.CurrentLocation); Location location = campaign.Map.CurrentLocation.Connections[destinationIndex].OtherLocation(campaign.Map.CurrentLocation);
campaign.Map.SelectLocation(location); campaign.Map.SelectLocation(location);
GameMain.Server.SendConsoleMessage(location.Name + " selected.", senderClient); GameMain.Server.SendConsoleMessage($"{location.DisplayName.Value} selected.", senderClient);
} }
); );
@@ -0,0 +1,24 @@
#nullable enable
using Barotrauma.Networking;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma;
partial class HighlightAction : EventAction
{
partial void SetHighlightProjSpecific(Entity entity, IEnumerable<Character>? targetCharacters)
{
if (entity is Item item && GameMain.Server != null)
{
IEnumerable<Client>? targetClients = null;
if (targetCharacters != null)
{
targetClients = targetCharacters
.Select(c => GameMain.Server.ConnectedClients.FirstOrDefault(client => client.Character == c))
.Where(c => c != null)!;
}
GameMain.Server?.CreateEntityEvent(item, new Item.SetHighlightEventData(State, highlightColor, targetClients));
}
}
}
@@ -97,7 +97,13 @@ namespace Barotrauma
void GiveMissionExperience(CharacterInfo info) void GiveMissionExperience(CharacterInfo info)
{ {
if (info == null) { return; } if (info == null) { return; }
var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f); var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f, info.Character);
//check if anyone else in the crew has talents that could give a bonus to this one
foreach (var c in crew)
{
if (c == info.Character) { continue; }
c.CheckTalents(AbilityEffectType.OnAllyGainMissionExperience, experienceGainMultiplierIndividual);
}
info.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual); info.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual);
int finalExperienceGain = (int)(experienceGain * experienceGainMultiplierIndividual.Value); int finalExperienceGain = (int)(experienceGain * experienceGainMultiplierIndividual.Value);
@@ -120,28 +120,41 @@ namespace Barotrauma
SaveUtil.SaveGame(GameMain.GameSession.SavePath); SaveUtil.SaveGame(GameMain.GameSession.SavePath);
DebugConsole.NewMessage("Campaign started!", Color.Cyan); DebugConsole.NewMessage("Campaign started!", Color.Cyan);
DebugConsole.NewMessage("Current location: " + GameMain.GameSession.Map.CurrentLocation.Name, Color.Cyan); DebugConsole.NewMessage("Current location: " + GameMain.GameSession.Map.CurrentLocation.DisplayName, Color.Cyan);
((MultiPlayerCampaign)GameMain.GameSession.GameMode).LoadInitialLevel(); ((MultiPlayerCampaign)GameMain.GameSession.GameMode).LoadInitialLevel();
} }
public static void LoadCampaign(string selectedSave) public static void LoadCampaign(string selectedSave, Client client)
{ {
GameMain.NetLobbyScreen.ToggleCampaignMode(true); GameMain.NetLobbyScreen.ToggleCampaignMode(true);
SaveUtil.LoadGame(selectedSave); try
if (GameMain.GameSession.GameMode is MultiPlayerCampaign mpCampaign)
{ {
mpCampaign.LastSaveID++; SaveUtil.LoadGame(selectedSave);
if (GameMain.GameSession.GameMode is MultiPlayerCampaign mpCampaign)
{
mpCampaign.LastSaveID++;
}
else
{
DebugConsole.ThrowError("Failed to load a campaign. Unexpected game mode: " + GameMain.GameSession.GameMode ?? "none");
return;
}
} }
else catch (Exception e)
{ {
DebugConsole.ThrowError("Unexpected game mode: " + GameMain.GameSession.GameMode); string errorMsg = $"Error while loading the save {selectedSave}";
if (client != null)
{
GameMain.Server?.SendDirectChatMessage($"{errorMsg}: {e.Message}\n{e.StackTrace}", client, ChatMessageType.Error);
}
DebugConsole.ThrowError(errorMsg, e);
return; return;
} }
DebugConsole.NewMessage("Campaign loaded!", Color.Cyan); DebugConsole.NewMessage("Campaign loaded!", Color.Cyan);
DebugConsole.NewMessage( DebugConsole.NewMessage(
GameMain.GameSession.Map.SelectedLocation == null ? GameMain.GameSession.Map.SelectedLocation == null ?
GameMain.GameSession.Map.CurrentLocation.Name : GameMain.GameSession.Map.CurrentLocation.DisplayName :
GameMain.GameSession.Map.CurrentLocation.Name + " -> " + GameMain.GameSession.Map.SelectedLocation.Name, Color.Cyan); GameMain.GameSession.Map.CurrentLocation.DisplayName + " -> " + GameMain.GameSession.Map.SelectedLocation.DisplayName, Color.Cyan);
} }
protected override void LoadInitialLevel() protected override void LoadInitialLevel()
@@ -188,7 +201,14 @@ namespace Barotrauma
} }
else else
{ {
LoadCampaign(saveFiles[saveIndex].FilePath); try
{
LoadCampaign(saveFiles[saveIndex].FilePath, client: null);
}
catch (Exception ex)
{
DebugConsole.ThrowError("Failed to load the campaign.", ex);
}
} }
}); });
} }
@@ -377,7 +397,7 @@ namespace Barotrauma
{ {
PendingSubmarineSwitch = null; PendingSubmarineSwitch = null;
GameMain.Server.EndGame(TransitionType.None, wasSaved: false); GameMain.Server.EndGame(TransitionType.None, wasSaved: false);
LoadCampaign(GameMain.GameSession.SavePath); LoadCampaign(GameMain.GameSession.SavePath, client: null);
LastSaveID++; LastSaveID++;
IncrementAllLastUpdateIds(); IncrementAllLastUpdateIds();
yield return CoroutineStatus.Success; yield return CoroutineStatus.Success;
@@ -1231,10 +1251,10 @@ namespace Barotrauma
{ {
foreach (CharacterInfo hireInfo in location.HireManager.PendingHires) foreach (CharacterInfo hireInfo in location.HireManager.PendingHires)
{ {
if (TryHireCharacter(location, hireInfo, sender)) if (TryHireCharacter(location, hireInfo, sender.Character, sender))
{ {
hiredCharacters.Add(hireInfo); hiredCharacters.Add(hireInfo);
}; }
} }
} }
@@ -66,7 +66,7 @@ namespace Barotrauma
{ {
foreach (Item item in slots[i].Items.ToList()) foreach (Item item in slots[i].Items.ToList())
{ {
if (!receivedItemIdsFromClient[i].Contains(item.ID)) if (!receivedItemIdsFromClient[i].Contains(item.ID) && item.IsInteractable(c.Character))
{ {
Item droppedItem = item; Item droppedItem = item;
Entity prevOwner = Owner; Entity prevOwner = Owner;
@@ -107,8 +107,7 @@ namespace Barotrauma
if (Entity.FindEntityByID(id) is not Item item || slots[i].Contains(item)) { continue; } if (Entity.FindEntityByID(id) is not Item item || slots[i].Contains(item)) { continue; }
if (item.GetComponent<Pickable>() is not Pickable pickable || if (item.GetComponent<Pickable>() is not Pickable pickable ||
(pickable.IsAttached && !pickable.PickingDone) || (pickable.IsAttached && !pickable.PickingDone) || item.AllowedSlots.None() || !item.IsInteractable(c.Character))
item.AllowedSlots.None())
{ {
DebugConsole.AddWarning($"Client {c.Name} tried to pick up a non-pickable item \"{item}\" (parent inventory: {item.ParentInventory?.Owner.ToString() ?? "null"})", DebugConsole.AddWarning($"Client {c.Name} tried to pick up a non-pickable item \"{item}\" (parent inventory: {item.ParentInventory?.Owner.ToString() ?? "null"})",
item.Prefab.ContentPackage); item.Prefab.ContentPackage);
@@ -161,6 +161,20 @@ namespace Barotrauma
msg.WriteUInt16(droppedItem.ID); msg.WriteUInt16(droppedItem.ID);
} }
break; break;
case SetHighlightEventData highlightEventData:
bool isTargetedForClient =
highlightEventData.TargetClients.IsEmpty ||
highlightEventData.TargetClients.Contains(c);
msg.WriteBoolean(isTargetedForClient);
if (isTargetedForClient)
{
msg.WriteBoolean(highlightEventData.Highlighted);
if (highlightEventData.Highlighted)
{
msg.WriteColorR8G8B8A8(highlightEventData.Color);
}
}
break;
default: default:
throw error($"Unsupported event type {itemEventData.GetType().Name}"); throw error($"Unsupported event type {itemEventData.GetType().Name}");
} }
@@ -1,4 +1,7 @@
using System.Collections.Generic; #nullable enable
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
@@ -16,4 +19,20 @@ partial class Item
Items = items.Distinct().ToImmutableArray(); Items = items.Distinct().ToImmutableArray();
} }
} }
public readonly struct SetHighlightEventData : IEventData
{
public EventType EventType => EventType.SetHighlight;
public readonly bool Highlighted;
public readonly Color Color;
public readonly ImmutableArray<Client> TargetClients;
public SetHighlightEventData(bool highlighted, Color color, IEnumerable<Client>? targetClients)
{
Highlighted = highlighted;
Color = color;
TargetClients = (targetClients ?? Enumerable.Empty<Client>()).ToImmutableArray();
}
}
} }
@@ -804,7 +804,7 @@ namespace Barotrauma.Networking
{ {
using (dosProtection.Pause(connectedClient)) using (dosProtection.Pause(connectedClient))
{ {
MultiPlayerCampaign.LoadCampaign(saveName); MultiPlayerCampaign.LoadCampaign(saveName, connectedClient);
} }
} }
} }
@@ -1230,11 +1230,6 @@ namespace Barotrauma.Networking
} }
c.LastRecvEntityEventID = lastRecvEntityEventID; c.LastRecvEntityEventID = lastRecvEntityEventID;
#warning TODO: remove this later
/*if (!CoroutineManager.IsCoroutineRunning("RoundRestartLoop"))
{
CoroutineManager.StartCoroutine(RoundRestartLoop(), "RoundRestartLoop");
}*/
} }
else if (lastRecvEntityEventID != c.LastRecvEntityEventID && GameSettings.CurrentConfig.VerboseLogging) else if (lastRecvEntityEventID != c.LastRecvEntityEventID && GameSettings.CurrentConfig.VerboseLogging)
{ {
@@ -1484,7 +1479,7 @@ namespace Barotrauma.Networking
{ {
using (dosProtection.Pause(sender)) using (dosProtection.Pause(sender))
{ {
MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath); MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath, sender);
} }
} }
} }
@@ -3945,7 +3940,6 @@ namespace Barotrauma.Networking
if (remainingJobs.None()) if (remainingJobs.None())
{ {
DebugConsole.ThrowError("Failed to assign a suitable job for bot \"" + c.Name + "\" (all jobs already have the maximum numbers of players). Assigning a random job..."); DebugConsole.ThrowError("Failed to assign a suitable job for bot \"" + c.Name + "\" (all jobs already have the maximum numbers of players). Assigning a random job...");
#warning TODO: is this randsync correct?
c.Job = Job.Random(Rand.RandSync.ServerAndClient); c.Job = Job.Random(Rand.RandSync.ServerAndClient);
assignedPlayerCount[c.Job.Prefab]++; assignedPlayerCount[c.Job.Prefab]++;
} }
@@ -426,8 +426,9 @@ namespace Barotrauma.Networking
} }
else else
{ {
double midRoundSyncTimeOut = uniqueEvents.Count / 100 * server.UpdateInterval.TotalSeconds; //assume we can get at least 10 events per second through
midRoundSyncTimeOut = Math.Max(10.0f, midRoundSyncTimeOut * 10.0f); double midRoundSyncTimeOut = uniqueEvents.Count / 10 * server.UpdateInterval.TotalSeconds;
midRoundSyncTimeOut = Math.Max(midRoundSyncTimeOut, server.ServerSettings.MinimumMidRoundSyncTimeout);
client.UnreceivedEntityEventCount = (UInt16)uniqueEvents.Count; client.UnreceivedEntityEventCount = (UInt16)uniqueEvents.Count;
client.NeedsMidRoundSync = true; client.NeedsMidRoundSync = true;
@@ -537,7 +537,7 @@ namespace Barotrauma.Networking
} }
//add the ID card tags they should've gotten when spawning in the shuttle //add the ID card tags they should've gotten when spawning in the shuttle
character.GiveIdCardTags(shuttleSpawnPoints[i], requireSpawnPointTagsNotGiven: false, createNetworkEvent: true); character.GiveIdCardTags(shuttleSpawnPoints[i], createNetworkEvent: true);
} }
} }
@@ -263,7 +263,7 @@ namespace Barotrauma
if (amountToChoose > viableTraitors.Count) if (amountToChoose > viableTraitors.Count)
{ {
DebugConsole.ThrowError( DebugConsole.ThrowError(
$"Error in traitor event {traitorEvent.Prefab.Identifier}. Not enough players to choose {amountToChoose} secondary traitors."+ $"Error in traitor event {traitorEvent.Prefab.Identifier}. Not enough players to choose {amountToChoose} secondary traitors. " +
$"Make sure the {nameof(traitorEvent.Prefab.MinPlayerCount)} of the event is high enough to support to desired amount of secondary traitors.", $"Make sure the {nameof(traitorEvent.Prefab.MinPlayerCount)} of the event is high enough to support to desired amount of secondary traitors.",
contentPackage: traitorEvent.Prefab.ContentPackage); contentPackage: traitorEvent.Prefab.ContentPackage);
amountToChoose = viableTraitors.Count; amountToChoose = viableTraitors.Count;
@@ -455,6 +455,15 @@ namespace Barotrauma
activeEvent.TraitorEvent.Prefab, activeEvent.TraitorEvent.Prefab,
activeEvent.TraitorEvent.CurrentState, activeEvent.TraitorEvent.CurrentState,
activeEvent.Traitor)); activeEvent.Traitor));
if (activeEvent.TraitorEvent.CurrentState == TraitorEvent.State.Completed)
{
SteamAchievementManager.OnTraitorWin(activeEvent.TraitorEvent.Traitor?.Character);
foreach (var secondaryTraitor in activeEvent.TraitorEvent.SecondaryTraitors)
{
SteamAchievementManager.OnTraitorWin(secondaryTraitor?.Character);
}
}
} }
if (previousTraitorEvents.Count > MaxPreviousEventHistory) if (previousTraitorEvents.Count > MaxPreviousEventHistory)
{ {
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>1.2.1.0</Version> <Version>1.2.4.0</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright> <Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
@@ -91,6 +91,11 @@ namespace Barotrauma
private IEnumerable<Hull> visibleHulls; private IEnumerable<Hull> visibleHulls;
private float hullVisibilityTimer; private float hullVisibilityTimer;
const float hullVisibilityInterval = 0.5f; const float hullVisibilityInterval = 0.5f;
/// <summary>
/// Returns hulls that are visible to the character, including the current hull.
/// Note that this is not an accurate visibility check, it only checks for open gaps between the adjacent and linked hulls.
/// </summary>
public IEnumerable<Hull> VisibleHulls public IEnumerable<Hull> VisibleHulls
{ {
get get
@@ -353,7 +358,7 @@ namespace Barotrauma
public static void UnequipContainedItems(Character character, Item parentItem, Func<Item, bool> predicate, bool avoidDroppingInSea = true, int? unequipMax = null) public static void UnequipContainedItems(Character character, Item parentItem, Func<Item, bool> predicate, bool avoidDroppingInSea = true, int? unequipMax = null)
{ {
var inventory = parentItem.OwnInventory; var inventory = parentItem.OwnInventory;
if (inventory == null) { return; } if (inventory == null || !inventory.Container.DrawInventory) { return; }
int removed = 0; int removed = 0;
if (predicate == null || inventory.AllItems.Any(predicate)) if (predicate == null || inventory.AllItems.Any(predicate))
{ {
@@ -262,7 +262,7 @@ namespace Barotrauma
if (aiElements.Count == 0) if (aiElements.Count == 0)
{ {
DebugConsole.ThrowError("Error in file \"" + c.Params.File + "\" - no AI element found.", DebugConsole.ThrowError("Error in file \"" + c.Params.File.Path + "\" - no AI element found.",
contentPackage: c.Prefab?.ContentPackage); contentPackage: c.Prefab?.ContentPackage);
outsideSteering = new SteeringManager(this); outsideSteering = new SteeringManager(this);
insideSteering = new IndoorsSteeringManager(this, false, false); insideSteering = new IndoorsSteeringManager(this, false, false);
@@ -312,7 +312,7 @@ namespace Barotrauma
} }
ReevaluateAttacks(); ReevaluateAttacks();
outsideSteering = new SteeringManager(this); outsideSteering = new SteeringManager(this);
insideSteering = new IndoorsSteeringManager(this, Character.Params.AI.CanOpenDoors, canAttackDoors); insideSteering = new IndoorsSteeringManager(this, AIParams.CanOpenDoors, canAttackDoors);
steeringManager = outsideSteering; steeringManager = outsideSteering;
State = AIState.Idle; State = AIState.Idle;
requiredHoleCount = (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderWidth) / Structure.WallSectionSize); requiredHoleCount = (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderWidth) / Structure.WallSectionSize);
@@ -322,6 +322,10 @@ namespace Barotrauma
} }
private CharacterParams.AIParams _aiParams; private CharacterParams.AIParams _aiParams;
/// <summary>
/// Shorthand for <see cref="Character.Params.AI"/> with null checking.
/// </summary>
/// <returns><see cref="Character.Params.AI"/> or an empty params. Does not return nulls.</returns>
public CharacterParams.AIParams AIParams public CharacterParams.AIParams AIParams
{ {
get get
@@ -565,7 +569,7 @@ namespace Barotrauma
} }
} }
if (Character.Params.UsePathFinding && Character.Params.AI.UsePathFindingToGetInside && AIParams.CanOpenDoors) if (Character.Params.UsePathFinding && AIParams.UsePathFindingToGetInside && AIParams.CanOpenDoors)
{ {
// Meant for monsters outside the player sub that target something inside the sub and can use the doors to access the sub (Husk). // Meant for monsters outside the player sub that target something inside the sub and can use the doors to access the sub (Husk).
bool IsCloseEnoughToTargetSub(float threshold) => SelectedAiTarget?.Entity?.Submarine is Submarine sub && sub != null && Vector2.DistanceSquared(Character.WorldPosition, sub.WorldPosition) < MathUtils.Pow(Math.Max(sub.Borders.Size.X, sub.Borders.Size.Y) / 2 + threshold, 2); bool IsCloseEnoughToTargetSub(float threshold) => SelectedAiTarget?.Entity?.Submarine is Submarine sub && sub != null && Vector2.DistanceSquared(Character.WorldPosition, sub.WorldPosition) < MathUtils.Pow(Math.Max(sub.Borders.Size.X, sub.Borders.Size.Y) / 2 + threshold, 2);
@@ -3097,7 +3101,10 @@ namespace Barotrauma
break; break;
} }
valueModifier *= targetMemory.Priority / (float)Math.Sqrt(dist); valueModifier *=
targetMemory.Priority /
//sqrt = the further the target is, the less the distance matters
MathF.Sqrt(dist);
if (valueModifier > targetValue) if (valueModifier > targetValue)
{ {
@@ -685,8 +685,8 @@ namespace Barotrauma
} }
if (removeDivingSuit) if (removeDivingSuit)
{ {
var divingSuit = Character.Inventory.FindItemByTag(Tags.HeavyDivingGear); var divingSuit = Character.Inventory.FindEquippedItemByTag(Tags.HeavyDivingGear);
if (divingSuit != null && !divingSuit.HasTag(Tags.DivingGearWearableIndoors)) if (divingSuit != null && !divingSuit.HasTag(Tags.DivingGearWearableIndoors) && divingSuit.IsInteractable(Character))
{ {
if (shouldActOnSuffocation || Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) if (shouldActOnSuffocation || Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
{ {
@@ -728,51 +728,48 @@ namespace Barotrauma
} }
if (takeMaskOff) if (takeMaskOff)
{ {
if (Character.HasEquippedItem(Tags.LightDivingGear)) var mask = Character.Inventory.FindEquippedItemByTag(Tags.LightDivingGear);
if (mask != null)
{ {
var mask = Character.Inventory.FindItemByTag(Tags.LightDivingGear); if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List<InvSlotType>() { InvSlotType.Any }))
if (mask != null)
{ {
if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List<InvSlotType>() { InvSlotType.Any })) if (Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
{ {
if (Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) mask.Drop(Character);
HandleRelocation(mask);
ReequipUnequipped();
}
else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask)
{
findItemState = FindItemState.DivingMask;
if (FindSuitableContainer(mask, out Item targetContainer))
{ {
mask.Drop(Character); findItemState = FindItemState.None;
HandleRelocation(mask); itemIndex = 0;
ReequipUnequipped(); if (targetContainer != null)
}
else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask)
{
findItemState = FindItemState.DivingMask;
if (FindSuitableContainer(mask, out Item targetContainer))
{ {
findItemState = FindItemState.None; var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
itemIndex = 0; decontainObjective.Abandoned += () =>
if (targetContainer != null)
{ {
var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () =>
{
ReequipUnequipped();
IgnoredItems.Add(targetContainer);
};
decontainObjective.Completed += () => ReequipUnequipped();
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
else
{
mask.Drop(Character);
HandleRelocation(mask);
ReequipUnequipped(); ReequipUnequipped();
} IgnoredItems.Add(targetContainer);
};
decontainObjective.Completed += () => ReequipUnequipped();
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
else
{
mask.Drop(Character);
HandleRelocation(mask);
ReequipUnequipped();
} }
} }
} }
else }
{ else
ReequipUnequipped(); {
} ReequipUnequipped();
} }
} }
} }
@@ -784,13 +781,11 @@ namespace Barotrauma
if (findItemState == FindItemState.None || findItemState == FindItemState.OtherItem) if (findItemState == FindItemState.None || findItemState == FindItemState.OtherItem)
{ {
for (int i = 0; i < 2; i++) foreach (Item item in Character.HeldItems)
{ {
var hand = i == 0 ? InvSlotType.RightHand : InvSlotType.LeftHand; if (item == null || !item.IsInteractable(Character)) { continue; }
Item item = Character.Inventory.GetItemInLimbSlot(hand);
if (item == null) { continue; }
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Any }) && Character.Submarine?.TeamID == Character.TeamID ) if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, CharacterInventory.AnySlot) && Character.Submarine?.TeamID == Character.TeamID)
{ {
if (item.AllowedSlots.Contains(InvSlotType.Bag) && Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Bag })) { continue; } if (item.AllowedSlots.Contains(InvSlotType.Bag) && Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Bag })) { continue; }
findItemState = FindItemState.OtherItem; findItemState = FindItemState.OtherItem;
@@ -1389,7 +1384,10 @@ namespace Barotrauma
// Don't react to friendly enemy AI attacking other characters. E.g. husks attacking someone when whe are a cultist. // Don't react to friendly enemy AI attacking other characters. E.g. husks attacking someone when whe are a cultist.
continue; continue;
} }
bool isWitnessing = otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) || otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull); bool isWitnessing =
otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) ||
otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull) ||
otherCharacter.CanSeeTarget(attacker, seeThroughWindows: true);
if (!isWitnessing) if (!isWitnessing)
{ {
//if the other character did not witness the attack, and the character is not within report range (or capable of reporting) //if the other character did not witness the attack, and the character is not within report range (or capable of reporting)
@@ -1754,11 +1752,11 @@ namespace Barotrauma
if (otherCharacter == character || otherCharacter.TeamID == character.TeamID || otherCharacter.IsDead || if (otherCharacter == character || otherCharacter.TeamID == character.TeamID || otherCharacter.IsDead ||
otherCharacter.Info?.Job == null || otherCharacter.Info?.Job == null ||
otherCharacter.AIController is not HumanAIController otherHumanAI || otherCharacter.AIController is not HumanAIController otherHumanAI ||
!otherHumanAI.VisibleHulls.Contains(character.CurrentHull)) Vector2.DistanceSquared(otherCharacter.WorldPosition, character.WorldPosition) > 1000.0f * 1000.0f)
{ {
continue; continue;
} }
if (!otherCharacter.CanSeeTarget(character)) { continue; } if (!otherCharacter.CanSeeTarget(character, seeThroughWindows: true)) { continue; }
if (!otherHumanAI.structureDamageAccumulator.ContainsKey(character)) { otherHumanAI.structureDamageAccumulator.Add(character, 0.0f); } if (!otherHumanAI.structureDamageAccumulator.ContainsKey(character)) { otherHumanAI.structureDamageAccumulator.Add(character, 0.0f); }
float prevAccumulatedDamage = otherHumanAI.structureDamageAccumulator[character]; float prevAccumulatedDamage = otherHumanAI.structureDamageAccumulator[character];
@@ -1840,13 +1838,13 @@ namespace Barotrauma
foreach (Character otherCharacter in Character.CharacterList) foreach (Character otherCharacter in Character.CharacterList)
{ {
if (otherCharacter == thief || otherCharacter.TeamID == thief.TeamID || otherCharacter.IsIncapacitated || otherCharacter.Stun > 0.0f || if (otherCharacter == thief || otherCharacter.TeamID == thief.TeamID || otherCharacter.IsIncapacitated || otherCharacter.Stun > 0.0f ||
otherCharacter.Info?.Job == null || !(otherCharacter.AIController is HumanAIController otherHumanAI) || otherCharacter.Info?.Job == null || otherCharacter.AIController is not HumanAIController otherHumanAI ||
!otherHumanAI.VisibleHulls.Contains(thief.CurrentHull)) Vector2.DistanceSquared(otherCharacter.WorldPosition, thief.WorldPosition) > 1000.0f * 1000.0f)
{ {
continue; continue;
} }
//if (!otherCharacter.IsFacing(thief.WorldPosition)) { continue; } //if (!otherCharacter.IsFacing(thief.WorldPosition)) { continue; }
if (!otherCharacter.CanSeeTarget(thief)) { continue; } if (!otherCharacter.CanSeeTarget(thief, seeThroughWindows: true)) { continue; }
// Don't react if the player is taking an extinguisher and there's any fires on the sub, or diving gear when the sub is flooding // Don't react if the player is taking an extinguisher and there's any fires on the sub, or diving gear when the sub is flooding
// -> allow them to use the emergency items // -> allow them to use the emergency items
if (thief.Submarine != null) if (thief.Submarine != null)
@@ -1070,7 +1070,8 @@ namespace Barotrauma
{ {
// Try reload ammunition from inventory // Try reload ammunition from inventory
static bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag(Tags.MobileRadio); static bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag(Tags.MobileRadio);
Item ammunition = character.Inventory.FindItem(i => i.HasIdentifierOrTags(ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i), recursive: true); Item ammunition = character.Inventory.FindItem(i =>
i.HasIdentifierOrTags(ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i) && i.IsInteractable(character), recursive: true);
if (ammunition != null) if (ammunition != null)
{ {
var container = Weapon.GetComponent<ItemContainer>(); var container = Weapon.GetComponent<ItemContainer>();
@@ -1089,6 +1090,9 @@ namespace Barotrauma
} }
else if (!HoldPosition && IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers != null) else if (!HoldPosition && IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers != null)
{ {
// Inventory not drawn = it's not interactable
// If the weapon is empty and the inventory is inaccessible, it can't be reloaded
if (!Weapon.OwnInventory.Container.DrawInventory) { return false; }
SeekAmmunition(ammunitionIdentifiers); SeekAmmunition(ammunitionIdentifiers);
} }
return false; return false;

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